Pattern copies of a post joined to a body already holding the original post had their tops in its plane, so every join first pushed faces apart (about 190 ms, then discarded). Faces now coincide only where their boxes meet, the push-apart retry runs once per boolean instead of twice, and splitting prism outlines skips the sampled arc projection for points off the arc's circle. Sensor enclosure evaluation: 3.07 s -> 1.63 s, same bodies. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
136 lines
6.9 KiB
Rust
136 lines
6.9 KiB
Rust
#![allow(clippy::unwrap_used, clippy::panic)]
|
|
use crate::*;
|
|
use solidcraft_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) = solidcraft_geom::env::var("BORE_AUDIT_DIR").ok_or(()) {
|
|
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);
|
|
}
|
|
|
|
/// Posts sunk into one floor have their tops in a common plane but apart: they don't
|
|
/// coincide, so joining another post doesn't go through pushing faces apart (#261).
|
|
#[test]
|
|
fn coplanar_faces_apart_from_each_other_do_not_coincide() {
|
|
let floor = box_solid(Vec3::new(0., 0., 0.), Vec3::new(80., 60., 2.)).unwrap();
|
|
let post = |x: f64| cylinder(Vec3::new(x, 30., 1.), Vec3::Z, 3., 6.).unwrap();
|
|
let with_one = boolean(&floor, &post(10.), BoolOp::Union).unwrap().unwrap();
|
|
assert!(crate::coplanar::coincident_planes(&with_one, &post(60.)).is_empty());
|
|
assert!(crate::coplanar::boolean_apart(&with_one, &post(60.), BoolOp::Union).is_none());
|
|
// A post over the first one's top does meet it there.
|
|
assert!(!crate::coplanar::coincident_planes(&with_one, &cylinder(Vec3::new(12., 30., 7.), Vec3::Z, 3., 6.).unwrap()).is_empty());
|
|
let joined = boolean(&with_one, &post(60.), BoolOp::Union).unwrap().unwrap();
|
|
let v = joined.tessellate_with(1e-3, true).unwrap().measure().volume;
|
|
let expected = 80. * 60. * 2. + 2. * std::f64::consts::PI * 9. * 5.;
|
|
assert!((v - expected).abs() < 0.5, "volume {v}, expected {expected}");
|
|
}
|