Joins and cuts of round pads, bosses and holes across a floor that falls a degree or two no longer fail: the B-rep results were right, but the plausibility check meshed them without refinement, and where the shallow cut trims a round side to a thin band the plain mesh spans it with triangles cutting through the cylinder, so the check saw missing material (0.8 to 1.6 % of samples misclassified), rejected the result, and the planar fallback then refused the round tool. The check now meshes the operands and the result with refinement on curved faces (as measure does). Test: round_pads_crossing_a_shallow_fall (a 1° fall and a 2° four-face fall toward a drain)
This commit is contained in:
1 parent
22672585c9
commit
8c1b1478ce
2 files changed
+65
-3
No files matched your search
@@ -249,9 +249,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)),
|
||||
@@ -260,7 +263,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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user