A thin part trimmed by a long one is no longer refused as an implausible result: a 437 mm bar with a 3 mm concave arc in its profile, cut by a 920 mm prism with a top tilted 21 degrees, failed with "implausible result volume 3870" (it was 3694 before the cut)
The boolean checks its result by meshing both operands and the result, then sampling membership densely where the operands' boxes overlap. The mesh step came from the larger body's size (0.46 mm here), which facets the bar's 3 mm arc so coarsely that two meshes of the same arc disagree over a few percent of the thin overlap box, and a correct cut failed the 0.4 % mismatch limit. The step now also follows the smallest side of the overlap box (2e-3 of it), never finer than 1e-5 of the size, so it only changes where the bodies meet in a region much thinner than they are. truck's first result for the cut is then accepted, and it matches a split of the bar by the tilted plane. Tests: oblique_cut_through_a_small_arc_matches_a_split replays examples/regressions/oblique_cut_of_cylinder.json and compares with solid.split_body by the same plane (fails on main). The full suite runs no slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f17ff913d6
commit
f3a689f284
3 files changed
+53
-2
No files matched your search
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -189,11 +189,28 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
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<Option<Body>> {
|
||||
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),
|
||||
|
||||
@@ -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}}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user