Modelled threads: shuffle the faceted boolean's polygons before the BSP (#1250) (#1395)

The groove's facets came in sweep order, and the BSP takes each list's first
polygon as its splitter, so the tree was as deep as the list: quadratic time and
memory. An M8 x 60 mm thread ran out of tree depth after 12.9 GB. A fixed shuffle
gives a shallow tree and the same result (volume and faces equal to 1e-9).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 08:53:15 -04:00
1 parent 0d99d33c38
commit 4b3b48388c
2 files changed
+40

No files matched your search

+22
View File
@@ -356,6 +356,11 @@ pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result<Optio
// whether the pieces' boundaries come out consistent: try a few tolerances, and take the
// first whose result is watertight and agrees with the complementary boolean (A − B and
// A ∩ B add up to A; A ∪ B is A plus B less A ∩ B).
// The BSP takes each list's first polygon as its splitting plane. Facets in sweep order
// (a thread's groove, turn after turn) make every splitter cut nothing, so the tree is as
// deep as the list is long: quadratic time and memory, and a long thread ran out of depth
// (#1250). The same lists in a fixed shuffled order give a shallow tree.
let (pa, pb) = (shuffled(pa), shuffled(pb));
let (va, vb) = (polys_volume(&pa), polys_volume(&pb));
let mut last = KernelError::Failed("faceted boolean".into());
for rel in [1e-8, 1e-7, 3e-8, 1e-9, 3e-7] {
@@ -383,6 +388,23 @@ pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result<Optio
Err(last)
}
/// `polys` in a fixed pseudo-random order (the same every run, so results are reproducible).
fn shuffled(mut polys: Vec<Poly>) -> Vec<Poly> {
let mut state = 0x9E37_79B9_7F4A_7C15u64 ^ polys.len() as u64;
let mut next = || {
// SplitMix64.
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
};
for i in (1..polys.len()).rev() {
polys.swap(i, (next() % (i as u64 + 1)) as usize);
}
polys
}
/// Boolean of two all-planar bodies.
pub fn planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
a.require_brep("a boolean")?;