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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 16:45:52 -04:00
committed by Irakli Kourtis
parent 31b9410f56
commit dfcdefa920
4 changed files with 38 additions and 16 deletions
+20
View File
@@ -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.
+1 -13
View File
@@ -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
+13
View File
@@ -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]);
}
+4 -3
View File
@@ -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) {