fix: fuse a loft standing on a face instead of returning touching shells (#577)

Fixes #396. A union that would come back as shells touching over an area is rejected and the face-in-face case is fused exactly; line and point contact stay as before.
This commit is contained in:
Kevin Ravensberg authored and GitHub committed 2026-10-13 06:04:41 -04:00
1 parent bb60a95289
commit 24cff445ad
5 files changed
+245 -2

No files matched your search

+36 -2
View File
@@ -156,6 +156,9 @@ fn contradicted(
(a_in_b && under_surface(ma, ia, ib, tol, false, None)) || (b_in_a && !a_in_b && under_surface(mb, ib, ia, tol, false, None))
}
/// Sampled overlap fraction below which two bodies share no material.
const NONE: f64 = 0.003;
/// Booleans that need no intersection: one body clear of the other, or inside it. These are
/// common with coincident curved faces (a hole drilled where one already is, a body joined
/// with itself), which the intersection can't handle. `counts`: the operands' faces and edges.
@@ -178,7 +181,6 @@ fn trivial(
}
let (ia, ib) = (ma.inside_index(), mb.inside_index());
let (b_in_a, a_in_b) = overlap(ma, mb, &ia, &ib)?;
const NONE: f64 = 0.003;
const ALL: f64 = 0.997;
let disjoint = b_in_a < NONE && a_in_b < NONE;
// The samples only say that nearly all of one body is clear of, or inside, the other. A
@@ -404,6 +406,19 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
let slack = 2e-3 * (va + vb) + 1e-9;
// Volume bounds, then membership against the operands (catches misclassified pieces), on
// one mesh of the result. Returns (plausible, volume).
// Shells that touch were never fused: coincident back-to-back faces leave the intersection
// nothing to cut, so it returns both operands side by side (#396). Volume and point
// sampling cannot see that; the shell grouping can. Only contact over an area counts
// (bodies tangent along a line stay two lumps), and operands already like that, saved
// before this was caught, stay joinable.
let unfused = |r: &Body| {
op == BoolOp::Union
&& r.shell_count() > 1
&& r.union_lumps().is_err()
&& a.union_lumps().is_ok()
&& b.union_lumps().is_ok()
&& crate::coplanar::faces_touch(a, b)
};
let check = |r: &Body| -> (bool, f64) {
let Ok(mr) = r.tessellate_with(tol_m, true) else { return (false, f64::NAN) };
let v = mr.measure().volume;
@@ -416,6 +431,9 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
if !bounds {
return (false, v);
}
if unfused(r) {
return (false, v);
}
let consistent = match &meshes {
(Some(ma), Some(mb)) => mismatch(ma, mb, &mr, op).is_none_or(|f| f < 0.004),
_ => true,
@@ -502,6 +520,18 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
Some(Ok(None)) | None => {}
}
}
// A face standing wholly inside a coincident face of the other body, back to back, and no
// other contact (#396): exact, by topology alone.
if op == BoolOp::Union
&& a.solid.boundaries().len() == 1
&& b.solid.boundaries().len() == 1
&& let Some(Ok(body)) = crate::coplanar::inset(a, b)
&& let (Some(ma), Some(mb)) = &meshes
&& overlap(ma, mb, &ma.inside_index(), &mb.inside_index()).is_some_and(|(b_in_a, a_in_b)| b_in_a < NONE && a_in_b < NONE)
&& check(&body).0
{
return Ok(Some(body));
}
// Bores on a common cylinder (a pad's bore around the hole it is joined over).
if op == BoolOp::Union && !BORES.with(|c| c.get()) && crate::coplanar::has_coincident_cylinders(a, b) {
BORES.with(|c| c.set(true));
@@ -548,7 +578,11 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
}
return Ok(Some(body));
}
last = format!("implausible result volume {v:.4}");
last = if unfused(&body) {
"the bodies touch along faces that could not be fused".into()
} else {
format!("implausible result volume {v:.4}")
};
}
Err(e) => last = e.to_string(),
},