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) {