Spline surfaces: a closed spline whose outermost knots appear once is periodic

uv::closed tells a spline closed on itself (periodic, one knot interval per period) by
its first and last control rows and knots clamped at both ends, checked as k[0..=p] and
k[n..=n+p] all equal. Over its interval [k[p], k[n]] a spline of degree p never uses
k[0] or k[n+p]: it starts at its first control point and ends at its last when
k[1..=p] and k[n..n+p-1] repeat. A periodic quadratic circle written with knot
multiplicities 1,2,...,2,1 (as OpenCascade writes one) was taken as open, so its seam
edge's two uses got one pcurve and the face was refused as a loop across a closed
spline surface's seam without a seam edge (periodic_knot_tube_faces_import_and_mesh:
"pcurves failed on 2 of 2 faces"). The check now skips the outermost knots; the knots
the existing test calls unclamped (uniform) stay open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:20:53 -04:00
committed by Irakli Kourtis
parent 44b859fe6e
commit ce71791ece
+10 -2
View File
@@ -99,10 +99,14 @@ fn same_point(a: Option<&[f64; 4]>, b: Option<&[f64; 4]>, extent: f64) -> bool {
}
}
/// Knots of degree `p` over `n` control points, clamped at both ends.
/// Knots of degree `p` over `n` control points, clamped at both ends: the spline starts at its
/// first control point and ends at its last, its interval `[k[p], k[n]]` ending on knots of
/// multiplicity `p` within it. The outermost knots (`k[0]`, `k[n + p]`) take no part in the
/// spline over its interval, so they need not repeat: a periodic quadratic circle written with
/// knot multiplicities `1, 2, …, 2, 1` (as OpenCascade writes one) is clamped.
fn clamped(p: usize, n: usize, k: &[f64]) -> bool {
let ends = |r: std::ops::RangeInclusive<usize>| k.get(r).is_some_and(|s| s.windows(2).all(|w| w.first() == w.get(1)));
k.len() == n + p + 1 && ends(0..=p) && ends(n..=n + p)
p >= 1 && k.len() == n + p + 1 && ends(1..=p) && ends(n..=n + p - 1)
}
/// `x` moved by whole periods `b − a` into `[a, b]` when it lies outside.
@@ -411,6 +415,10 @@ mod tests {
let dom = domain(&s);
let Some(q) = project(&s, &dom, p, Some([0.98, 0.5]), 1e-9) else { panic!("projects") };
assert!((unwrap(q[0], 0.98, dom.period[0]) - 1.02).abs() < 1e-9, "{q:?}");
// Knots whose outermost values do not repeat, as OpenCascade writes a periodic circle:
// the interval still ends on knots of multiplicity 2, so the spline is closed there.
let outer_once = ring([0.0; 4], vec![-0.25, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.25]);
assert_eq!(domain(&outer_once).period, [Some(1.0), None]);
// Ends that only come near each other, a weight that differs, or unclamped knots: open,
// and evaluated past the interval as before (the end span continued).
for (what, open) in [