smlib export: a face's loop crosses its poles where the face's parameter box says

Where two edge uses of a loop meet at a pole (meridians of a sphere), the periodic parameter's
jump there is free, and the pcurves took whichever jump made the loop counter-clockwise. Two
meridians a three-quarter turn apart bound a quarter of the sphere or three quarters of it, and
the loop alone can't tell which: a half disc revolved 270 degrees on SMLib came back as a quarter
ball turned inside out (sweeps/2667, V = -226 against 678.6).

pcurve::pcurves_within takes the face's parameter box, as SMLib gives it, and picks the jumps
whose extent in that parameter is nearest the box's; pcurves is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruck
2026-10-21 11:35:08 -04:00
committed by jyviko
parent 5014c4f17f
commit 8ad09ec64c
3 changed files with 67 additions and 7 deletions
+44 -6
View File
@@ -67,6 +67,20 @@ const CHECKS: usize = 16;
/// the surface. Loops are as the record keeps them (relative to the surface's natural normal: outer counter-clockwise, inner clockwise); each
/// pcurve runs on its use's interval in its curve's direction, whatever `forward` says.
pub fn pcurves(surface_record: &Surface, loops: &[Vec<EdgeUse<'_>>], tolerance: f64) -> Result<Vec<Vec<Curve2>>> {
pcurves_within(surface_record, loops, tolerance, None)
}
/// [`pcurves`] for a face whose parameter box is known (`domain`, u then v, as its kernel
/// gives it): where two uses meet at a pole, the periodic parameter's jump there is free, and
/// the loop takes the jumps whose extent in that parameter is nearest the box's. Two meridians
/// a three-quarter turn apart bound a quarter of a sphere or three quarters of it; the loop alone
/// can't tell which, the box can.
pub fn pcurves_within(
surface_record: &Surface,
loops: &[Vec<EdgeUse<'_>>],
tolerance: f64,
domain: Option<[[f64; 2]; 2]>,
) -> Result<Vec<Vec<Curve2>>> {
if !(tolerance.is_finite() && tolerance > 0.0 && tolerance <= 1e3) {
return Err(Error::Invalid(format!("pcurve tolerance {tolerance}")));
}
@@ -76,7 +90,7 @@ pub fn pcurves(surface_record: &Surface, loops: &[Vec<EdgeUse<'_>>], tolerance:
}
let mut budget = Budget(0);
surface(surface_record, 0, &mut budget).map_err(|e| Error::Invalid(e.to_string()))?;
let on = On::new(surface_record)?;
let on = On::new(surface_record)?.within(domain);
// A seam edge used twice is fitted once.
let mut done: Vec<(u32, Curve2)> = Vec::new();
let mut out = Vec::with_capacity(loops.len());
@@ -117,6 +131,8 @@ pub fn pcurves(surface_record: &Surface, loops: &[Vec<EdgeUse<'_>>], tolerance:
struct On {
surface: SurfaceEval,
dom: Domain,
/// The face's own parameter box, where its kernel gives one.
face: Option<[[f64; 2]; 2]>,
}
/// A curve sample: its parameter, point, surface parameters (periodic ones unwrapped along the
@@ -135,7 +151,12 @@ impl On {
fn new(s: &Surface) -> Result<Self> {
let surface = SurfaceEval::new(s)?;
let dom = surface.domain();
Ok(On { surface, dom })
Ok(On { surface, dom, face: None })
}
fn within(self, face: Option<[[f64; 2]; 2]>) -> Self {
let face = face.filter(|f| f.iter().flatten().all(|x| x.is_finite()));
On { face, ..self }
}
fn pcurve(&self, record: &Curve, curve: &CurveEval, range: [f64; 2], tol: f64) -> Result<Curve2> {
@@ -418,7 +439,23 @@ impl On {
let other = uses.iter().flat_map(|(c, _)| points(c).into_iter().filter_map(|q| q.get(1 - d).copied()));
let (lo, hi) = other.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| (a.min(x), b.max(x)));
let flat = if hi >= lo { 1e-9 * period * (hi - lo) } else { 0.0 };
let mut best: Option<(bool, usize, Vec<f64>)> = None;
// With the face's box: how far the loop's extent along `d` is from the box's
// (in steps of a millionth of a period, so rounding doesn't decide).
let width = self.face.and_then(|f| f.get(d).copied()).map(|[a, b]| (b - a).abs());
let misfit = |extra: &[f64]| -> u64 {
let Some(width) = width else { return 0 };
let (mut a, mut b) = (f64::INFINITY, f64::NEG_INFINITY);
for ((c, _), s) in uses.iter().zip(extra) {
for q in points(c) {
if let Some(x) = q.get(d) {
a = a.min(x + s);
b = b.max(x + s);
}
}
}
if b < a { 0 } else { ((b - a - width).abs() / period * 1e6).round() as u64 }
};
let mut best: Option<(u64, bool, usize, Vec<f64>)> = None;
for combo in 0..3usize.pow(free.len() as u32) {
let (mut code, mut moved, mut add) = (combo, 0, 0.0);
let mut extra = shift.clone();
@@ -434,11 +471,12 @@ impl On {
}
}
let right = area(uses, &extra, d) > flat;
if best.as_ref().is_none_or(|(r, m, _)| (right && !r) || (right == *r && moved < *m)) {
best = Some((right, moved, extra));
let fit = misfit(&extra);
if best.as_ref().is_none_or(|(f, r, m, _)| fit < *f || (fit == *f && ((right && !r) || (right == *r && moved < *m)))) {
best = Some((fit, right, moved, extra));
}
}
if let Some((_, _, extra)) = best {
if let Some((_, _, _, extra)) = best {
shift = extra;
}
}
+22
View File
@@ -234,6 +234,28 @@ fn meridians_run_straight_into_the_poles() {
}
}
/// Two meridians a three-quarter turn apart bound a quarter of a sphere or three quarters of
/// it; the loop alone can't tell which. Given the face's box, the loop spans it: three quarters
/// of a turn in u (a ball revolved 270°), or a quarter where the box says so.
#[test]
fn meridians_at_a_pole_span_the_face_box() {
let sphere = Surface::Sphere { frame: about_z([0.; 3], false), radius: 6. };
let at = |a: f64| Curve::Circle { frame: frame([0.; 3], [a.cos(), a.sin(), 0.], [0., 0., 1.]), radius: 6. };
let (first, last) = (at(0.), at(1.5 * PI));
let half = [-FRAC_PI_2, FRAC_PI_2];
for (box_u, span) in [([0., 1.5 * PI], 1.5 * PI), ([-FRAC_PI_2, 0.], FRAC_PI_2)] {
for flip in [false, true] {
let l = vec![
EdgeUse { edge: 0, curve: &first, interval: half, forward: !flip, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &last, interval: half, forward: flip, tolerance: 0.0 },
];
let pcs = &pcurves_within(&sphere, std::slice::from_ref(&l), 1e-7, Some([box_u, [-FRAC_PI_2, FRAC_PI_2]])).unwrap()[0];
let (a, b) = (pcs[0].eval(0.).unwrap()[0], pcs[1].eval(0.).unwrap()[0]);
assert!(((a - b).abs() - span).abs() < 1e-9, "box {box_u:?}, flip {flip}: u {a} and {b}");
}
}
}
/// A whole sphere bounded only by its seam (one meridian from pole to pole, used twice), in
/// frames where rounding leaves the loop with the uses on one line a tiny positive area: the
/// two uses come out a period apart and the loop turns counter-clockwise around the whole
+1 -1
View File
@@ -1125,7 +1125,7 @@ fn pcurves(surface: &b::Surface, domain: [[f64; 2]; 2], loops: &[Loop], edges: &
}
uses.push(loop_uses);
}
let mut curves = b::pcurve::pcurves(surface, &uses, tolerance).map_err(|e| invalid(format!("pcurve: {e}")))?;
let mut curves = b::pcurve::pcurves_within(surface, &uses, tolerance, Some(domain)).map_err(|e| invalid(format!("pcurve: {e}")))?;
into_domain(surface, domain, &mut curves);
Ok(curves)
}