From dfcdefa920e3fc4554ae41e4ab5f52fb9d35bb1a Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:45:52 -0400 Subject: [PATCH] STEP import: a loop jumps at a pole to within the tolerance, as its pcurves closed it The pcurves close a face's loop through a pole to within the face's tolerance (a point where crossing half a period moves less than that): there the periodic parameter is free and the loop jumps across. The reader's check that no loop crosses a seam without a seam edge took a pole only exactly (1e-9 relative), so it refused the loops the pcurves had closed near one. Poppy Humanoid's nearly round pin ends (a spindle torus of major radius 0.005, minor 0.345) have their apex vertex at the tube's top, 3.6e-5 mm above the true pole: their meridians end 1.7e-6 rad short of it, and 13 faces were refused. Both now use one test, SurfaceEval::pole_within. Test: a point 2e-7 mm from a sphere's axis is a pole within 1e-6 mm, not within 1e-7. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2 --- crates/geom/src/brep/eval.rs | 20 ++++++++++++++++++++ crates/geom/src/brep/pcurve.rs | 14 +------------- crates/geom/src/brep/tests.rs | 13 +++++++++++++ crates/step/src/topology.rs | 7 ++++--- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/crates/geom/src/brep/eval.rs b/crates/geom/src/brep/eval.rs index 420579fac5..73b6260ccc 100644 --- a/crates/geom/src/brep/eval.rs +++ b/crates/geom/src/brep/eval.rs @@ -241,6 +241,26 @@ impl SurfaceEval { let (a, b) = (su.len(), sv.len()); [a <= 1e-9 * b.max(1e-12), b <= 1e-9 * a.max(1e-12)] } + + /// [`Self::pole`], or within `reach` mm of one: moving along a direction across half its + /// period (or its whole range) moves the point no more than `reach` (a boundary point that + /// lies at a pole to within a tolerance, where its periodic parameter is free). + pub fn pole_within(&self, uv: [f64; 2], reach: f64) -> [bool; 2] { + let mut out = self.pole(uv); + let Some((_, su, sv)) = self.derivs(uv[0], uv[1]) else { return out }; + let dom = self.domain(); + for (d, speed) in [su.len(), sv.len()].into_iter().enumerate() { + let span = match (dom.period.get(d).copied().flatten(), dom.range.get(d).copied().flatten()) { + (Some(period), _) => 0.5 * period, + (None, Some((a, b))) => (b - a).abs(), + (None, None) => continue, + }; + if let Some(flag) = out.get_mut(d) { + *flag |= speed * span <= reach; + } + } + out + } } /// A tensor spline's rows (`rows[i][j]`, i along u) as the evaluator's u-fastest array. diff --git a/crates/geom/src/brep/pcurve.rs b/crates/geom/src/brep/pcurve.rs index 8460e5dff3..5c31d60ae3 100644 --- a/crates/geom/src/brep/pcurve.rs +++ b/crates/geom/src/brep/pcurve.rs @@ -529,19 +529,7 @@ impl On { /// parameter by half its period (or across its range) moves the point by no more than `reach` /// at its speed there. There the parameter is as free as at the pole itself. fn pole(&self, uv: [f64; 2], reach: f64) -> [bool; 2] { - let mut out = self.surface.pole(uv); - let Some((_, su, sv)) = self.surface.derivs(uv[0], uv[1]) else { return out }; - for (d, speed) in [su.len(), sv.len()].into_iter().enumerate() { - let span = match (self.dom.period.get(d).copied().flatten(), self.dom.range.get(d).copied().flatten()) { - (Some(period), _) => 0.5 * period, - (None, Some((a, b))) => (b - a).abs(), - (None, None) => continue, - }; - if let Some(flag) = out.get_mut(d) { - *flag |= speed * span <= reach; - } - } - out + self.surface.pole_within(uv, reach) } /// The loop's uses moved by whole periods so each starts where the one before it ends (in diff --git a/crates/geom/src/brep/tests.rs b/crates/geom/src/brep/tests.rs index 02fd84fe1f..48fa9e14f6 100644 --- a/crates/geom/src/brep/tests.rs +++ b/crates/geom/src/brep/tests.rs @@ -844,3 +844,16 @@ fn a_face_box_reaches_its_boundary_extremes() { let [_, v] = form::extent(&cylinder, &[vec![(&cut, [0.1, 0.1 + TAU], true)]]).expect("an extent"); assert!((v[0] - 10.0).abs() < 1e-9 && (v[1] - 50.0).abs() < 1e-9, "{v:?}"); } + +/// A point next to a sphere's pole is a pole to within the reach it is given: crossing half a +/// turn of longitude there moves it about π times its distance from the axis, and no further. +#[test] +fn a_point_beside_a_pole_is_one_within_its_reach() { + let sphere = eval::SurfaceEval::new(&Surface::Sphere { frame: a_frame(), radius: 2. }).unwrap(); + let near = [0.3, FRAC_PI_2 - 1e-7]; + // 2e-7 mm from the axis: half a turn moves it 2π·1e-7. + assert_eq!(sphere.pole(near), [false, false]); + assert_eq!(sphere.pole_within(near, 1e-6), [true, false]); + assert_eq!(sphere.pole_within(near, 1e-7), [false, false]); + assert_eq!(sphere.pole_within([0.3, 0.2], 1e-3), [false, false]); +} diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index 5ce1bd5668..e4d5ba0ca4 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -1104,10 +1104,11 @@ impl<'b, 'a> Builder<'b, 'a> { // does not wrap: a loop around a closed spline surface without a seam edge jumps across // its domain. let natural = surface.natural(); - // At a pole (where the surface collapses to a point across a direction) a loop may jump - // in that direction: the seam's two uses meet there a period apart. + // At a pole (where the surface collapses to a point across a direction, to within the + // tolerance, as the pcurves closed the loop there) a loop may jump in that direction: the + // seam's two uses meet there a period apart. let poles = SurfaceEval::new(&surface).ok(); - let at_pole = |uv: [f64; 2]| poles.as_ref().is_some_and(|e| e.pole(uv).iter().any(|p| *p)); + let at_pole = |uv: [f64; 2]| poles.as_ref().is_some_and(|e| e.pole_within(uv, tol).iter().any(|p| *p)); for (l, pc) in edge_loops.iter().zip(&pcs) { let mut ends = Vec::with_capacity(l.len()); for (&(_, forward), c) in l.iter().zip(pc) {