From 9c51f9d4335e5e082ca585872f46f6aa2027ee58 Mon Sep 17 00:00:00 2001 From: Giovanni <1319291+gvocale@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:50:00 -0400 Subject: [PATCH] Retry a face's parameter boundary leniently (#970) Cutting one lofted round tube from another failed in truck's divide_face ("parameter boundary") for some section counts: the intersection points lie on the loft's seam, the previous point's parameter can sit a hair outside the range, and the Newton search from there fails. The boundary is built as before; only when that returns None is it built again leniently, starting each search from the hint clamped into the parameter range and, as a last resort, searching the whole surface. Nothing that succeeds today changes. The boolean-retry guards are untouched. Fixes 5 of the 6 failing hollow-elbow cases in #968 (new test crates/engine/src/tests/loft_cut.rs, volumes within 0.3% of exact). 90 degrees / 14 sections still fails in a later step and stays open in #968. --- crates/engine/src/tests.rs | 1 + crates/engine/src/tests/loft_cut.rs | 53 +++++++++++++++++++ .../src/transversal/divide_face/mod.rs | 42 ++++++++++++--- 3 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 crates/engine/src/tests/loft_cut.rs diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c49a473f..ef56c232 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -254,6 +254,7 @@ fn join_of_two_overlapping_rings_is_not_refused() { mod boolean_diagnostics; mod component_frames; +mod loft_cut; mod mirror_planes; mod path_scope; mod primitive_points; diff --git a/crates/engine/src/tests/loft_cut.rs b/crates/engine/src/tests/loft_cut.rs new file mode 100644 index 00000000..7bb5dcf3 --- /dev/null +++ b/crates/engine/src/tests/loft_cut.rs @@ -0,0 +1,53 @@ +//! A hollow elbow: a tube lofted from round sections along an arc, minus a narrower tube lofted the +//! same way (and running 5 mm past both ends, so the cut opens the ends). +//! +//! The boolean gave up with "no result" for some section counts and not for their neighbours: the +//! points of the intersection curves lie on the seam between the two halves of a loft's circle, +//! where truck's search for their parameters (from the previous point's) failed. + +use std::f64::consts::PI; + +use super::*; + +/// `sections` circles of radius `radius` centred on an arc of radius `path` turning `angle` +/// (radians), in planes normal to the arc, lofted into the body `body`; `over` extends the +/// sections past both ends along the tangent. +fn tube(s: &mut Session, body: &str, radius: f64, path: f64, angle: f64, sections: usize, over: f64) { + let mut stations: Vec<[f64; 4]> = (0..sections) + .map(|i| { + let t = angle * i as f64 / (sections - 1) as f64; + [path * t.cos(), path * t.sin(), -t.sin(), t.cos()] + }) + .collect(); + if over > 0.0 { + let (a, b) = (stations[0], stations[sections - 1]); + stations.insert(0, [a[0] - over * a[2], a[1] - over * a[3], a[2], a[3]]); + stations.push([b[0] + over * b[2], b[1] + over * b[3], b[2], b[3]]); + } + let mut list = Vec::new(); + for (i, [x, y, tx, ty]) in stations.iter().enumerate() { + let name = format!("{body}{i}"); + run(s, "sketch.create", json!({"plane": {"origin": [x, y, 0], "x_dir": [0, 0, 1], "y_dir": [ty, -tx, 0]}, "name": name})); + run(s, "sketch.circle.center", json!({"center": [0, 0], "radius": radius})); + run(s, "sketch.finish", json!({})); + list.push(json!({"sketch": name})); + } + run(s, "solid.loft", json!({"sections": list, "operation": "new", "name": format!("Loft{body}"), "body_name": body})); +} + +#[test] +fn a_hollow_elbow_cut_from_two_lofted_tubes() { + let (path, outer, inner) = (100.0, 30.0, 18.0); + // Each of these gave up in truck's `parameter boundary` step. + for (degrees, sections) in [(60.0, 5), (60.0, 7), (90.0, 10), (120.0, 7), (120.0, 13)] { + let angle = degrees * PI / 180.0; + let mut s = Session::default(); + tube(&mut s, "Outer", outer, path, angle, sections, 0.0); + tube(&mut s, "Inner", inner, path, angle, sections, 5.0); + run(&mut s, "solid.combine", json!({"target": "Outer", "tools": ["Inner"], "operation": "cut", "name": "Hollow"})); + assert_eq!(s.model.state().bodies.len(), 1, "{degrees} degrees, {sections} sections"); + let want = PI * (outer * outer - inner * inner) * path * angle; + let got = volume(&mut s); + assert!(rel(got, want) < 3e-3, "{degrees} degrees, {sections} sections: {got} vs {want}"); + } +} diff --git a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs index 75c2a774..2d8062a7 100644 --- a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs +++ b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs @@ -8,16 +8,26 @@ use std::ops::Deref; use truck_meshalgo::prelude::*; use truck_topology::*; +/// SolidCraft: `p` moved into the surface's parameter range (a point found on the boundary can lie a +/// hair outside it, and a search started from there fails). +fn clamped
>(surface: &S, p: Point2) -> Point2 { + match surface.try_range_tuple() { + (Some((u0, u1)), Some((v0, v1))) => Point2::new(p.x.clamp(u0, u1), p.y.clamp(v0, v1)), + _ => p, + } +} + fn create_parameter_boundary
( face: &Face
, wire: &Wire
,
polys: &mut HashMap >,
tol: f64,
+ lenient: bool,
) -> Option