{
crate::extrude(&pl, &[r], 0.0, depth)?.pop().ok_or_else(|| KernelError::Failed("slab".into()))
}
+/// Do the bodies have planar faces in a common plane?
+pub(crate) fn has_coincident_faces(a: &Body, b: &Body) -> bool {
+ let tol = (a.size().max(b.size()) * 1e-7).max(1e-9);
+ let (pa, pb) = (planes(a), planes(b));
+ pa.iter().any(|(_, x)| pb.iter().any(|(_, y)| x.n.dot(y.n).abs() > 1.0 - 1e-9 && (x.d - y.d * x.n.dot(y.n)).abs() < tol * 10.0))
+}
+
/// Retry a failed boolean with coincident planar faces pushed apart. `None` when there are no
/// coincident faces or the faces can't be pushed.
pub(crate) fn boolean_apart(a: &Body, b: &Body, op: BoolOp) -> Option>> {
diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs
index 2513699d..cd5acdbd 100644
--- a/crates/kernel/src/ops.rs
+++ b/crates/kernel/src/ops.rs
@@ -72,6 +72,12 @@ fn mismatch(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh, result: &solve
/// Boolean of two bodies. The result may be empty (`Ok(None)`) for a cut that removes
/// everything or an intersection of disjoint bodies. Results are checked against volume bounds
/// and retried with shifted copies and other tolerances when they fail or look wrong.
+thread_local! {
+ /// Set while a boolean runs on bodies whose coincident faces were pushed apart (no
+ /// second round of pushing).
+ static APART: std::cell::Cell = const { std::cell::Cell::new(false) };
+}
+
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result