Summary
A latent over-cut in BooleanClippingProcessor::process_with_depth_inner's solo_step computation, found stranded on PR #3922's diverged branch (fix-issue-3919-roof-clip-gate) as its one remaining unmerged commit. #3922's own original fix already landed separately as f7872db62; this issue covers only the remaining unique change from that branch.
The defect
rust/geometry/src/processors/boolean/mod.rs computes, per DIFFERENCE chain:
let solo_step = spine.len() == 1;
solo_step answers a per-cutter question for the accept-gate rejection fallback in single_cutter_gate.rs: does this cutter have a sibling that can compensate for a gate-rejected subtract? true escalates to the riskier unbounded FallThrough clip; false picks the safer KeepUncut.
But the outer chain-resolution loop (process_with_depth_inner) retries try_union_polygonal_chain at every suffix level of a left-deep DIFFERENCE spine. When the top-level batch fails and a shorter suffix batches successfully, the caller's spine ends up holding only the single outermost node — spine.len() == 1 — even though that node's own cutter has siblings, already folded into the mesh by the successful suffix batch below it. The current formula can't tell that case apart from a genuine single-cutter shape (no sibling anywhere), so it reports "no sibling" and sends a gate rejection to the riskier FallThrough — an over-cut in exactly the case !solo_step exists to prevent.
Fix
Track whether the mesh entering the sequential-apply loop already came from a successful batch (based_on_batch), and only treat a length-1 spine as truly solo when it did not:
let solo_step = spine.len() == 1 && !based_on_batch;
Measured impact
Entering House.ifc's real chain directly at entity #2146 (issue #960 fixture, treated as a representation root): before the fix, an accept-gate rejection on #2146's own cutter over-cuts to max Z ≈ 2735.6 mm (FallThrough); after the fix, it correctly keeps the batched result un-cut at max Z ≈ 4475.3 mm (KeepUncut).
Status on main today
This defect is currently latent, not active, on main. Entering the #960 fixture at entity #2146 (as the stranded branch's regression test does) currently produces spine.len() == 2 on main, not 1 — so the buggy formula isn't reached by that particular fixture today. A scan of every DIFFERENCE-chain root in the only in-repo fixture with this shape (five chains, ~50 nodes total) found no node that simultaneously satisfies spine.len() == 1, based_on_batch == true, AND an actual accept-gate rejection at that node — so no current in-repo fixture makes this bug active. The accompanying regression test therefore passes both before and after the fix; it is a guard against a real, structurally-possible defect, not a live reproduction. The PR fixing this will state that plainly rather than presenting the guard test as a reproduction.
Where it's stranded
PR #3922's branch has diverged 1000+ lines from main in unrelated files. Its original fix already landed as f7872db62. This one commit (fixing solo_step, plus the three files it touches: mod.rs, single_cutter_gate.rs, chain_cycle_tests.rs) is the only unique, unmerged change left on that branch.
Summary
A latent over-cut in
BooleanClippingProcessor::process_with_depth_inner'ssolo_stepcomputation, found stranded on PR #3922's diverged branch (fix-issue-3919-roof-clip-gate) as its one remaining unmerged commit. #3922's own original fix already landed separately asf7872db62; this issue covers only the remaining unique change from that branch.The defect
rust/geometry/src/processors/boolean/mod.rscomputes, per DIFFERENCE chain:solo_stepanswers a per-cutter question for the accept-gate rejection fallback insingle_cutter_gate.rs: does this cutter have a sibling that can compensate for a gate-rejected subtract?trueescalates to the riskier unboundedFallThroughclip;falsepicks the saferKeepUncut.But the outer chain-resolution loop (
process_with_depth_inner) retriestry_union_polygonal_chainat every suffix level of a left-deep DIFFERENCE spine. When the top-level batch fails and a shorter suffix batches successfully, the caller'sspineends up holding only the single outermost node —spine.len() == 1— even though that node's own cutter has siblings, already folded into the mesh by the successful suffix batch below it. The current formula can't tell that case apart from a genuine single-cutter shape (no sibling anywhere), so it reports "no sibling" and sends a gate rejection to the riskierFallThrough— an over-cut in exactly the case!solo_stepexists to prevent.Fix
Track whether the mesh entering the sequential-apply loop already came from a successful batch (
based_on_batch), and only treat a length-1 spine as truly solo when it did not:Measured impact
Entering House.ifc's real chain directly at entity #2146 (issue #960 fixture, treated as a representation root): before the fix, an accept-gate rejection on #2146's own cutter over-cuts to max Z ≈ 2735.6 mm (FallThrough); after the fix, it correctly keeps the batched result un-cut at max Z ≈ 4475.3 mm (KeepUncut).
Status on
maintodayThis defect is currently latent, not active, on
main. Entering the #960 fixture at entity #2146 (as the stranded branch's regression test does) currently producesspine.len() == 2onmain, not1— so the buggy formula isn't reached by that particular fixture today. A scan of every DIFFERENCE-chain root in the only in-repo fixture with this shape (five chains, ~50 nodes total) found no node that simultaneously satisfiesspine.len() == 1,based_on_batch == true, AND an actual accept-gate rejection at that node — so no current in-repo fixture makes this bug active. The accompanying regression test therefore passes both before and after the fix; it is a guard against a real, structurally-possible defect, not a live reproduction. The PR fixing this will state that plainly rather than presenting the guard test as a reproduction.Where it's stranded
PR #3922's branch has diverged 1000+ lines from
mainin unrelated files. Its original fix already landed asf7872db62. This one commit (fixingsolo_step, plus the three files it touches:mod.rs,single_cutter_gate.rs,chain_cycle_tests.rs) is the only unique, unmerged change left on that branch.