Merge pull request #119 from CypherNaught-0x/fix/boolean-shallow-slope

Booleans across a floor that falls a degree or two no longer reject correct results
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-11 07:11:35 -04:00
commit c11fc79288
2 files changed
+65 -3

No files matched your search

+6 -3
View File
@@ -259,9 +259,12 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
let size = a.size().max(b.size());
// The checks mesh both bodies; where they meet in a region much thinner than they are (a
// thin part trimmed by a long one), a step from their size would facet the small faces
// there coarsely enough to fail the membership test on a correct result.
// there coarsely enough to fail the membership test on a correct result. The meshes are
// refined: a round side trimmed to a thin band (a short boss crossing a gently sloping
// floor) is otherwise spanned by triangles cutting through it, which reads as missing
// material.
let tol_m = (size * 5e-4).min(meeting_size(a, b) * 2e-3).max(size * 1e-5);
let meshes = (a.tessellate(tol_m).ok(), b.tessellate(tol_m).ok());
let meshes = (a.tessellate_with(tol_m, true).ok(), b.tessellate_with(tol_m, true).ok());
let (va, vb) = match &meshes {
(Some(ma), Some(mb)) => (ma.measure().volume, mb.measure().volume),
_ => (volume(a), volume(b)),
@@ -270,7 +273,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
// Volume bounds, then membership against the operands (catches misclassified pieces), on
// one mesh of the result. Returns (plausible, volume).
let check = |r: &Body| -> (bool, f64) {
let Ok(mr) = r.tessellate(tol_m) else { return (false, f64::NAN) };
let Ok(mr) = r.tessellate_with(tol_m, true) else { return (false, f64::NAN) };
let v = mr.measure().volume;
let bounds = v > 0.0
&& match op {
+59
View File
@@ -290,6 +290,65 @@ fn moving_a_body_with_an_intersection_curve_edge_ends() {
assert!(rel(v0, v1) < 1e-6, "{v0} vs {v1}");
}
/// A trough whose floor falls 1° (or 2° toward a drain, in four faces), and short round pads
/// and bosses crossing the fall. The B-rep results were right, but the plausibility check meshed
/// them without refinement: on a round side trimmed to a thin band by the shallow cut, the plain
/// mesh spans the band with triangles cutting through the cylinder, so the check saw missing
/// material, rejected the result, and the planar fallback refused the round pad.
#[test]
fn round_pads_crossing_a_shallow_fall() {
let hs = |n: Vec3, p: Vec3| {
let n = n.normalized().unwrap();
HalfSpace { n, d: n.dot(p) }
};
let walls = |floor: &[HalfSpace]| {
let mut h = vec![
hs(Vec3::X, Vec3::new(290.0, 0.0, 0.0)),
hs(-Vec3::X, Vec3::new(10.0, 0.0, 0.0)),
hs(Vec3::Y, Vec3::new(0.0, 140.0, 0.0)),
hs(-Vec3::Y, Vec3::new(0.0, 10.0, 0.0)),
hs(Vec3::Z, Vec3::new(0.0, 0.0, 90.0)),
];
h.extend_from_slice(floor);
convex_polyhedron(&h).unwrap()
};
let block = box_solid(Vec3::ZERO, Vec3::new(300.0, 150.0, 80.0)).unwrap();
// One fall along x at 1°: floor z = 8 + t (x − 10).
let t = 1f64.to_radians().tan();
let fall = boolean(&block, &walls(&[hs(Vec3::new(t, 0.0, -1.0), Vec3::new(10.0, 0.0, 8.0))]), BoolOp::Cut).unwrap().unwrap();
let v0 = measure(&fall).unwrap().volume;
for x in [60.0, 100.0] {
// A pad from z = 9 to 12 (its bottom just under the floor, its top 2–3 mm above).
let pad = cylinder(Vec3::new(x, 75.0, 9.0), Vec3::Z, 15.0, 3.0).unwrap();
let u = boolean(&fall, &pad, BoolOp::Union).unwrap().unwrap();
// The part of the pad above the floor: the floor under the disc is planar, so its mean
// height is the height at the centre (where the floor is below z = 9 everywhere).
let floor = |x: f64| 8.0 + t * (x - 10.0);
let above = if floor(x - 15.0) >= 9.0 { PI * 225.0 * (12.0 - floor(x)) } else { f64::NAN };
let v = measure(&u).unwrap().volume;
if above.is_finite() {
assert!(rel(v, v0 + above) < 1e-5, "pad at x {x}: {v} vs {}", v0 + above);
} else {
assert!(v > v0 && v < v0 + PI * 225.0 * 3.0, "pad at x {x}: {v}");
}
}
// Four faces falling 2° toward a drain point, and a pad over a valley edge and a wall.
let t2 = 2f64.to_radians().tan();
let d = Vec3::new(240.0, 110.0, 8.0);
let hopper = walls(&[
hs(Vec3::new(t2, 0.0, -1.0), d),
hs(Vec3::new(-t2, 0.0, -1.0), d),
hs(Vec3::new(0.0, t2, -1.0), d),
hs(Vec3::new(0.0, -t2, -1.0), d),
]);
let trough = boolean(&block, &hopper, BoolOp::Cut).unwrap().unwrap();
let pad = cylinder(Vec3::new(270.0, 70.0, 8.0), Vec3::Z, 25.0, 5.0).unwrap();
let u = boolean(&trough, &pad, BoolOp::Union).unwrap().unwrap();
// Integrated over the disc (outside the wall at x = 290): about 6517 mm³ above the floor.
let gain = measure(&u).unwrap().volume - measure(&trough).unwrap().volume;
assert!((gain - 6517.0).abs() < 10.0, "{gain}");
}
#[test]
fn planar_booleans_with_coincident_faces() {
// L-bracket: a wall flush with three sides of its base.