Geometry: param_at_length checks spline validity before clamping to the domain (fixes #704) (#741)

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 05:09:01 -04:00
1 parent 5d39f8710c
commit b209bb53e9
1 file changed
+21 -1
+21 -1
View File
@@ -497,8 +497,12 @@ impl Spline {
/// backward for negative), clamped to the domain.
pub fn param_at_length(&self, from: f64, len: f64) -> f64 {
let (lo, hi) = self.domain();
if !self.is_valid() {
// Checked before `clamp`: an invalid spline's bounds may be reversed or NaN.
return if from.is_finite() { from } else { lo };
}
let from = if from.is_finite() { from.clamp(lo, hi) } else { lo };
if !self.is_valid() || !len.is_finite() || len == 0.0 {
if !len.is_finite() || len == 0.0 {
return from;
}
let (a, b, target) = if len > 0.0 { (from, hi, len) } else { (lo, from, -len) };
@@ -732,6 +736,22 @@ mod tests {
}
}
#[test]
fn param_at_length_on_invalid_domain_does_not_panic() {
// Reversed bounds (knots out of order) and a NaN bound both make the spline invalid; a
// finite start must come back unchanged rather than reach `clamp` with bad bounds.
let mut reversed = cubic();
reversed.knots.reverse();
assert!(!reversed.is_valid() && reversed.domain().0 > reversed.domain().1);
let mut nan = cubic();
nan.knots[nan.degree] = f64::NAN;
assert!(!nan.is_valid() && nan.domain().0.is_nan());
for s in [reversed, nan] {
assert_eq!(s.param_at_length(0.5, 1.0), 0.5);
assert_eq!(s.param_at_length(0.5, -1.0), 0.5);
}
}
#[test]
fn ellipse_arc_length() {
let c = Ellipse::full(Vec2::ZERO, Vec2::new(3.0, 0.0), 1.0);