Boolean: cut around a part's sealed voids when the whole cut fails (#1570)
A cut clear of every void cuts the outer shell alone and puts the voids back (robustness seed 15 now builds).
This commit is contained in:
1 parent
1a55241882
commit
723c7b61c4
3 files changed
+101
-2
No files matched your search
@@ -37,6 +37,29 @@ fn check_nested(body: &Body) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Robustness seed 15, shrunk: a sphere cut at a corner of a part with a sealed internal void far
|
||||
/// from the sphere. The same cut on the part without the void works; with it, the boolean failed
|
||||
/// ("no result", then "the exact fallback … takes planar faces only"). A cut from a box or a
|
||||
/// cylinder on the voided part worked.
|
||||
#[test]
|
||||
fn sphere_cut_far_from_a_void_keeps_the_void() {
|
||||
let block = box_solid(Vec3::ZERO, Vec3::new(40.0, 40.0, 40.0)).unwrap();
|
||||
let cavity = box_solid(Vec3::new(25.0, 25.0, 25.0), Vec3::new(30.0, 30.0, 30.0)).unwrap();
|
||||
let hollow = boolean(&block, &cavity, BoolOp::Cut).unwrap().unwrap();
|
||||
assert_eq!(hollow.solid.boundaries().len(), 2, "the cavity is a second shell");
|
||||
let ball = sphere(Vec3::new(0.0, 0.0, 40.0), 8.0).unwrap();
|
||||
let cut = boolean(&hollow, &ball, BoolOp::Cut).unwrap().unwrap();
|
||||
// The block, less the cavity, less the eighth of the ball inside the corner.
|
||||
let want = 64_000.0 - 125.0 - std::f64::consts::PI * 512.0 / 6.0;
|
||||
let got = measure(&cut).unwrap().volume;
|
||||
assert!((got - want).abs() < want * 1e-3, "volume {got}, want {want}");
|
||||
assert_eq!(cut.solid.boundaries().len(), 2, "the cavity stays a sealed second shell");
|
||||
let mesh = cut.tessellate_with(0.01, true).unwrap();
|
||||
for (p, material) in [(Vec3::new(27.5, 27.5, 27.5), false), (Vec3::new(1.0, 1.0, 39.0), false), (Vec3::new(20.0, 20.0, 20.0), true)] {
|
||||
assert_eq!(mesh.contains(p), material, "material at {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_cavities_stay_with_their_material_islands_for_every_shell_order() {
|
||||
let body = nested_island();
|
||||
|
||||
@@ -332,6 +332,15 @@ fn boolean_via(a: &Body, b: &Body, op: BoolOp, whole: &dyn Fn(&Body, &Body, Bool
|
||||
}
|
||||
}
|
||||
}
|
||||
// A piece with sealed voids, cut clear of them: the outer shell alone, the voids put back.
|
||||
if op == BoolOp::Cut && r.is_err() {
|
||||
let (cut, report) = capture_boolean_diagnostics(|| cut_around_voids(a, b, whole));
|
||||
if let Ok(Some(x)) = cut {
|
||||
r = Ok(Some(x));
|
||||
diagnostics = report;
|
||||
diagnostics.push(BooleanFallback::new(BooleanStrategy::Piecewise));
|
||||
}
|
||||
}
|
||||
if op == BoolOp::Union
|
||||
&& let Ok(Some(body)) = &r
|
||||
&& !preserves_planar_surfaces(a, b, body)
|
||||
@@ -374,6 +383,72 @@ fn boolean_via(a: &Body, b: &Body, op: BoolOp, whole: &dyn Fn(&Body, &Body, Bool
|
||||
r
|
||||
}
|
||||
|
||||
/// The most shells [`cut_around_voids`] takes apart.
|
||||
const MAX_VOID_SHELLS: usize = 64;
|
||||
|
||||
/// `a` cut by `b` around `a`'s voids: when `a` is one piece with sealed voids (inner shells) and
|
||||
/// `b` stays clear of every void, the outer shell alone is cut and the voids are put back. The cut
|
||||
/// of the whole failed on such bodies where the same cut of the outer shell works: a sphere cut
|
||||
/// far from a void (robustness seed 15). `None` when `a` has no voids, `b` comes near one, or
|
||||
/// the cut of the outer shell fails too. The result carries no history.
|
||||
fn cut_around_voids(a: &Body, b: &Body, whole: &dyn Fn(&Body, &Body, BoolOp) -> Result<Option<Body>>) -> Result<Option<Body>> {
|
||||
let shells = a.solid.boundaries().clone();
|
||||
if shells.len() < 2 || shells.len() > MAX_VOID_SHELLS || b.solid.boundaries().len() != 1 || a.lumps()?.len() != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
let tol = (a.size() * 1e-3).max(1e-3);
|
||||
let mut boxes = Vec::with_capacity(shells.len());
|
||||
for sh in &shells {
|
||||
let piece =
|
||||
guard("void shell", || Body::new(Solid::try_new(vec![sh.clone()]).map_err(|e| KernelError::Failed(format!("void shell: {e}")))?))?;
|
||||
let m = piece.tessellate(tol)?;
|
||||
if m.positions.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
boxes.push(m.bounds());
|
||||
}
|
||||
let extent = |i: usize| {
|
||||
boxes.get(i).map_or(0.0, |x| {
|
||||
let d = x.max - x.min;
|
||||
d.x * d.y * d.z
|
||||
})
|
||||
};
|
||||
let Some(outer) = (0..shells.len()).max_by(|i, j| extent(*i).total_cmp(&extent(*j))) else { return Ok(None) };
|
||||
let Some(ob) = boxes.get(outer) else { return Ok(None) };
|
||||
let tm = b.tessellate((b.size() * 1e-3).max(1e-3))?;
|
||||
let tb = tm.bounds();
|
||||
// Clear of the tool by more than both meshes' deflection.
|
||||
let gap = 4.0 * tol + (b.size() * 1e-3).max(1e-3);
|
||||
let inside = |x: &solidcraft_geom::Aabb3| {
|
||||
x.min.x >= ob.min.x && x.min.y >= ob.min.y && x.min.z >= ob.min.z && x.max.x <= ob.max.x && x.max.y <= ob.max.y && x.max.z <= ob.max.z
|
||||
};
|
||||
let clear = |x: &solidcraft_geom::Aabb3| {
|
||||
x.max.x + gap < tb.min.x
|
||||
|| tb.max.x + gap < x.min.x
|
||||
|| x.max.y + gap < tb.min.y
|
||||
|| tb.max.y + gap < x.min.y
|
||||
|| x.max.z + gap < tb.min.z
|
||||
|| tb.max.z + gap < x.min.z
|
||||
};
|
||||
let mut voids = Vec::new();
|
||||
for (i, (sh, x)) in shells.iter().zip(&boxes).enumerate() {
|
||||
if i == outer {
|
||||
continue;
|
||||
}
|
||||
if !inside(x) || !clear(x) {
|
||||
return Ok(None);
|
||||
}
|
||||
voids.push(sh.clone());
|
||||
}
|
||||
let Some(outer_shell) = shells.get(outer) else { return Ok(None) };
|
||||
let alone =
|
||||
guard("outer shell", || Body::new(Solid::try_new(vec![outer_shell.clone()]).map_err(|e| KernelError::Failed(format!("outer shell: {e}")))?))?;
|
||||
let Some(cut) = boolean_via(&alone, b, BoolOp::Cut, whole)? else { return Ok(None) };
|
||||
let mut all = cut.solid.boundaries().clone();
|
||||
all.extend(voids);
|
||||
Ok(Some(guard("voids back", || Body::new(Solid::try_new(all).map_err(|e| KernelError::Failed(format!("voids back: {e}")))?))?))
|
||||
}
|
||||
|
||||
/// Every face of an all-planar union must remain on an operand's original plane.
|
||||
/// Curved operands keep their existing surface-specific boolean handling.
|
||||
fn preserves_planar_surfaces(a: &Body, b: &Body, result: &Body) -> bool {
|
||||
|
||||
Reference in new issue
Block a user