fix(geometry): close small boundary gaps consolidate leaves on overlapping/rotated operands - #3373
fix(geometry): close small boundary gaps consolidate leaves on overlapping/rotated operands#3373BIMvoice wants to merge 1 commit into
Conversation
…pping/rotated operands (#3353) A family of tears survives the #3341 parity-segment fix on overlapping and rotated operand pairs. Reproduced with a sweep of rotated-cutter / axis-aligned-host box pairs over Difference, Union and Intersection: two pinned cases each come back with unmatched directed half-edges after a closed-in operand pair. On one, consolidate_coplanar's per-bucket independent re-triangulation emits the same boundary vertex twice from adjacent buckets, a few hundred micrometres apart. On the other, the raw kernel arrangement output is already torn before consolidation runs at all. close_micro_gaps is a bounded, last-resort weld applied to whatever consolidate_coplanar is about to return: it only fires when that mesh already has an open boundary edge at 0.1 mm, so a sound mesh is returned untouched, and it keeps the weld only when it strictly reduces the open-edge count without increasing spike-triangle count. Unlike the closed-in/ closed-out gate already tried and rejected for validate_mesh, a weld that doesn't help is simply discarded rather than reverting the cut. This does not close the whole #3353 family — the sweep still measures a residual, smaller tear rate, and at least one case has open edges too far apart for a vertex weld to fix, pointing at a deeper near-degenerate arrangement defect. Claude-Session: https://claude.ai/code/session_01QPHChk3Ve9N519A4kY7436
|
Warning Review limit reached
This review includes 3 billable files and costs up to $0.75. Or wait 30 minutes for your next included review. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Comment |
Viewer benchmark⚠ 1 metric(s) exceeded the regression threshold (advisory only, not blocking). 01_Snowdon_Towers_Sample_Structural(1).ifcBaseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
AC20-FZK-Haus.ifcBaseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
Refresh the baseline from a CI run: dispatch the Benchmark workflow with |
|
Do not merge this as written. The census failure is a real regression, not a bless-needed improvement, and investigating it surfaced a worse problem than the red check. 58 hosts regressed against the golden, and they are not the scenario #3353 targets — they are Note the first row: a host with zero open edges on both sides lost 18 triangles. Nothing was torn there, so nothing needed repairing. Root cause. The guard checks the aggregate outcome of each call — open-edge count strictly down, badness not up. That cannot detect "closed one tear, silently merged two unrelated closed features elsewhere in the same mesh", which is what the On the tolerance. Aggregate improvement is not sufficient evidence. The tear count dropping 522 -> 411 across 180,000 op-instances is compatible with these 58 hosts getting worse, which is exactly what happened. I checked whether #3366's misclassification bug was masking anything here — it was not: A candidate direction, not proposed for merge yet: restrict the weld to vertices that actually sit on an open boundary edge (an eligibility mask passed into a Flagging rather than closing; the call is yours. |
|
Follow-up: I tried the boundary-restricted weld I sketched above, and it is not sufficient. Reporting it as a dead end rather than pushing it. Change tested: Census result, run locally with the CI command ( Down from 58, not zero. The canary is fixed. Why that is not enough. The remaining 53 hosts have genuine open boundaries — they are already torn. Restricting the weld to boundary vertices does not help there, because that is exactly where their damage is. Welding at ~1.95mm along a real open boundary merges features that legitimately sit that close: That last one is the clearest signal: the output became triangulator-dependent, which is the property this census exists to protect. There are real improvements in the same run (e.g. Conclusion: a position weld is the wrong mechanism for this class, at any eligibility scoping I can see. Closing a tear needs to know which two boundary loops should join — topology — and a distance threshold cannot recover that. The gap sizes here (up to centimetres in at least one case from the earlier sweep) are also well past what any weld tolerance can bridge without merging real features. Nothing pushed; the branch is unchanged at |
|
One more data point that I think settles this, from trying to rescue the approach rather than abandon it. The obvious tightening is to make the accept guard also refuse any weld that loses triangles — But it breaks this PR's own regression test: So the two requirements are in direct conflict under a distance-threshold weld:
No tolerance or eligibility scoping resolves that, because both cases look identical to a position weld — it cannot tell "these two boundary vertices are the two sides of one tear" from "these two boundary vertices belong to unrelated loops that happen to sit 2mm apart". That is topology, and a distance threshold does not have it. The eligibility mask is still worth keeping as a concept — it demonstrably fixed the closed-feature corruption ( Nothing pushed. |
|
New diagnosis: this PR is aimed at the wrong stage. Reporting it here because it invalidates the premise, not just the implementation. A fresh investigation traced 1. 2. The tear already exists in pure Vid-space — in the exact arrangement's own symbolic vertex identity, before any float conversion and before any position-based welding. Measured via a temporary debug hook computing edge balance purely from That second point matters most: no position weld at any tolerance or eligibility scoping can fix a tear that exists in symbolic identity before positions are consulted. It explains why the weld could only ever trade one corruption for another, and why tightening its guard broke this PR's own test. 3. The actual mechanism. The bad edge (shared
Root cause in one sentence: That is this repo's most familiar defect shape — two implementations of one rule with nothing enforcing that they agree — and it sits in No fix is proposed here, deliberately. Reconciling the two detectors (or replacing both with one symmetric test) changes the correctness-critical core every CSG operation depends on, and warrants its own scope with the full census plus cross-platform determinism validation that a Provenance, so you can weigh it: the no-op comparison and the Vid-space edge count come from temporary Recommendation: leave #3353 open, and treat this PR as superseded rather than fixable — the weld cannot address a tear that predates the stage it operates on. |
|
Follow-up: the diagnosis above has now been independently re-verified from scratch, on current Claim A — confirmed. Claim B — confirmed. The tear exists in pure Vid-space, before any float conversion or position weld: Three non-manifold symbolic edges, and the arrangement reports full recovery ( Third claim — confirmed. Running this PR's own pinned test file against current Nothing in the earlier comment needs correcting. The practical consequence is unchanged and now on firmer footing: a position weld inside Still not proposing a fix here. Reconciling |
Summary
proptest_shrunk) the raw kernel output is already watertight;consolidate_coplanar's per-bucket independent re-triangulation is what tears it, emitting the same boundary vertex twice from adjacent buckets a few hundred micrometres apart. On another (sweep_261) the raw arrangement output is already torn before consolidation runs at all.close_micro_gaps(rust/geometry/src/csg/consolidate.rs) is a bounded, last-resort weld applied to whateverconsolidate_coplanaris about to return: it fires only when that mesh already has an open boundary edge at 0.1 mm (a sound mesh returns byte-identical), and it keeps the weld only when it strictly reduces the open-edge count without increasing spike-triangle count. This is a repair, not a gate — unlike the closed-in/closed-out enforcement already tried and rejected forClippingProcessor::validate_mesh(measured to regress the corpus watertightness census), a weld that doesn't help is simply discarded and the mesh returned exactly as it was.Root cause
Two vertices meant to be the same point — one bucket's earcut output and its neighbour's, or two near-degenerate exact intersections in the raw arrangement itself — differ by a few hundred micrometres: well under any real feature this crate triangulates, well over the exact kernel's own snap grid.
Test plan
rust/geometry/tests/issue_3353_rotated_overlap_tearing.rs— two real cases from a 60000-pair sweep, checked across Difference/Union/Intersection.close_micro_gapslocally fails the new test (sweep_261 Union: 3 unmatched edges); restoring it passes.cargo test -p ifc-lite-geometry— full crate suite green, no failures.cargo test -p ifc-lite-geometry --test touching_operand --test csg_property_test --test csg_quality_regression --test issue_3353_rotated_overlap_tearing— all pass (no regression on the Multi-opening cuts tear thin shells open, and the kernel removes volume from a merely touching operand #3341 gates or the existing quality/watertightness regressions).🤖 Generated with Claude Code
https://claude.ai/code/session_01QPHChk3Ve9N519A4kY7436