Files
solidcraft-upstream/crates/kernel/src/thread_tests.rs
T
bherbruckandClaude Opus 5.5 4b3b48388c 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 <[email protected]>
2026-10-15 08:53:15 -04:00

65 lines
3.7 KiB
Rust

//! Faceted booleans with helical tools (modelled threads).
use std::f64::consts::PI;
use solidcraft_geom::{Plane, Vec2, Vec3};
use crate::{BoolOp, cylinder, faceted_boolean, sweep_helix_mesh};
/// A trapezoid groove screwed into a shaft: the result is closed, and the volume removed is
/// the section's 2π∫r dA per turn over the turns inside the shaft.
#[test]
fn faceted_cut_of_a_helical_groove() {
let shaft = cylinder(Vec3::ZERO, Vec3::new(0.0, 0.0, 1.0), 1.5, 4.0).unwrap();
let (p, k) = (0.5, Vec3::new(0.0, 0.0, 1.0));
// From a turn under the shaft to its middle: 4.5 turns, the first half-turn in the air.
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(1.2, -0.06), Vec2::new(1.6, -0.06), Vec2::new(1.6, 0.06), Vec2::new(1.2, 0.06)];
let groove = sweep_helix_mesh(&plane, &section, base, k, p, 5.0, 48).unwrap();
let cut = faceted_boolean(&shaft, &groove, BoolOp::Cut, 1e-3).unwrap().unwrap();
let m = cut.tessellate(0.01).unwrap().measure();
// In the shaft from z = 0: the section (r 1.2…1.5, 0.12 tall) for 4 turns, plus the part of
// the first turn's section above z = 0 (its z range is ±0.06 about the turn's height).
let per_turn = 2.0 * PI * 0.12 * (1.5f64.powi(2) - 1.2f64.powi(2)) / 2.0;
let removed = PI * 2.25 * 4.0 - m.volume;
assert!((removed - 4.0 * per_turn).abs() < 0.03 * per_turn * 4.0, "{removed} vs {}", 4.0 * per_turn);
}
/// A groove cut into the wall of a bore through a block (a nut): it runs out of both ends.
#[test]
fn faceted_cut_into_a_bore() {
let nut = crate::box_solid(Vec3::new(-3.0, -3.0, 0.0), Vec3::new(3.0, 3.0, 2.0)).unwrap();
let bore = cylinder(Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, 0.0, 1.0), 1.23, 4.0).unwrap();
let nut = crate::boolean(&nut, &bore, BoolOp::Cut).unwrap().unwrap();
let v0 = nut.tessellate(1e-3).unwrap().measure().volume;
let (p, k) = (0.5, 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(1.1, -0.1), Vec2::new(1.5, -0.1), Vec2::new(1.5, 0.1), Vec2::new(1.1, 0.1)];
let groove = sweep_helix_mesh(&plane, &section, base, k, p, 6.0, 48).unwrap();
let cut = faceted_boolean(&nut, &groove, BoolOp::Cut, 2.5e-3).unwrap().unwrap();
let removed = v0 - cut.tessellate(1e-3).unwrap().measure().volume;
// Four turns of the section's part outside the bore (r 1.23…1.5, 0.2 tall).
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, &section, 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}");
}