Skip to content

Add 2D extrusion support for fabrication outputs#3

Merged
xarthurx merged 21 commits into
mainfrom
feat-2d-extrusion-feature
Apr 22, 2026
Merged

Add 2D extrusion support for fabrication outputs#3
xarthurx merged 21 commits into
mainfrom
feat-2d-extrusion-feature

Conversation

@xarthurx
Copy link
Copy Markdown
Owner

Summary

  • add standalone extrude_2d with contour tracing, polygonization, cap triangulation, and prism mesh assembly for 2D density/SDF fields
  • add xtf extrude CLI support plus package-root export, README usage, and project-memory documentation
  • add focused extrusion unit/property/integration/CLI coverage, then follow up with a fix for python -m xeltofab.cli invocation

Verification

  • uv run pytest -q
  • uv run ruff check .
  • uv run ruff format --check .
  • uv run ty check src/xeltofab/extrude.py src/xeltofab/cli.py
  • uv run xtf extrude data/examples/beams_2d_25x50_sample0.npy -o /tmp/smoke.stl -t 10
  • PYTHONWARNINGS=error uv run python -m xeltofab.cli extrude data/examples/beams_2d_25x50_sample0.npy -o /tmp/xeltofab_module_strict.stl -t 10
  • rebuilt wheel/sdist install smokes for xtf extrude and python -m xeltofab.cli extrude

Notes

  • local main was reset back to origin/main; the work now lives on feat-2d-extrusion-feature
  • no version bump or tag was added in this branch

xarthurx added 15 commits April 21, 2026 15:26
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
Copilot AI review requested due to automatic review settings April 21, 2026 19:59
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xel-to-fab Ready Ready Preview, Comment Apr 22, 2026 9:01am

This comment was marked as outdated.

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread website/package.json
Comment thread src/xeltofab/extrude.py
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.
@xarthurx xarthurx merged commit 43c3a67 into main Apr 22, 2026
3 checks passed
@xarthurx xarthurx mentioned this pull request Apr 22, 2026
@xarthurx xarthurx deleted the feat-2d-extrusion-feature branch May 8, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants