From b209bb53e9fb707c5467e37a13deb8dd58f1b232 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:09:01 +0200 Subject: [PATCH] Geometry: param_at_length checks spline validity before clamping to the domain (fixes #704) (#741) Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/geom/src/nurbs.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/geom/src/nurbs.rs b/crates/geom/src/nurbs.rs index e7d26a7f..df589d3c 100644 --- a/crates/geom/src/nurbs.rs +++ b/crates/geom/src/nurbs.rs @@ -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);