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.
This commit is contained in:
1 parent
4687e3b073
commit
9c51f9d433
3 files changed
+88
-8
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
+34
-8
@@ -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<P, S: ParametricSurface<Point = P>>(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<P, C, S>(
|
||||
face: &Face<P, C, S>,
|
||||
wire: &Wire<P, C>,
|
||||
polys: &mut HashMap<EdgeID<C>, PolylineCurve<P>>,
|
||||
tol: f64,
|
||||
lenient: bool,
|
||||
) -> Option<PolylineCurve<Point2>>
|
||||
where
|
||||
P: Copy,
|
||||
C: BoundedCurve<Point = P> + ParameterDivision1D<Point = P>,
|
||||
S: Clone + SearchParameter<D2, Point = P> + SearchNearestParameter<D2, Point = P>,
|
||||
S: Clone + ParametricSurface<Point = P> + SearchParameter<D2, Point = P> + SearchNearestParameter<D2, Point = P>,
|
||||
{
|
||||
let surface = face.surface();
|
||||
let pt = wire.front_vertex().unwrap().point();
|
||||
@@ -35,10 +45,23 @@ where
|
||||
});
|
||||
let mut p = *vec.last().unwrap();
|
||||
let closure = |q: &P| -> Option<Point2> {
|
||||
p = surface
|
||||
.search_parameter(*q, Some(p.into()), 100)
|
||||
.or_else(|| surface.search_nearest_parameter(*q, Some(p.into()), 100))?
|
||||
.into();
|
||||
// SolidCraft: the points of an intersection curve on a lofted or swept surface lie on
|
||||
// the seam between its halves, a hair outside the parameter range, and Newton's
|
||||
// method from the previous point's parameter then fails (or the surface bends away
|
||||
// from it). `lenient`, the second try after the first gave up: start from the hint
|
||||
// moved into the range, and as a last resort search the whole surface.
|
||||
let hint = if lenient { clamped(&surface, p) } else { p };
|
||||
let found = surface
|
||||
.search_parameter(*q, Some(hint.into()), 100)
|
||||
.or_else(|| surface.search_nearest_parameter(*q, Some(hint.into()), 100));
|
||||
let found = if lenient {
|
||||
found
|
||||
.or_else(|| surface.search_parameter(*q, None, 100))
|
||||
.or_else(|| surface.search_nearest_parameter(*q, None, 100))
|
||||
} else {
|
||||
found
|
||||
};
|
||||
p = found?.into();
|
||||
Some(p)
|
||||
};
|
||||
let add: Option<Vec<Point2>> = match edge.orientation() {
|
||||
@@ -65,12 +88,15 @@ fn divide_one_face<C, S>(
|
||||
) -> Option<Vec<FaceWithShapesOpStatus<C, S>>>
|
||||
where
|
||||
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
|
||||
S: Clone + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
|
||||
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
|
||||
{
|
||||
let (mut pre_faces, mut negative_wires) = (Vec::new(), Vec::new());
|
||||
let mut map = HashMap::default();
|
||||
loops.iter().try_for_each(|wire| {
|
||||
let poly = crate::tr!(create_parameter_boundary(face, wire, &mut map, tol), "parameter boundary");
|
||||
let poly = crate::tr!(
|
||||
create_parameter_boundary(face, wire, &mut map, tol, false).or_else(|| create_parameter_boundary(face, wire, &mut map, tol, true)),
|
||||
"parameter boundary"
|
||||
);
|
||||
match poly.area() > 0.0 {
|
||||
true => pre_faces.push(vec![WireChunk { poly, wire }]),
|
||||
false => negative_wires.push(WireChunk { poly, wire }),
|
||||
@@ -133,7 +159,7 @@ pub fn divide_faces<C, S>(
|
||||
) -> Option<FacesClassification<Point3, C, S>>
|
||||
where
|
||||
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
|
||||
S: Clone + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
|
||||
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
|
||||
{
|
||||
let mut res = FacesClassification::<Point3, C, S>::default();
|
||||
shell
|
||||
|
||||
Reference in new issue
Block a user