Skip to content

fix(booleans): manifold exports for multi-tool differences - #838

Merged
ecto merged 7 commits into
mainfrom
claude/bold-jemison-d71ab9
Aug 28, 2026
Merged

fix(booleans): manifold exports for multi-tool differences#838
ecto merged 7 commits into
mainfrom
claude/bold-jemison-d71ab9

Conversation

@ecto

@ecto ecto commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the shell-ring reproducer from rana (cad/parts/shell-ring.loon, rana e32e7a2): [difference TOOL SUBJECT] with a multi-stage union tool (bore + groove + circular-patterned slots + notch) exported with dropped cut stages, retained interior cap faces, and ~200 edges not shared by exactly two triangles — so the part looked open in cross-section but sliced as fused.

Builds on the non-manifold mesh-boolean session (claude/distracted-brattain-0aca56, included here — it fixed the dropped union stages; this PR closes the manifoldness half).

What changed

  • vcad-kernel-tessellate::repair_watertightness — a gated mesh repair pipeline (duplicate-facet drop, membrane strip via ray parity, redundant-patch peel with a directed edge ledger, slit-rail snap, min-area loop fill + rim-pair zip, micro/pinch collapse, carve-and-refill). Every pass reverts unless the defective-edge count strictly drops and enclosed volume survives; the pipeline engages only on structural damage (severity-gated), and only at export boundaries — never mid-pipeline, where tessellations feed classification decisions.
  • Mesh-CSG fallback repairs its own output between two quadric projection passes and strips operand membranes, so parity classification and the MC volume oracle see sound input. Chained triangle-soup input skips the heavy passes.
  • Watertightness-swap triggers: wide cracks (rail gap > 0.5 mm), a missed-cut probe of tool ∩ subject, and doubled differences at scale — all above the known-good catalogue signatures (b1's analytic wall and the cone×cylinder overlaps stay analytic; guarded by the boolean fidelity baseline). Volume agreement also accepts the operand-implied MC prediction, since a doubled-sheet analytic result double-counts its own volume.
  • crates/vcad-kernel-tessellate/src/mesh_ray.rs: ray-parity machinery moved from vcad-kernel-booleans::mesh into the tessellate crate (booleans re-exports).

Tests

  • New end-to-end regression crates/vcad-eval/tests/shell_ring_manifold.rs: all three authored forms (pipe of unions, flat union tool, chained single-tool differences) must be edge-manifold with material absent inside every cutter and present in the walls.
  • Green: vcad-kernel-booleans, vcad-kernel-tessellate, vcad-kernel, vcad-eval (incl. torr catalogue + b1 guards), vcad-loon, vcad-torture --release (fidelity baseline unchanged), cargo clippy -D warnings, cargo fmt --check.

🤖 Generated with Claude Code

ecto and others added 5 commits August 28, 2026 07:39
Field report: exported STLs of printed parts carried hundreds to thousands
of edges not shared by exactly two triangles. Bambu Studio flagged them and
its auto-repair resolved them by FILLING — a printed rotor came out with no
shaft hole. Vertex welding did not move the count (flat from 1e-4 to 1e-1):
the defect is topological.

The reported repro never reached the mesh fallback. It came out of the
analytic B-rep path, from two bugs in the planar circle splitter, both
about a face's HOLES:

1. Existing hole loops were routed to the disk or the ring sub-face by
   testing the hole's CENTROID against the splitting circle. Every
   concentric loop — whatever its radius — has its centroid at the shared
   center, so a hole larger than the circle went to the disk: a face whose
   hole is bigger than its own outer loop, which the tessellator draws as
   the full disk (a membrane over the bore). Replaced with a real
   containment test on the loop's vertices (`loop_vs_circle`).

2. A circle lying entirely inside one of the face's holes counted as
   "inside the face" — only the outer loop was consulted — splitting off a
   disk over the hole plus a redundant nested hole on the ring.

Both produce doubled surface, which is exactly what a slicer reports.
Nested annular caps are what a union-of-differences makes, which is why
this only showed up on stacked rings.

Measured: the reported case goes 1902 -> 0 bad edges with volume 16965.4
against 16965.8 analytic truth; a real backplate 2063 -> 22 (and those 22
are unpaired hairline seams, no doubled surface).

Also, so a doubled result can never again pass silently:
- MeshReport/BooleanReport gain `overused_edges`. The existing `open_edges`
  is a NET DIRECTED count, so it cancels to zero on a doubled surface and
  could not see this defect at all.
- Over-used edges now open the mesh fallback on their own, with no
  capability flag required, and the fallback is taken only when it is
  strictly better: manifold and volume-agreeing.
- The mesh CSG cancels coincident triangle pairs (zero-thickness flaps).

A second, distinct defect is captured with its diagnosis and an #[ignore]d
test in coplanar_boss_straddling_a_split.rs: a boss whose caps are flush
with the body's goes non-manifold when its footprint straddles a line an
earlier boolean split clear across that face (gear teeth). Bisected to a
~3 degree band opposite an earlier tooth; a small overhang avoids it
(72-tooth blank: 1286 -> 31 bad edges).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rism

`[cylinder-n 7.5 24.0 6]` looks like a hex boss in source review and is
not one: the third argument is a fidelity hint for the boolean and seam
machinery, the surface stays an analytic cylinder, and the tessellator
draws a circle. That lie shipped a "hex drive" that was a plain round
bore and cost a print. Worse, for a bare primitive the count never
reaches tessellation at all — a cylinder exports at 32 segments whether
the source says 8, 16, 48 or 96 — while lib.loon told authors to reach
for these forms when bore fidelity is load-bearing.

The -n forms now refuse counts under 8 and name the primitive that does
make facets. The kernel already had a true faceted Prism, so this adds
`polygon-prism sides radius height` and `hex-prism across-flats height`
(across-flats: what a wrench measures), and rewrites the lib.loon
docstring to say what the hint actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new over-used-edge check was applied to BOTH fallback swaps, not just
the one it was written for. Five torture cases (chain-00, chain-08,
chain-10, rand-072, rand-220) pass precisely BECAUSE they take the
watertightness swap, and a mesh fallback that closes every crack while
carrying a few over-used edges was suddenly rejected — handing back the
cracked B-rep and turning them into bad-geometry.

Each swap is now judged against the defect it repairs and only that one:
the non-manifold swap still demands a manifold fallback, the
watertightness swap is back to its original acceptance (watertight,
non-empty, volume-agreeing). The watertightness swap also reports the
fallback's real `overused_edges` rather than an assumed zero.

Torture track: no regressions, 6 improvements vs baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The non-manifold swap this branch added rested on a claim I have now
disproved: that an edge shared by more than two triangles is never
legitimate geometry. Instrumenting this crate's own catalogue shows
known-good results scoring up to 13 over-used edges — b1's blade cut
scores 1 — against 11 and 14 for the doubled-surface defects that
motivated the measurement. The populations overlap exactly the way they
do for open edges, so no count separates them, and the swap did what the
comment above it warns about: it traded b1's analytic r45 wall for
coarse soup and lost 505 mm³ (torr_boolean_catalogue::b1_difference_dual
and b1_partition_identity, both failing on Rust nightly CI).

`overused_edges` stays on MeshReport and BooleanReport, as advisory as
open_edges and documented that way. It earns its place by being the only
number that sees this class at all — open_edges is a NET DIRECTED count,
so it cancels to zero on doubled surface. Repairing that class belongs at
its root in the splitters, which is where this branch's actual fix is.

Also reverts the widened operand-tessellation gate, which existed only to
feed the swap.

torr catalogue 24/24; torture track: no regressions vs baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shell-ring reproducer (rana): [difference TOOL SUBJECT] where TOOL is
a multi-stage pipe of unions (bore + groove cylinder + patterned slots +
notch). The analytic splitters trim adjacent faces to different polylines
on such arrangements, leaving 0.3-1.25 mm slits, doubled sheets, interior
membranes, and cap flaps in the export — ~200 non-manifold edges that
slicers "repair" by fusing the part solid.

Three layers of fix:

* A mesh repair pipeline (vcad-kernel-tessellate::repair_watertightness):
  exact-duplicate facet drop, parity-based membrane strip (both-sides-in
  walls AND both-sides-out flaps), redundant-patch peel with a directed
  edge ledger, slit-rail snapping, min-area loop fill with rim-pair
  zipping, micro/pinch edge collapse, and carve-and-refill — every pass
  reverted unless the defective-edge count strictly drops and volume
  survives, and the whole pipeline engaged only on structural damage
  (severity-gated, so catalogue-good results with short 3-way seam edges
  are never perturbed). Runs at export boundaries (Solid::to_mesh,
  BooleanResult::to_mesh), never mid-pipeline.

* The mesh-CSG fallback repairs its own output between two quadric
  projection passes (constraints are read off pristine incident normals,
  then repaired vertices are pulled back onto their carriers), and strips
  operand membranes so parity classification and the Monte-Carlo volume
  oracle see sound input. Chained triangle-soup input skips the heavy
  passes — parity is unreliable there and "repairs" measurably added
  volume.

* Three new watertightness-swap triggers in boolean_op_reported: wide
  cracks (rail gap > 0.5 mm; hairline seams and the 0.13 mm cone-cylinder
  overlaps stay analytic), a missed-cut probe of tool ∩ subject for
  differences, and doubled differences at scale (>= 4 open and >= 3
  over-used edges — above b1's known-good signature). Volume agreement
  now also accepts the operand-implied Monte-Carlo prediction, because a
  doubled-sheet analytic result double-counts its own volume.

The regression test covers all three authored forms of the reproducer
(multi-stage pipe, flat union tool, chained single-tool differences) and
asserts edge-manifoldness plus material absence in every cutter and
presence in the walls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
vcad-mcp Building Building Preview Aug 28, 2026 4:20pm
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Aug 28, 2026 4:20pm
vcad Ignored Ignored Aug 28, 2026 4:20pm
vcad-docs Ignored Ignored Aug 28, 2026 4:20pm

Request Review

…71ab9

# Conflicts:
#	crates/vcad-kernel-booleans/src/api.rs
The torture track (full) failed with 14 regressions: chained and random
boolean cases whose results gained open boundary edges, plus a STEP
roundtrip whose re-import died on DIRECTION('', (NaN, NaN, NaN)).

The repair pipeline optimizes a position-quantized defect measure, but
downstream validity oracles count raw index-level boundary edges — a
"repair" can improve one while regressing the other. Three fixes:

* repair_watertightness now carries a hard exit invariant: never hand
  back more raw boundary edges than the input had (revert wholesale).
* mesh_fallback's refine block holds the same invariant against the
  plain fallback output, and the watertightness swap demands the
  fallback be closed on BOTH metrics before it is taken.
* A collapse pass for exactly-degenerate triangles at the repair tail:
  a DP fill can emit a collinear triple whose NaN plane normal the STEP
  writer emits verbatim, producing a file that cannot be re-imported.

Torture track (full): no regressions vs baseline locally, with several
former bad-geometry cases now passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ecto
ecto merged commit 82b3cb6 into main Aug 28, 2026
15 checks passed
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.

1 participant