Skip to content

fix(algo): correct sequential multi-tool cuts on thin-walled solids#779

Merged
andymai merged 2 commits into
mainfrom
fix/gfa-thinwall-sequential-cuts
Jun 10, 2026
Merged

fix(algo): correct sequential multi-tool cuts on thin-walled solids#779
andymai merged 2 commits into
mainfrom
fix/gfa-thinwall-sequential-cuts

Conversation

@andymai

@andymai andymai commented Jun 10, 2026

Copy link
Copy Markdown
Owner

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.

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.
Copilot AI review requested due to automatic review settings June 10, 2026 12:54
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

WASM Binary Size

File main PR Delta
brepkit_wasm.wasm 4.90 MB 4.90 MB +.8 KB (+0%)

⚠️ Size increased slightly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@andymai andymai enabled auto-merge (squash) June 10, 2026 13:02
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This 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 build_box to reject non-axis-aligned planes and enforce ≤2 distinct offsets per axis, preventing garbage inner-box classifiers from surviving subsequent booleans. The Euler-balance fix removes the surplus >= 0 lower bound to correctly accept high-genus shells (a wall with N through-holes has genus N and surplus 2 − 2N), paired with the expansion of has_free_edges to walk all shells of the solid.

  • analytic.rs: build_box now requires strictly axis-aligned plane normals (|n_i| > 1 − 1e-9) and bails immediately on any oblique plane; the new sort_dedup helper additionally rejects more than 2 distinct offsets per axis.
  • boolean/mod.rs: euler_balanced drops the surplus ≥ 0 floor to admit genus-N surfaces; a new solid_edge_use_counts helper replaces the old outer-shell-only edge walk in has_free_edges to catch open cavity shells.
  • regress_hexwall_cuts.rs: new regression test drives 20 and 133 sequential hex-prism cuts through an 84×84×30mm hollow bin and asserts per-cut analytic volume deltas within 0.05 mm³.

Confidence Score: 5/5

Safe 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 is_closed_manifold, which provides the per-shell guarantee that euler_balanced alone no longer enforces. The one note — pooled edge counts in has_free_edges can theoretically mask a cross-shell free edge — is covered by is_closed_manifold in every acceptance path and is a defence-in-depth concern rather than a reachable defect.

The edge-pooling logic in has_free_edges (boolean/mod.rs) is worth a second look, specifically the interaction with BooleanOp::Intersect on line 546 where it is the primary topology check.

Important Files Changed

Filename Overview
crates/algo/src/classifier/analytic.rs Tightened build_box to require strictly axis-aligned planes (
crates/operations/src/boolean/mod.rs Two linked changes: euler_balanced drops the surplus >= 0 lower bound to accept genus-N surfaces; has_free_edges now pools edge counts across all shells via the new solid_edge_use_counts helper. The pooling can theoretically mask a free edge present once in each shell, but is_closed_manifold in all acceptance paths independently catches it per-shell.
crates/operations/tests/regress_hexwall_cuts.rs New regression test covering sequential hex-prism cuts on a hollow thin-walled box. Volume-delta assertions are tight (0.05 mm³ tolerance). Existing threads cover the timeout invariant and open-top bin geometry.

Fix All in Claude Code

Prompt To Fix All With AI
Fix 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

Fix in Claude Code

Comment on lines +107 to +113
/// 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Fix in Claude Code

Comment on lines +27 to +33
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")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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!

Fix in Claude Code

# Conflicts:
#	crates/operations/src/boolean/mod.rs
@andymai andymai merged commit 45e8fb4 into main Jun 10, 2026
18 checks passed
@andymai andymai deleted the fix/gfa-thinwall-sequential-cuts branch June 10, 2026 13:27
@brepkit brepkit Bot mentioned this pull request Jun 10, 2026
brepkit Bot added a commit that referenced this pull request Jun 10, 2026
🤖 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>
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