fix(booleans): manifold exports for multi-tool differences - #838
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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>
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
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.crates/vcad-kernel-tessellate/src/mesh_ray.rs: ray-parity machinery moved fromvcad-kernel-booleans::meshinto the tessellate crate (booleans re-exports).Tests
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.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