STEP import: a face's loop through a pole closes its turn, so a dimple keeps its area

A round face's box (form::extent) unwrapped each periodic parameter from one boundary point to
the next. Through a pole the loop goes on from anywhere round the direction the pole collapses:
a hemisphere bounded by its rim and its seam put its second seam use on the first and its box a
quarter turn off the seam, while its pcurves ran from the seam. Past the first pole the loop is
now moved the whole periods that close it. Reference case 14's re-imported dimple had a
quarter of its area missing (9601 against 9706 mm2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 02:19:02 -04:00
committed by Irakli Kourtis
parent 4a0c139106
commit 271f0c5de1
2 changed files with 61 additions and 11 deletions
+40 -11
View File
@@ -590,6 +590,11 @@ pub fn extent(label: &Surface, loops: &[Vec<(&Curve, [f64; 2], bool)>]) -> Optio
// and a box over both is more than a turn wide.
let (mut l_lo, mut l_hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
let mut prev: Option<[f64; 2]> = None;
// The loop's bounds in each direction, each with the number of poles the loop had passed
// through when it got there; and the first and last points off a pole.
let mut bounds: Vec<(usize, usize, f64)> = Vec::new();
let (mut crossings, mut on_pole) = (0usize, false);
let mut first: Option<[f64; 2]> = None;
for (c, [t0, t1], forward) in l {
let curve = super::eval::CurveEval::new(c).ok()?;
// The point at `t` on the label, a periodic parameter unwrapped next to `near`.
@@ -604,28 +609,34 @@ pub fn extent(label: &Surface, loops: &[Vec<(&Curve, [f64; 2], bool)>]) -> Optio
}
Some(q)
};
let mut samples: Vec<(f64, [f64; 2], [bool; 2])> = Vec::with_capacity(EXTENT_POINTS + 1);
let mut samples: Vec<(f64, [f64; 2], [bool; 2], usize)> = Vec::with_capacity(EXTENT_POINTS + 1);
for k in 0..=EXTENT_POINTS {
let f = k as f64 / EXTENT_POINTS as f64;
let t = if *forward { t0 * (1.0 - f) + t1 * f } else { t1 * (1.0 - f) + t0 * f };
let q = at(t, prev)?;
// At a pole the collapsed parameter is anything: it bounds nothing.
let pole = eval.pole(q);
for d in 0..2 {
if !pole.get(d).copied().unwrap_or(false) {
l_lo[d] = l_lo[d].min(q[d]);
l_hi[d] = l_hi[d].max(q[d]);
if pole.iter().any(|p| *p) {
on_pole = first.is_some();
} else {
if on_pole {
crossings += 1;
on_pole = false;
}
}
if !pole.iter().any(|p| *p) {
first = first.or(Some(q));
prev = Some(q);
}
samples.push((t, q, pole));
for (d, x) in q.iter().enumerate() {
if !pole.get(d).copied().unwrap_or(false) {
bounds.push((crossings, d, *x));
}
}
samples.push((t, q, pole, crossings));
}
// A sample is a point of the boundary, not its extreme: each sample higher (or lower)
// than its neighbours brackets an extreme of the edge, which is found there.
for k in 0..samples.len() {
let (_, q, pole) = samples[k];
let (_, q, pole, passed) = samples[k];
let (a, b) = (samples[k.saturating_sub(1)].0, samples[(k + 1).min(samples.len() - 1)].0);
for d in 0..2 {
// Next to a pole the parameter it collapses is no function of the point: a
@@ -646,12 +657,30 @@ pub fn extent(label: &Surface, loops: &[Vec<(&Curve, [f64; 2], bool)>]) -> Optio
}
let f = |t: f64| at(t, Some(q)).map_or(f64::NEG_INFINITY, |p| sign * p[d]);
let x = sign * golden_max(&f, a.min(b), a.max(b));
l_lo[d] = l_lo[d].min(x);
l_hi[d] = l_hi[d].max(x);
bounds.push((passed, d, x));
}
}
}
}
// Through a pole the loop goes on from anywhere round the periodic direction it
// collapses, so continuing from the point before it is no measure of where: a loop
// through a pole closes in (u, v), the pole taking up its turn (a hemisphere bounded by its
// rim and its seam, down the seam to the pole and up it on the other side). Past the
// first pole, the loop is moved the whole periods that close it.
let last = prev;
for d in 0..2 {
let shift = match (period.get(d), first, last) {
(Some(Some(per)), Some(f), Some(e)) if crossings > 0 => -((e[d] - f[d]) / per).round() * per,
_ => 0.0,
};
for &(passed, dd, x) in &bounds {
if dd == d {
let x = if passed > 0 { x + shift } else { x };
l_lo[d] = l_lo[d].min(x);
l_hi[d] = l_hi[d].max(x);
}
}
}
for d in 0..2 {
if !(l_lo[d] <= l_hi[d]) {
continue;
+21
View File
@@ -850,6 +850,27 @@ fn a_hole_beside_the_seam_lies_in_its_face_turn() {
assert!(v[0].abs() < 1e-9 && (v[1] - h).abs() < 1e-9, "{v:?}");
}
/// A hemisphere bounded by its rim and its seam, its loop starting a quarter turn along the rim
/// (the order a re-imported dimple's loop came in): along the rim to the seam, down the seam to
/// the pole, up it on the other side and round the rest of the rim. Through the pole the loop goes
/// on a turn away; continuing from the point before the pole put the seam's second use on the
/// first and the box a quarter turn off the seam (oracle 14: the re-imported face's form over
/// -270..90 degrees, its pcurves over 0..360, and its area a quarter short).
#[test]
fn a_loop_through_a_pole_closes_its_turn_from_the_seam() {
let r = 15.0f64;
let sphere = Surface::Sphere { frame: Frame { origin: [0.; 3], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] }, radius: r };
let rim = Curve::Circle { frame: Frame { origin: [0.; 3], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] }, radius: r };
let seam = Curve::Circle { frame: Frame { origin: [0.; 3], x_axis: [1., 0., 0.], y_axis: [0., 0., -1.] }, radius: r };
let outer =
vec![(&rim, [0., FRAC_PI_2], false), (&seam, [0., FRAC_PI_2], true), (&seam, [0., FRAC_PI_2], false), (&rim, [FRAC_PI_2, TAU], false)];
let [u, v] = form::extent(&sphere, &[outer]).expect("an extent");
assert!((u[1] - u[0] - TAU).abs() < 1e-9, "one turn: {u:?}");
let from = u[0].rem_euclid(TAU);
assert!(from.min(TAU - from) < 1e-9, "from the seam: {u:?}");
assert!((v[0] + FRAC_PI_2).abs() < 1e-9 && v[1].abs() < 1e-9, "{v:?}");
}
/// A face's box reaches its boundary's extremes, not its samples' nearest ones: an ellipse cut
/// from a cylinder of radius 20 by a plane at 45 degrees rises 20 above its centre, at an angle
/// between the points the box is sampled at. Short of it, the face's NURBS form stopped below the