diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs index 4db35a63..54f0ee80 100644 --- a/crates/engine/src/boolean_regression_tests.rs +++ b/crates/engine/src/boolean_regression_tests.rs @@ -1,7 +1,7 @@ //! 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 serde_json::{Value, json}; use crate::Session; @@ -32,3 +32,22 @@ fn shallow_loft_cut_from_the_top_face() { let got = volume(&s, "Body1"); assert!((got - want).abs() < 1e-9 * want, "{got} vs {want}"); } + +#[test] +fn oblique_cut_through_a_small_arc_matches_a_split() { + let scripts = [include_str!("../../../examples/regressions/oblique_cut_of_cylinder.json")]; + // The same bar split by the prism's tilted top: the piece above it is the cut's result. + let mut split = Session::default(); + let script: Value = serde_json::from_str(scripts[0]).unwrap(); + let steps = script.get("commands").and_then(Value::as_array).unwrap(); + split.run_script(&Value::Array(steps[..5].to_vec())).unwrap(); + let whole = volume(&split, "Tool"); + split.execute("solid.split_body", &json!({"body": "Tool", "plane": {"origin": [0, -472, 853.91], "normal": [0, 15.44, 40]}})).unwrap(); + let above = volume(&split, "Tool"); + assert!(above < whole - 10.0, "{above} of {whole}"); + for script in scripts { + let s = replay(script); + let got = volume(&s, "Tool"); + assert!((got - above).abs() < 1e-6 * above, "{got} vs {above}"); + } +} diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index 1b9f9afd..14de038f 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -189,11 +189,28 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { r } +/// Smallest side of the box where the bodies' vertex boxes overlap (infinite when they don't). +fn meeting_size(a: &Body, b: &Body) -> f64 { + let bbox = |x: &Body| { + let mut bb = solvecraft_geom::Aabb3::EMPTY; + for v in x.solid.vertex_iter() { + bb.add(from_p3(v.point())); + } + bb + }; + let (ba, bb) = (bbox(a), bbox(b)); + let s = solvecraft_geom::Aabb3 { min: ba.min.max(bb.min), max: ba.max.min(bb.max) }.size(); + if s.x > 0.0 && s.y > 0.0 && s.z > 0.0 { s.x.min(s.y).min(s.z) } else { f64::INFINITY } +} + fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { a.require_brep("a boolean")?; b.require_brep("a boolean")?; let size = a.size().max(b.size()); - let tol_m = size * 5e-4; + // The checks mesh both bodies; where they meet in a region much thinner than they are (a + // thin part trimmed by a long one), a step from their size would facet the small faces + // there coarsely enough to fail the membership test on a correct result. + let tol_m = (size * 5e-4).min(meeting_size(a, b) * 2e-3).max(size * 1e-5); let meshes = (a.tessellate(tol_m).ok(), b.tessellate(tol_m).ok()); let (va, vb) = match &meshes { (Some(ma), Some(mb)) => (ma.measure().volume, mb.measure().volume), diff --git a/examples/regressions/oblique_cut_of_cylinder.json b/examples/regressions/oblique_cut_of_cylinder.json new file mode 100644 index 00000000..3fd277ef --- /dev/null +++ b/examples/regressions/oblique_cut_of_cylinder.json @@ -0,0 +1,15 @@ +{ + "description": "A thin 437 mm bar whose profile has a 3 mm concave arc, cut by a long prism whose top is tilted 21 degrees to the bar (an elliptical cut through the arc's cylinder). The check on the result meshed both bodies at a step from the 920 mm prism, which faceted the arc so coarsely that a correct cut failed the membership test. Result: the bar with its end trimmed by the tilted plane, as a split by that plane gives.", + "commands": [ + {"command": "sketch.create", "params": {"plane": "XZ", "offset": -462, "name": "Profile"}}, + {"command": "sketch.line", "params": {"points": [[147.4407, -849.95], [147.4407, -851.0], [151.3952, -851.0], [151.3952, -847.4709], [150.3952, -847.4709]]}}, + {"command": "sketch.arc.center_point", "params": {"center": [147.4407, -846.95], "start": "l1.start", "end": "l4.end"}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"sketch": "Profile", "profiles": "all", "direction": "positive", "start_offset": -1, "distance": 437.52, "operation": "new", "body_name": "Tool"}}, + {"command": "sketch.create", "params": {"plane": "YZ", "name": "Clip"}}, + {"command": "sketch.line", "params": {"points": [[-472, 853.91], [-432, 838.47], [-432, 820], [-472, 820]], "closed": true}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"sketch": "Clip", "profiles": "all", "start_offset": -10, "distance": 920, "operation": "new", "body_name": "ClipBody"}}, + {"command": "solid.combine", "params": {"target": "Tool", "tools": ["ClipBody"], "operation": "cut", "keep_tools": true}} + ] +}