From 4b3b48388c0ce3ef7c2285decbe1cb3d590df15e Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 15 Oct 2026 08:53:15 -0400 Subject: [PATCH] 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 --- crates/kernel/src/polybool.rs | 22 ++++++++++++++++++++++ crates/kernel/src/thread_tests.rs | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/kernel/src/polybool.rs b/crates/kernel/src/polybool.rs index 582b4eba..e9c376f8 100644 --- a/crates/kernel/src/polybool.rs +++ b/crates/kernel/src/polybool.rs @@ -356,6 +356,11 @@ pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result Result) -> Vec { + 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> { a.require_brep("a boolean")?; diff --git a/crates/kernel/src/thread_tests.rs b/crates/kernel/src/thread_tests.rs index 70a2ddd0..38c53699 100644 --- a/crates/kernel/src/thread_tests.rs +++ b/crates/kernel/src/thread_tests.rs @@ -44,3 +44,21 @@ fn faceted_cut_into_a_bore() { let want = 4.0 * PI * 0.2 * (1.5f64.powi(2) - 1.23f64.powi(2)); assert!((removed - want).abs() < 0.03 * want, "{removed} vs {want}"); } + +/// #1250: a long thread (48 turns of an M8 groove on a 60 mm shaft). The groove's facets come in +/// sweep order; taken as BSP splitters in that order they made the tree as deep as the facet +/// list ("planar boolean: tree too deep", after minutes and gigabytes). It cuts now, closed. +#[test] +fn a_long_thread_cuts_without_a_deep_tree() { + let (r, len, p) = (4.0, 60.0, 1.25); + let shaft = cylinder(Vec3::ZERO, Vec3::new(0.0, 0.0, 1.0), r, len).unwrap(); + let k = Vec3::new(0.0, 0.0, 1.0); + let base = Vec3::new(0.0, 0.0, -p); + let plane = Plane { origin: base, x: Vec3::new(1.0, 0.0, 0.0), y: k }; + let section = [Vec2::new(3.3, -0.08), Vec2::new(4.1, -0.5), Vec2::new(4.1, 0.5), Vec2::new(3.3, 0.08)]; + let groove = sweep_helix_mesh(&plane, §ion, base, k, p, len / p + 2.0, 48).unwrap(); + let cut = faceted_boolean(&shaft, &groove, BoolOp::Cut, p * 0.005).unwrap().unwrap(); + let v = cut.tessellate(0.01).unwrap().measure().volume; + let v0 = PI * r * r * len; + assert!(v < v0 * 0.95 && v > v0 * 0.6, "{v} of {v0}"); +}