fix(algo): correct sequential multi-tool cuts on thin-walled solids#779
Conversation
Sequential multi-tool cuts on a hollow thin-walled box (the honeycomb wall pattern) corrupted geometry from cut 2 and silently no-opped from cut ~13 while returning Ok, with per-cut volume deltas swinging from -79 to +920 mm^3 against an expected 10.10 mm^3. Two root causes: 1. The composite analytic classifier accepted arbitrary plane soups as axis-aligned boxes. After the first hex cut, the solid's cavity-side plane set (recess walls + oblique hex laterals) collapsed into a garbage inner box (z_max = 3.44 instead of the open top), so recess interior points classified Inside the solid. GFA then kept phantom tool fragments inside the cavity, producing non-manifold results that fell back to the lossy mesh path on every subsequent cut. build_box now requires every plane to be axis-aligned with at most two distinct offsets per axis; anything else falls back to ray-cast classification, which handles these solids correctly. 2. The GFA acceptance gate rejected genus >= 2 results: euler_balanced only accepted surplus 2 (genus 0) or 0 (genus 1), but a thin wall pierced by N through-holes is a genus-N closed surface with surplus 2 - 2N. Any even surplus <= 2 is now accepted, paired with the closed-manifold requirement. The manifold and free-edge checks also now walk all shells (outer + cavity) instead of only the outer shell, so open or non-manifold cavity shells can no longer slip through the gate. Adds a regression test cutting the full honeycomb pattern (133 hex prisms through one 1.2mm wall) asserting each cut removes exactly the analytic prism volume; it completes in ~14s where the old fallback path churned for minutes per test.
WASM Binary Size
|
There was a problem hiding this comment.
Pull request overview
This PR fixes incorrect behavior in sequential boolean cuts on hollow thin-walled solids by tightening analytic classification (avoiding invalid “axis-aligned box” classifiers from arbitrary plane sets) and by strengthening boolean acceptance gates to correctly validate manifoldness across all shells (outer + inner cavity shells). It also adds a dedicated regression test that reproduces the historical “erratic from cut 2” and “silent no-op from ~cut 13” failure modes on a thin-walled honeycomb-like cut pattern.
Changes:
- Add a regression test covering sequential disjoint hex-prism cuts through a thin wall, validating per-cut analytic volume deltas across both short and full sequences.
- Restrict the analytic “box” classifier to true axis-aligned plane sets with ≤2 distinct offsets per axis; otherwise fall back to non-box classification behavior.
- Fix boolean result validation by counting edge uses across all shells, preventing cavity-shell manifold issues from being missed.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
crates/operations/tests/regress_hexwall_cuts.rs |
Adds regression coverage for sequential thin-wall hex-prism cuts with per-cut volume-delta assertions. |
crates/operations/src/boolean/mod.rs |
Updates Euler/genus acceptance notes and ensures manifold/free-edge checks traverse all shells via shared edge-use counting. |
crates/algo/src/classifier/analytic.rs |
Hardens box-classifier construction to only accept truly axis-aligned “box” plane sets; rejects oblique/over-specified plane soups. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR fixes two root causes of silent no-ops and wrong volume deltas on sequential cuts into hollow thin-walled solids. The classifier fix tightens
Confidence Score: 5/5Safe to merge. The two root-cause fixes are mathematically sound and backed by a well-constructed regression test; all acceptance-gate paths continue to require an independent closed-manifold check. The analytic classifier tightening is clearly correct and conservative (it bails out to ray-cast on any doubt). The Euler-balance relaxation is paired at every call site with The edge-pooling logic in Important Files Changed
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
crates/operations/src/boolean/mod.rs:2351-2354
**Pooled edge counts can mask a free edge when the same edge index appears once in each shell.**
`solid_edge_use_counts` sums edge uses across every shell into a single map. If an edge index appears exactly once in the outer shell and exactly once in an inner cavity shell (a cross-shell edge, which is topologically invalid), its pooled count is 2 and `has_free_edges` returns false — the free edge is silently hidden. In practice this is caught by the independent `is_closed_manifold` call in every acceptance path (per-shell count of 1 ≠ 2 fails that check), so the gate as a whole remains sound. But `has_free_edges` no longer reliably detects free edges in the outer shell on its own, which weakens a defence-in-depth guard that is the only topology check for the `BooleanOp::Intersect` path (line 546).
Consider walking each shell's edge counts independently (the same pattern used in `shell_is_closed_manifold`) so the function's contract — "outer shell has a free edge" — holds without depending on a companion call.
Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
| if planes.len() < 4 { | ||
| return None; | ||
| } | ||
| let axis_tol = 1e-9; |
There was a problem hiding this comment.
The
axis_tol value of 1e-9 is a magic constant sitting between f64::EPSILON (~2.2e-16) and tol.linear (1e-7). The intent — reject any normal that isn't essentially exactly axis-aligned — is clear from the comment, but the choice of 1e-9 vs. something like f64::EPSILON * 4096.0 (a few thousand ULPs) is unexplained and easy to accidentally bump. A named constant with a short rationale comment would make future adjustments deliberate rather than arbitrary.
| let axis_tol = 1e-9; | |
| // Allow at most ~4096 ULPs of deviation from a unit axis component; | |
| // this is far tighter than tol.linear (1e-7) so only truly axis-aligned | |
| // normals pass, while 30°/60° hex-prism faces (component ≈ 0.866) are | |
| // unambiguously rejected. | |
| let axis_tol = f64::EPSILON * 4096.0; // ≈ 9.1e-13 |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/algo/src/classifier/analytic.rs
Line: 966
Comment:
The `axis_tol` value of `1e-9` is a magic constant sitting between `f64::EPSILON` (~2.2e-16) and `tol.linear` (1e-7). The intent — reject any normal that isn't essentially exactly axis-aligned — is clear from the comment, but the choice of `1e-9` vs. something like `f64::EPSILON * 4096.0` (a few thousand ULPs) is unexplained and easy to accidentally bump. A named constant with a short rationale comment would make future adjustments deliberate rather than arbitrary.
```suggestion
// Allow at most ~4096 ULPs of deviation from a unit axis component;
// this is far tighter than tol.linear (1e-7) so only truly axis-aligned
// normals pass, while 30°/60° hex-prism faces (component ≈ 0.866) are
// unambiguously rejected.
let axis_tol = f64::EPSILON * 4096.0; // ≈ 9.1e-13
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| /// Full honeycomb wall: every center the spacing pattern yields (133 with | ||
| /// these parameters). Also guards against the historical multi-minute | ||
| /// mesh-fallback churn — this must complete in seconds. | ||
| #[test] | ||
| fn hexwall_sequential_cuts_full() { | ||
| run_sequential_hex_cuts(usize::MAX); | ||
| } |
There was a problem hiding this comment.
No timeout guard for the "must complete in seconds" invariant. The comment explicitly calls out the historical multi-minute mesh-fallback churn and says the test must complete in seconds, but there is no enforcement. If a future change silently re-introduces the fallback, the test would still pass — it would just be very slow. Consider wrapping the call with a deadline: e.g. spawn the heavy work on a thread and
join with a Duration, or use a test harness timeout attribute. Without this, the performance invariant is documentation-only.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/operations/tests/regress_hexwall_cuts.rs
Line: 107-113
Comment:
**No timeout guard for the "must complete in seconds" invariant.** The comment explicitly calls out the historical multi-minute mesh-fallback churn and says the test must complete in seconds, but there is no enforcement. If a future change silently re-introduces the fallback, the test would still pass — it would just be very slow. Consider wrapping the call with a deadline: e.g. spawn the heavy work on a thread and `join` with a `Duration`, or use a test harness timeout attribute. Without this, the performance invariant is documentation-only.
How can I resolve this? If you propose a fix, please make it concise.| fn make_hollow_bin(topo: &mut Topology) -> brepkit_topology::solid::SolidId { | ||
| let outer = primitives::make_box(topo, 84.0, 84.0, 30.0).expect("outer"); | ||
| let cavity = | ||
| primitives::make_box(topo, 84.0 - 2.0 * WALL_T, 84.0 - 2.0 * WALL_T, 30.0).expect("cavity"); | ||
| transform_solid(topo, cavity, &Mat4::translation(WALL_T, WALL_T, WALL_T)).expect("mv cavity"); | ||
| boolean(topo, BooleanOp::Cut, outer, cavity).expect("hollow shell") | ||
| } |
There was a problem hiding this comment.
make_hollow_bin produces an open-top bin, not a six-sided hollow box. The cavity is 30.0 mm tall (matching the outer box), translated by WALL_T = 1.2 mm in z, so it runs from z = 1.2 to z = 31.2. Since the outer box ceiling is at z = 30.0, the top face is fully removed by the boolean cut — the bin has no lid. The PR description says "hollow 84×84×30 box (1.2mm walls)", which implies closed walls on all sides. For the specific regression being tested (front-wall hex cuts) this doesn't affect correctness, but a comment noting the intentionally open top would prevent confusion, and a future test that relies on top-wall geometry would silently use the wrong shape.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/operations/tests/regress_hexwall_cuts.rs
Line: 27-33
Comment:
**`make_hollow_bin` produces an open-top bin, not a six-sided hollow box.** The cavity is `30.0` mm tall (matching the outer box), translated by `WALL_T = 1.2` mm in z, so it runs from `z = 1.2` to `z = 31.2`. Since the outer box ceiling is at `z = 30.0`, the top face is fully removed by the boolean cut — the bin has no lid. The PR description says "hollow 84×84×30 box (1.2mm walls)", which implies closed walls on all sides. For the specific regression being tested (front-wall hex cuts) this doesn't affect correctness, but a comment noting the intentionally open top would prevent confusion, and a future test that relies on top-wall geometry would silently use the wrong shape.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
# Conflicts: # crates/operations/src/boolean/mod.rs
🤖 I have created a release *beep* *boop* --- ## [2.102.10](v2.102.9...v2.102.10) (2026-06-10) ### Bug Fixes * **algo:** correct sequential multi-tool cuts on thin-walled solids ([#779](#779)) ([45e8fb4](45e8fb4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: brepkit[bot] <265643962+brepkit[bot]@users.noreply.github.com>
Sequential cuts on hollow thin-walled solids (honeycomb wall patterns, compartment grids) were erratic from cut 2 and silently no-op'd from cut ~13 (Ok returned, volume frozen). Root causes: (1) the analytic box classifier accepted arbitrary plane soups as axis-aligned boxes — after the first cut the cavity-side planes collapsed into a garbage inner box classifying recess points Inside, so phantom tool fragments survived; it now requires strictly axis-aligned planes with ≤2 offsets per axis, else ray-cast. (2) An acceptance-gate rejection path detailed in the commit. New regression: hollow 84×84×30 box (1.2mm walls), sequential disjoint hex-prism cuts with per-cut analytic volume deltas; no hang under timeout. Full gates green.