diff --git a/crates/kernel/src/coplanar.rs b/crates/kernel/src/coplanar.rs index bc2b20ba..661d6cff 100644 --- a/crates/kernel/src/coplanar.rs +++ b/crates/kernel/src/coplanar.rs @@ -201,6 +201,13 @@ fn slab_beyond(n: Vec3, d: f64, depth: f64, extent: f64) -> Result { 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> { a.require_brep("a boolean")?; b.require_brep("a boolean")?; @@ -94,6 +100,18 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { bounds && consistent }; let mut last = String::new(); + // Coincident faces make the intersection fail after many retries; push them apart first. + if !APART.with(|c| c.get()) && crate::coplanar::has_coincident_faces(a, b) { + APART.with(|c| c.set(true)); + let r = crate::coplanar::boolean_apart(a, b, op); + APART.with(|c| c.set(false)); + if let Some(Ok(Some(body))) = r { + let v = volume(&body); + if v > 0.0 && plausible(v, &body) { + return Ok(Some(body)); + } + } + } let mut empty_votes = 0; for (attempt, j) in JITTER.iter().enumerate() { let shift = mt::Vector3::new(j[0], j[1], j[2]) * (size * 0.01); @@ -139,9 +157,6 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { } } // Coincident planar faces: push them apart (exactly) and try again. - thread_local! { - static APART: std::cell::Cell = const { std::cell::Cell::new(false) }; - } if !APART.with(|c| c.get()) { APART.with(|c| c.set(true)); let r = crate::coplanar::boolean_apart(a, b, op);