diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs new file mode 100644 index 00000000..4db35a63 --- /dev/null +++ b/crates/engine/src/boolean_regression_tests.rs @@ -0,0 +1,34 @@ +//! Booleans that failed, panicked or hung, replayed from the scripts in +//! `examples/regressions/` (each script's description says what went wrong). + +use serde_json::Value; + +use crate::Session; + +fn replay(script: &str) -> Session { + let mut s = Session::default(); + let script: Value = serde_json::from_str(script).unwrap(); + s.run_script(&script).unwrap(); + s +} + +fn volume(s: &Session, body: &str) -> f64 { + s.model.state().body(body).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0) +} + +/// Slab less a 557.68 × 100 × 120 pocket and the loft below its floor (556.68 × 99 down 4 mm to +/// 80 × 80; prismatoid: h/6 · (A_top + A_bottom + 4 A_mid)). +fn slab_pocket_loft() -> (f64, f64) { + let slab = 900.0 * 480.0 * 160.0; + let loft = 4.0 / 6.0 * (556.68 * 99.0 + 80.0 * 80.0 + 4.0 * (556.68 + 80.0) / 2.0 * (99.0 + 80.0) / 2.0); + (slab - 557.68 * 100.0 * 120.0, loft) +} + +#[test] +fn shallow_loft_cut_from_the_top_face() { + let s = replay(include_str!("../../../examples/regressions/shallow_loft_cut_from_top_face.json")); + let (_, loft) = slab_pocket_loft(); + let want = 900.0 * 480.0 * 160.0 - loft; + let got = volume(&s, "Body1"); + assert!((got - want).abs() < 1e-9 * want, "{got} vs {want}"); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 22a5106f..beb357bb 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -503,6 +503,8 @@ impl Session { } } +#[cfg(test)] +mod boolean_regression_tests; #[cfg(test)] mod construct_tests; #[cfg(test)] diff --git a/crates/kernel/src/heal.rs b/crates/kernel/src/heal.rs index fcd17878..b86c5858 100644 --- a/crates/kernel/src/heal.rs +++ b/crates/kernel/src/heal.rs @@ -404,6 +404,12 @@ pub fn heal(solid: Solid, size: f64) -> Solid { /// [`heal`], keeping faces apart along lines they were split along on purpose. pub(crate) fn heal_keep(solid: Solid, size: f64, keep: &[Vec]) -> Solid { + // Edges a boolean made are intersection curves, which panic where they no longer + // evaluate; the solid then stays as it is. + crate::guard("heal", || Ok(heal_shells(&solid, size, keep))).ok().flatten().unwrap_or(solid) +} + +fn heal_shells(solid: &Solid, size: f64, keep: &[Vec]) -> Option { let tol = (size * 1e-7).max(1e-9); let mut shells = Vec::new(); let mut changed = false; @@ -428,9 +434,9 @@ pub(crate) fn heal_keep(solid: Solid, size: f64, keep: &[Vec]) -> Solid { shells.push(cur); } if !changed { - return solid; + return None; } - Solid::try_new(shells).unwrap_or(solid) + Solid::try_new(shells).ok() } #[cfg(test)] diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index b812cbb7..1b9f9afd 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -296,7 +296,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { empty_votes += 1; last = "empty result".into(); } - Ok(s) => match Body::new(crate::heal::heal_keep(s, size, &keep)) { + Ok(s) => match guard("boolean result", || Body::new(crate::heal::heal_keep(s, size, &keep))) { Ok(body) => { let (ok, v) = check(&body); if ok { diff --git a/examples/regressions/shallow_loft_cut_from_top_face.json b/examples/regressions/shallow_loft_cut_from_top_face.json new file mode 100644 index 00000000..a898231b --- /dev/null +++ b/examples/regressions/shallow_loft_cut_from_top_face.json @@ -0,0 +1,16 @@ +{ + "description": "A shallow, wide loft cut starting in a body's top face: slab 900 x 480 x 160, loft cut from a 556.68 x 99 rectangle on the top face down 4 mm to an 80 x 80 square (sides about 0.9 degrees from horizontal). Healing an intermediate boolean result panicked outside the boolean's guard and stopped every later attempt. Result: the slab less the loft, 69 003 015.31 mm³.", + "commands": [ + {"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [900, 480]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 160, "name": "Slab"}}, + {"command": "sketch.create", "params": {"plane": "XY", "offset": 160, "name": "LoftTop"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [171.66, 329.34], "p1": [728.34, 428.34]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "sketch.create", "params": {"plane": "XY", "offset": 156, "name": "LoftBottom"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [410, 338.84], "p1": [490, 418.84]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.loft", "params": {"sections": [{"sketch": "LoftTop"}, {"sketch": "LoftBottom"}], "operation": "cut", "name": "LoftCut"}} + ] +}