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:
H. Conrad authored and GitHub committed 2026-10-15 22:18:31 -04:00
1 parent 1a55241882
commit 723c7b61c4
3 files changed
+101 -2

No files matched your search

+23
View File
@@ -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();
+75
View File
@@ -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 {