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>
65 lines
3.7 KiB
Rust
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, §ion, 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, §ion, 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, §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}");
|
|
}
|