Files
OneCAD/crates/kernel/src/bore_union_tests.rs
T

119 lines
5.7 KiB
Rust

#![allow(clippy::unwrap_used, clippy::panic)]
use crate::*;
use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
fn fixture() -> (Body, Body, Vec3, f64) {
let slope = 1f64.to_radians().tan();
let hs = |normal: Vec3, point: Vec3| {
let n = normal.normalized().unwrap();
HalfSpace { n, d: n.dot(point) }
};
let stock = convex_polyhedron(&[
hs(Vec3::X, Vec3::new(40., 0., 0.)),
hs(-Vec3::X, Vec3::ZERO),
hs(Vec3::Y, Vec3::new(0., 30., 0.)),
hs(-Vec3::Y, Vec3::ZERO),
hs(-Vec3::Z, Vec3::ZERO),
hs(Vec3::new(-slope, 0., 1.), Vec3::new(0., 0., 20.)),
])
.unwrap();
let cutter = cylinder(Vec3::new(20., 15., -5.), Vec3::Z, 5., 40.).unwrap();
let bored = boolean(&stock, &cutter, BoolOp::Cut).unwrap().unwrap();
let web = box_solid(Vec3::new(14., 14., 16.05), Vec3::new(26., 16., 16.07)).unwrap();
let target = boolean(&bored, &web, BoolOp::Union).unwrap().unwrap();
let region = Region2 { outer: Loop2::circle(Vec2::new(20., 15.), 8.), holes: vec![Loop2::circle(Vec2::new(20., 15.), 5.).reversed()] };
let ring = extrude(&Plane::XY, &[region], 14., 24.).unwrap().remove(0);
let web_volume = 0.02 * 2. * (24f64.sqrt() + 25. * 0.2f64.asin());
let fine_target = target.tessellate_with(1e-4, true).unwrap();
let added = fine_target.measure().volume - bored.tessellate_with(1e-4, true).unwrap().measure().volume;
assert!((added - web_volume).abs() < 0.005, "actual connected web adds {added}, expected {web_volume}");
for x in [16., 20., 24.] {
for y in [14.2, 15., 15.8] {
assert!(fine_target.contains(Vec3::new(x, y, 16.06)), "source web must exist at {x},{y}");
}
}
assert!(!fine_target.contains(Vec3::new(20., 12., 16.06)));
let floor_volume = (20. + slope * 20.) * (1200. - 25. * std::f64::consts::PI);
let ring_added = std::f64::consts::PI * (64. - 25.) * (24. - (20. + slope * 20.));
(target, ring, Vec3::new(20., 15., 16.06), floor_volume + web_volume + ring_added)
}
#[test]
fn thin_connected_web_between_bore_stations_survives_union() {
let (target, ring, probe, expected) = fixture();
let target_before = step_export(&[&target], "source").unwrap();
let ring_before = step_export(&[&ring], "ring").unwrap();
let size = target.size().max(ring.size());
let tol = (size * 5e-4).max(size * 1e-5);
let ma = target.tessellate_with(tol, true).unwrap();
let mb = ring.tessellate_with(tol, true).unwrap();
assert!(ma.contains(probe), "fixture must contain the real connected web");
assert!(!mb.contains(probe));
let va = ma.measure().volume;
let vb = mb.measure().volume;
let slack = 2e-3 * (va + vb) + 1e-9;
let accept = |body: &Body| {
let mr = body.tessellate_with(tol, true).unwrap();
let v = mr.measure().volume;
let mismatch = super::mismatch(&ma, &mb, &mr, BoolOp::Union);
eprintln!("fallback candidate V={v}, expected={expected}, web={}, mismatch={mismatch:?}", mr.contains(probe));
v > 0. && v >= va.max(vb) - slack && v <= va + vb + slack && mismatch.is_none_or(|f| f < 0.004)
};
let direct = crate::coplanar::bore_union(&target, &ring, &accept);
eprintln!("direct fallback result={}", direct.is_some());
let public = boolean(&target, &ring, BoolOp::Union);
if let Ok(dir) = std::env::var("SOLVECRAFT_BORE_AUDIT_DIR") {
let dir = std::path::Path::new(&dir);
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join("target-with-web.step"), step_export(&[&target], "target with web").unwrap()).unwrap();
if let Some(result) = &direct {
std::fs::write(dir.join("direct-union.step"), step_export(&[result], "direct union").unwrap()).unwrap();
}
if let Ok(Some(result)) = &public {
std::fs::write(dir.join("public-union.step"), step_export(&[result], "public union").unwrap()).unwrap();
}
}
let direct_keeps_web = direct.as_ref().is_none_or(|result| result.tessellate_with(1e-3, true).unwrap().contains(probe));
let public_keeps_web = match &public {
Ok(Some(result)) => {
let keep = result.tessellate_with(1e-3, true).unwrap().contains(probe);
eprintln!("public Union result web={keep}");
keep
}
result => {
eprintln!("public Union honestly refused: {result:?}");
true
}
};
assert!(direct_keeps_web, "accepted fallback removed real web material");
assert!(public_keeps_web, "public Union removed real web material");
assert_eq!(step_export(&[&target], "source").unwrap(), target_before);
assert_eq!(step_export(&[&ring], "ring").unwrap(), ring_before);
}
#[test]
fn unproved_curved_boundary_between_clear_endpoints_refuses_recut() {
let a = Vec2::new(6., -1.);
let b = Vec2::new(6., 1.);
let c = Vec2::new(8., 1.);
let d = Vec2::new(8., -1.);
let region = Region2 {
outer: Loop2 {
segs: vec![
Seg2::Cubic { p0: a, p1: Vec2::new(3., -0.5), p2: Vec2::new(3., 0.5), p3: b },
Seg2::Line { a: b, b: c },
Seg2::Line { a: c, b: d },
Seg2::Line { a: d, b: a },
],
}
.reversed(),
holes: vec![],
};
let body = extrude(&Plane::XY, &[region], 16.05, 16.07).unwrap().remove(0);
assert!(a.len() > 5. && b.len() > 5., "endpoint samples miss the interior excursion");
assert!(body.tessellate_with(1e-4, true).unwrap().contains(Vec3::new(4.5, 0., 16.06)));
let before = step_export(&[&body], "curved source").unwrap();
assert!(crate::bore_guard::boundaries_clear(&body, &[], Vec3::ZERO, Vec3::Z, 5., 14., 24., 1e-6).is_none());
assert_eq!(step_export(&[&body], "curved source").unwrap(), before);
}