Add 2D extrusion support for fabrication outputs#3
Merged
Conversation
Bootstrap the standalone extrusion surface so the remaining TDD tasks can build on a real public entrypoint. This adds the new module stub, exports the API from the package root, installs the geometry dependencies needed by the design, and makes the project-local type checker available so the required verification command can run inside uv. Constraint: The feature must stay outside pipeline.py/state.py/extract.py per the approved design Constraint: The workflow requires uv-managed type checks with ty before each task commit Rejected: Delay dependency installation until the first geometry task | would keep the smoke test red for the wrong reason Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep extrude_2d standalone; do not backdoor it into the existing 2D pipeline branch Tested: uv sync; uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py src/xeltofab/__init__.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py src/xeltofab/__init__.py; uv run ty check src/xeltofab/extrude.py src/xeltofab/__init__.py Not-tested: No geometry behavior yet beyond import/export scaffolding
Establish the public guardrails before any geometry logic lands so callers get fast, deterministic failures for invalid inputs. This keeps later helper tests focused on extrusion behavior rather than stub exceptions. Constraint: Public API errors must match the approved design doc strings closely enough for CLI wrapping and tests Rejected: Leave validation until the end-to-end task | would blur invalid-input failures with unfinished geometry work Confidence: high Scope-risk: narrow Reversibility: clean Directive: Preserve these entrypoint checks at the top of extrude_2d even if helpers are refactored later Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: Geometry behavior remains stubbed
Implement the preprocessing subset that extrusion owns: Gaussian smoothing, field-type-aware thresholding, optional pinhole cleanup, and small-component filtering. The helper mirrors preprocess.py behavior where appropriate while keeping the extrusion-only min_component_area contract explicit. Constraint: xelToFab's installed scikit-image uses remove_small_objects(max_size=...) semantics, not min_size Rejected: Reuse preprocess() through a synthetic PipelineState | cannot represent SDF thresholds or the extrusion-specific island filter knob cleanly Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep _build_binary and preprocess.py behavior aligned for shared density settings; the parity test later depends on this Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: End-to-end contour and mesh generation still unimplemented
Trace extrusion boundaries from the binary mask with the same marching-squares backend the repo already uses for 2D extraction, but normalize coordinates immediately into canonical (x, y) geometry space. The zero pad guarantees closed loops even for material that touches the image boundary. Constraint: find_contours emits (row, col) and must be normalized before shapely sees the data Rejected: Keep native row/col ordering until mesh assembly | would propagate axis ambiguity and make clipping rectangles easy to get wrong Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat _trace_contours as the only place allowed to convert from skimage row/col coordinates into geometry coordinates Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: Polygonization and mesh assembly are still pending
Turn traced contour loops into clean polygonal regions that preserve holes and clip back to the image rectangle when the source material touches the domain boundary. The implementation assigns CW rings to their containing CCW shell, then normalizes shapely's post-intersection output back to a MultiPolygon for downstream mesh construction. Constraint: Hole loops cannot be modeled as standalone Polygon objects and unioned; that fills the void instead of preserving it Rejected: Build one Polygon per contour and rely on unary_union to infer holes | fails for nested rings and loses the extrusion voids Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep contour coordinates canonical (x, y) and clip with box(0, 0, width - 1, height - 1) Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: Earcut triangulation and 3D assembly are still pending
Triangulate each cleaned polygon cap with earcut so the later prism builder can reuse the same 2D vertices for bottom, top, and wall construction. The helper drops shapely's closing duplicate per ring and preserves hole rings in the ordering earcut expects. Constraint: mapbox-earcut expects cumulative ring-end offsets and treats every coordinate row as a distinct vertex Rejected: Hand-roll cap triangulation | unnecessary complexity when earcut already handles polygons with holes Confidence: high Scope-risk: narrow Reversibility: clean Directive: Never pass shapely's duplicated closing coordinate into earcut; it creates degenerate vertices and unstable indexing Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: 3D cap orientation and side-wall assembly remain pending
Assemble the cleaned 2D polygons into closed prisms by reusing the cap triangulation for top and bottom faces and stitching every ring edge into side walls. The implementation uses the corrected cap winding (bottom reversed, top kept) and relies on ring orientation so holes generate inward cavity walls with outward-facing normals. Constraint: A CCW triangle in the xy-plane points +z, so the bottom cap must reverse earcut winding for outward normals Rejected: Swap x and y late in the pipeline | that would introduce a reflection and force a second round of winding fixes Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve the ring order emitted by _triangulate_polygon when emitting walls; holes depend on that orientation Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: Public extrude_2d wiring and real fixture integration are still pending
Replace the public stub with the full extrusion pipeline: binary cleanup, contour tracing, polygonization, and prism assembly. While wiring the helpers into the public API, the end-to-end tests exposed a real shapely post-clipping behavior: intersection/buffer can flip ring orientation, so the polygon output is normalized before meshing to keep cap and wall winding consistent. Constraint: The public extrusion path must stay standalone and avoid pipeline.py/state.py/extract.py changes Rejected: Fix negative volumes by swapping axes at the end | would mask the real winding problem and introduce a reflected coordinate frame Confidence: high Scope-risk: moderate Reversibility: clean Directive: After any shapely cleanup or clipping step, re-orient polygons before using ring direction for 3D face emission Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format src/xeltofab/extrude.py tests/test_extrude.py; uv run ruff check src/xeltofab/extrude.py tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: CLI and real-fixture integration remain pending
Lock in two high-signal invariants for the public extrusion API: thicker extrusions scale volume linearly, and projected area stays close to the source material area despite marching-squares subpixel boundaries. These checks make future geometry refactors less likely to silently break physically meaningful behavior. Constraint: Marching-squares boundaries live on subpixel offsets, so area assertions must stay qualitative rather than exact Rejected: Assert exact projected area formulas for traced fields | too brittle against contour discretization and likely to flake Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep future extrusion tests tolerant of marching-squares half-pixel geometry unless the tracing algorithm itself changes Tested: uv run pytest tests/test_extrude.py -v; uv run ruff format tests/test_extrude.py; uv run ruff check tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: No new CLI or fixture coverage in this task
Validate the public extrusion path on the checked-in Beams2D fixtures and lock in parity between extrusion's binary preprocessing and the existing density pipeline behavior. This catches regressions where the standalone extrusion cleanup drifts away from preprocess.py or becomes impractical on real dataset sizes. Constraint: The parity check must reflect preprocess.py's max_size-based small-component semantics exactly Rejected: Limit coverage to synthetic fields | would miss real contour complexity and fixture-scale performance surprises Confidence: high Scope-risk: narrow Reversibility: clean Directive: If preprocess.py changes its morphology or component filter semantics, update _build_binary and this parity test in the same change Tested: uv run pytest tests/test_extrude.py -v; uv run ruff check tests/test_extrude.py; uv run ruff format tests/test_extrude.py; uv run ty check src/xeltofab/extrude.py Not-tested: CLI wiring remains out of scope for this task
Expose the standalone extrusion API through the CLI so 2D scalar fields can go straight from the existing loader stack to fabrication-ready mesh files. The command mirrors the Python API options, rejects 3D inputs with a friendly handoff to `xtf process`, and keeps output format inference delegated to trimesh via the destination suffix. Constraint: The CLI must reuse load_field instead of bypassing repo loaders for .mat/.npz/.csv inputs Rejected: Route 2D extrusion through the existing process command | conflicts with the deliberate standalone design and existing 2D contour behavior Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep CLI field_type typed as Literal[density, sdf] so ty can verify the handoff into extrude_2d Tested: uv run pytest tests/test_cli_extrude.py -v; uv run pytest -v; uv run ruff format src/xeltofab/cli.py tests/test_cli_extrude.py; uv run ruff check .; uv run ty check src/xeltofab/extrude.py src/xeltofab/cli.py Not-tested: Manual shell smoke against the real Beams2D fixture is deferred to final verification
Document the new extrusion workflow where users already look first: the Quick
Start section. The examples cover both the CLI path and the Python API so the
feature is discoverable for dataset users and library consumers alike.
Constraint: Website docs are intentionally out of scope for this implementation pass
Rejected: Leave usage details only in code/tests | too hard for new users to discover the new command and API
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep README examples aligned with the actual CLI flags and public import surface whenever extrude_2d changes
Tested: uv run python -c "import pathlib; print(pathlib.Path('README.md').read_text())" | head -80; uv run ty check src/xeltofab/extrude.py src/xeltofab/cli.py
Not-tested: Markdown renderer-specific formatting outside the local text preview
Capture the new standalone extrusion workflow and the two failure modes that were easy to miss during implementation: keeping preprocess parity in sync and re-orienting polygons after shapely cleanup before using ring winding for 3D face emission. This preserves the feature's rationale for the next maintainer. Constraint: PROGRESS entries must record the concrete fix commit and prevention guidance immediately after the work lands Rejected: Leave the learning only in code comments and tests | easier for future work to miss the same geometry pitfall Confidence: high Scope-risk: narrow Reversibility: clean Directive: When extrusion geometry changes, update PROGRESS alongside code if the change teaches a non-obvious lesson Tested: Manual review of docs/PROGRESS.md entry against landed commits and final verification results Not-tested: No executable behavior change; documentation-only commit
Fresh verification uncovered that the installed console script worked but direct module invocation did nothing, because the Click application module defined the commands without ever calling the entrypoint when executed as `python -m`. Add the standard `__main__` handoff and lock it with a regression test so both launch paths behave the same. Constraint: The existing `xtf` console script entrypoint must keep working unchanged Rejected: Ignore module invocation because the console script already works | verification found a real broken launch path that is cheap to support Confidence: high Scope-risk: narrow Reversibility: clean Directive: Any future CLI module expected to support `python -m ...` needs an explicit `if __name__ == "__main__": main()` guard plus a test Tested: uv run pytest tests/test_cli_extrude.py -v; uv run ruff check src/xeltofab/cli.py tests/test_cli_extrude.py; uv run ruff format src/xeltofab/cli.py tests/test_cli_extrude.py; uv run ty check src/xeltofab/extrude.py src/xeltofab/cli.py Not-tested: Full repository regression suite not rerun in this bugfix commit
Fresh verification uncovered a non-obvious CLI gap after the main extrusion feature landed: `python -m xeltofab.cli` defined commands but never executed the Click app. Record the bug, root cause, fix commit, and prevention rule so future CLI work does not regress the alternate launch path. Constraint: PROGRESS entries must capture post-implementation bugs and their fix commits once discovered Rejected: Leave the follow-up fix undocumented | makes it too easy to reintroduce the same missing `__main__` handoff later Confidence: high Scope-risk: narrow Reversibility: clean Directive: If a CLI module is intended to support module execution, keep a regression test and a `__main__` handoff together Tested: Manual audit of docs/PROGRESS.md entry against commit df74c99 and module-invocation verification output Not-tested: Documentation-only commit; no runtime behavior change
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Adds dedicated reference pages for the 2D→3D extrusion feature and surfaces it across existing docs: - new api/extrude-2d.mdx — full signature, parameters, how-it-works, when-to-use, examples, and errors/warnings - new cli/extrude.mdx — usage, options table, and worked examples - sidebars updated: extrude lands after process in CLI; extrude-2d lands after process-from-sdf in API - quick-start gains a 2D→3D extrusion subsection pointing at both the Python and CLI entrypoints - pipeline-overview no longer dead-ends on `save_mesh()` for 2D; it now directs readers to extrude_2d when they want a 3D mesh - index.mdx key-features and stage-3 bullet reworded so 2D is not framed as contours-only Verified with `bun run types:check` and `bun run build`; both new routes appear in the generated static page list.
- add two new generators in scripts/generate_doc_images.py: gen_extrude_2d_contour (2-panel: input field + traced shells/holes colored by CCW/CW orientation) and gen_extrude_2d_mesh (iso-view pyvista render of extrude_2d output, smooth-shaded, no edges — the earcut fan triangulation is visually noisy and not informative here) - both use beams_2d_100x200_sample1.npy which covers the full domain - api/extrude-2d.mdx: hero mesh image above Import, contour image at the end of "How it works" as a visual anchor for the pipeline steps - quick-start.mdx: small mesh image inside the 2D→3D extrusion block
extrude_2d was running find_contours on the binarized mask, which discards all sub-pixel information from the pre-threshold smoothing and forces every traced contour onto pixel edges. The consequence was visible as staircase zigzag on any oblique sidewall of the extruded mesh. Root-cause fix: add _build_signed_field(field, binary, ...) which rebuilds the continuous signed field (positive inside, zero on boundary) and only clamps pixels that the cleanup steps (fill_holes, min_component_area) actively removed — natural boundaries stay symmetric so a solid binary block still traces at integer pixel positions. _trace_contours now dispatches on dtype: bool keeps the old pixel-aligned behavior (backward-compatible for the existing white-box tests), float traces the zero-iso of the signed field with sub-pixel precision. extrude_2d drives the float path. Also fixes the docs 3D render: smooth_shading=True was averaging normals across the cap-to-wall seam and falsely banding the cap. Switched to flat shading so each cap facet uses its own +z normal and the (mathematically planar) top reads as uniformly lit. Regenerated both docs images; all 269 tests pass; PROGRESS.md records the root cause and prevention guidance.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 21 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Previously, negative values were silently treated as a no-op via the > 0 guards in _build_binary. That masked typos at the public API boundary; docs present both knobs as non-negative. Now extrude_2d raises ValueError on negative input, matching the existing thickness <= 0 validation style. Added matching tests. Suggested by Copilot review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
extrude_2dwith contour tracing, polygonization, cap triangulation, and prism mesh assembly for 2D density/SDF fieldsxtf extrudeCLI support plus package-root export, README usage, and project-memory documentationpython -m xeltofab.cliinvocationVerification
uv run pytest -quv run ruff check .uv run ruff format --check .uv run ty check src/xeltofab/extrude.py src/xeltofab/cli.pyuv run xtf extrude data/examples/beams_2d_25x50_sample0.npy -o /tmp/smoke.stl -t 10PYTHONWARNINGS=error uv run python -m xeltofab.cli extrude data/examples/beams_2d_25x50_sample0.npy -o /tmp/xeltofab_module_strict.stl -t 10xtf extrudeandpython -m xeltofab.cli extrudeNotes
mainwas reset back toorigin/main; the work now lives onfeat-2d-extrusion-feature