1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use smallvec::SmallVec;
30
31pub type BasisValues = SmallVec<[f64; 8]>;
33
34pub type DerivativeRows = SmallVec<[BasisValues; 4]>;
37
38const SMALL_ORDER: usize = 8;
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct KnotVector {
44 knots: Vec<f64>,
45 degree: usize,
46}
47
48impl KnotVector {
49 pub fn new(knots: Vec<f64>, degree: usize) -> OgeomResult<Self> {
59 if degree == 0 {
60 ogeom_bail!(Construction, "degree must be at least 1");
61 }
62 let minimum = 2 * (degree + 1);
65 if knots.len() < minimum {
66 ogeom_bail!(
67 Construction,
68 "degree {degree} needs at least {minimum} knots, got {}",
69 knots.len()
70 );
71 }
72 if !knots.iter().all(|k| k.is_finite()) {
73 ogeom_bail!(Construction, "knot vector contains a non-finite value");
74 }
75 if knots.windows(2).any(|w| w[1] < w[0]) {
76 ogeom_bail!(Construction, "knot vector is not non-decreasing");
77 }
78
79 let this = Self { knots, degree };
80 if this.domain_start() >= this.domain_end() {
81 ogeom_bail!(Construction, "knot vector spans an empty domain");
82 }
83
84 for end in [this.domain_start(), this.domain_end()] {
87 let count = this.multiplicity_of(end);
88 if count > this.degree + 1 {
89 ogeom_bail!(
90 Construction,
91 "end knot {end} has multiplicity {count}, above degree + 1 = {}",
92 this.degree + 1
93 );
94 }
95 }
96
97 let (first, last) = (this.degree, this.knots.len() - this.degree - 1);
99 let mut index = first;
100 while index < last {
101 let value = this.knots[index];
102 let mut count = 0;
103 while index < last && this.knots[index] == value {
104 count += 1;
105 index += 1;
106 }
107 if value > this.domain_start() && value < this.domain_end() && count > this.degree {
110 ogeom_bail!(
111 Construction,
112 "interior knot {value} has multiplicity {count}, above degree {}",
113 this.degree
114 );
115 }
116 }
117 Ok(this)
118 }
119
120 pub fn clamped_uniform(degree: usize, control_points: usize) -> OgeomResult<Self> {
130 if control_points < degree + 1 {
131 ogeom_bail!(
132 Construction,
133 "degree {degree} needs at least {} control points, got {control_points}",
134 degree + 1
135 );
136 }
137 let interior = control_points - degree - 1;
138 let mut knots = Vec::with_capacity(control_points + degree + 1);
139 knots.extend(core::iter::repeat_n(0.0, degree + 1));
140 for i in 1..=interior {
141 #[allow(clippy::cast_precision_loss)]
142 knots.push(i as f64 / (interior + 1) as f64);
143 }
144 knots.extend(core::iter::repeat_n(1.0, degree + 1));
145 Self::new(knots, degree)
146 }
147
148 pub fn averaged(degree: usize, parameters: &[f64]) -> OgeomResult<Self> {
159 if parameters.len() < degree + 1 {
160 ogeom_bail!(
161 Construction,
162 "degree {degree} needs at least {} parameters",
163 degree + 1
164 );
165 }
166 if parameters.windows(2).any(|w| w[1] <= w[0]) {
167 ogeom_bail!(Construction, "parameters must be strictly increasing");
168 }
169 let n = parameters.len();
170 let mut knots = Vec::with_capacity(n + degree + 1);
171 knots.extend(core::iter::repeat_n(parameters[0], degree + 1));
172 #[allow(clippy::cast_precision_loss)]
173 for j in 1..n - degree {
174 let mean: f64 = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
175 knots.push(mean);
176 }
177 knots.extend(core::iter::repeat_n(parameters[n - 1], degree + 1));
178 Self::new(knots, degree)
179 }
180
181 #[must_use]
183 pub const fn degree(&self) -> usize {
184 self.degree
185 }
186
187 #[must_use]
189 pub fn knots(&self) -> &[f64] {
190 &self.knots
191 }
192
193 #[must_use]
195 pub const fn control_point_count(&self) -> usize {
196 self.knots.len() - self.degree - 1
197 }
198
199 #[must_use]
201 pub fn domain_start(&self) -> f64 {
202 self.knots[self.degree]
203 }
204
205 #[must_use]
207 pub fn domain_end(&self) -> f64 {
208 self.knots[self.knots.len() - self.degree - 1]
209 }
210
211 #[must_use]
213 pub fn domain(&self) -> (f64, f64) {
214 (self.domain_start(), self.domain_end())
215 }
216
217 #[must_use]
220 pub fn is_clamped(&self) -> bool {
221 let last = self.knots.len() - 1;
222 self.knots[..=self.degree]
223 .iter()
224 .all(|k| *k == self.knots[0])
225 && self.knots[last - self.degree..]
226 .iter()
227 .all(|k| *k == self.knots[last])
228 }
229
230 #[must_use]
234 pub fn multiplicity_at(&self, index: usize) -> usize {
235 let Some(&value) = self.knots.get(index) else {
236 return 0;
237 };
238 self.knots.iter().filter(|k| **k == value).count()
239 }
240
241 #[must_use]
243 pub fn multiplicity_of(&self, value: f64) -> usize {
244 self.knots.iter().filter(|k| **k == value).count()
245 }
246
247 #[must_use]
249 pub fn distinct(&self) -> Vec<(f64, usize)> {
250 let mut out: Vec<(f64, usize)> = Vec::new();
251 for &k in &self.knots {
252 match out.last_mut() {
253 Some((value, count)) if *value == k => *count += 1,
254 _ => out.push((k, 1)),
255 }
256 }
257 out
258 }
259
260 #[must_use]
262 pub fn contains(&self, u: f64, tol: Tolerances) -> bool {
263 let (start, end) = self.domain();
264 u >= start - tol.parametric() && u <= end + tol.parametric()
265 }
266
267 pub fn span(&self, u: f64, tol: Tolerances) -> OgeomResult<usize> {
278 let (start, end) = self.domain();
279 if !u.is_finite() || u < start - tol.parametric() || u > end + tol.parametric() {
280 ogeom_bail!(Domain, "parameter {u} outside knot domain [{start}, {end}]");
281 }
282 Ok(self.span_unchecked(u))
283 }
284
285 #[must_use]
288 pub fn span_unchecked(&self, u: f64) -> usize {
289 let last = self.control_point_count() - 1;
290 if u >= self.knots[last + 1] {
295 let mut span = last;
296 while span > self.degree && self.knots[span] >= self.knots[span + 1] {
297 span -= 1;
298 }
299 return span;
300 }
301 if u <= self.knots[self.degree] {
302 let mut span = self.degree;
303 while span < last && self.knots[span] >= self.knots[span + 1] {
304 span += 1;
305 }
306 return span;
307 }
308 let mut low = self.degree;
309 let mut high = last + 1;
310 while high - low > 1 {
311 let mid = usize::midpoint(low, high);
312 if u < self.knots[mid] {
313 high = mid;
314 } else {
315 low = mid;
316 }
317 }
318 low
319 }
320
321 #[must_use]
331 pub fn basis(&self, span: usize, u: f64) -> BasisValues {
332 let p = self.degree;
333 let mut n = BasisValues::with_capacity(p + 1);
334 n.push(1.0);
335 let mut left = BasisValues::with_capacity(p + 1);
336 let mut right = BasisValues::with_capacity(p + 1);
337 left.push(0.0);
338 right.push(0.0);
339
340 for j in 1..=p {
341 left.push(u - self.knots[span + 1 - j]);
342 right.push(self.knots[span + j] - u);
343 let mut saved = 0.0;
344 n.push(0.0);
345 for r in 0..j {
346 let denominator = right[r + 1] + left[j - r];
350 let temp = n[r] / denominator;
351 n[r] = saved + right[r + 1] * temp;
352 saved = left[j - r] * temp;
353 }
354 n[j] = saved;
355 }
356 n
357 }
358
359 #[must_use]
365 pub fn basis_derivatives(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
366 if self.degree < SMALL_ORDER {
367 return self.basis_derivatives_small(span, u, n);
368 }
369 self.basis_derivatives_any(span, u, n)
370 }
371
372 fn basis_derivatives_small(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
378 const M: usize = SMALL_ORDER;
379 let p = self.degree;
380 let order = n.min(p);
381 let mut ndu = [[0.0_f64; M]; M];
382 ndu[0][0] = 1.0;
383 let mut left = [0.0_f64; M];
384 let mut right = [0.0_f64; M];
385 for j in 1..=p {
386 left[j] = u - self.knots[span + 1 - j];
387 right[j] = self.knots[span + j] - u;
388 let mut saved = 0.0;
389 for r in 0..j {
390 ndu[j][r] = right[r + 1] + left[j - r];
391 let temp = ndu[r][j - 1] / ndu[j][r];
392 ndu[r][j] = saved + right[r + 1] * temp;
393 saved = left[j - r] * temp;
394 }
395 ndu[j][j] = saved;
396 }
397
398 let mut rows = [[0.0_f64; M]; M];
399 for (j, slot) in rows[0].iter_mut().enumerate().take(p + 1) {
400 *slot = ndu[j][p];
401 }
402 let mut a = [[0.0_f64; M]; 2];
403 for r in 0..=p {
404 let (mut s1, mut s2) = (0_usize, 1_usize);
405 a[0][0] = 1.0;
406 for k in 1..=order {
407 let mut d = 0.0;
408 let rk = r as isize - k as isize;
409 let pk = p - k;
410 if r >= k {
411 a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
412 d = a[s2][0] * ndu[rk as usize][pk];
413 }
414 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
415 let j2 = if r as isize - 1 <= pk as isize {
416 k - 1
417 } else {
418 p - r
419 };
420 for j in j1..=j2 {
421 let index = (rk + j as isize) as usize;
422 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
423 d += a[s2][j] * ndu[index][pk];
424 }
425 if r <= pk {
426 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
427 d += a[s2][k] * ndu[r][pk];
428 }
429 rows[k][r] = d;
430 core::mem::swap(&mut s1, &mut s2);
431 }
432 }
433 let mut factor = p;
434 for (k, row) in rows.iter_mut().enumerate().take(order + 1).skip(1) {
435 #[allow(clippy::cast_precision_loss)]
436 let scale = factor as f64;
437 for value in row.iter_mut().take(p + 1) {
438 *value *= scale;
439 }
440 factor = factor.saturating_mul(p.saturating_sub(k));
441 }
442 (0..=n)
443 .map(|k| {
444 if k < M {
445 BasisValues::from_slice(&rows[k][..=p])
446 } else {
447 BasisValues::from_elem(0.0, p + 1)
448 }
449 })
450 .collect()
451 }
452
453 fn basis_derivatives_any(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
454 let p = self.degree;
455 let order = n.min(p);
456
457 let mut ndu: SmallVec<[BasisValues; 8]> =
463 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
464 .take(p + 1)
465 .collect();
466 ndu[0][0] = 1.0;
467 let mut left = BasisValues::from_elem(0.0, p + 1);
468 let mut right = BasisValues::from_elem(0.0, p + 1);
469
470 for j in 1..=p {
471 left[j] = u - self.knots[span + 1 - j];
472 right[j] = self.knots[span + j] - u;
473 let mut saved = 0.0;
474 for r in 0..j {
475 ndu[j][r] = right[r + 1] + left[j - r];
476 let temp = ndu[r][j - 1] / ndu[j][r];
477 ndu[r][j] = saved + right[r + 1] * temp;
478 saved = left[j - r] * temp;
479 }
480 ndu[j][j] = saved;
481 }
482
483 let mut derivatives: DerivativeRows =
484 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
485 .take(n + 1)
486 .collect();
487 for (j, slot) in derivatives[0].iter_mut().enumerate() {
488 *slot = ndu[j][p];
489 }
490 let mut a = [
495 BasisValues::from_elem(0.0, p + 1),
496 BasisValues::from_elem(0.0, p + 1),
497 ];
498 for r in 0..=p {
499 let (mut s1, mut s2) = (0_usize, 1_usize);
500 a[0][0] = 1.0;
501 for k in 1..=order {
502 let mut d = 0.0;
503 let rk = r as isize - k as isize;
504 let pk = p - k;
505 if r >= k {
506 a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
507 d = a[s2][0] * ndu[rk as usize][pk];
508 }
509 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
510 let j2 = if r as isize - 1 <= pk as isize {
511 k - 1
512 } else {
513 p - r
514 };
515 for j in j1..=j2 {
516 let index = (rk + j as isize) as usize;
517 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
518 d += a[s2][j] * ndu[index][pk];
519 }
520 if r <= pk {
521 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
522 d += a[s2][k] * ndu[r][pk];
523 }
524 derivatives[k][r] = d;
525 core::mem::swap(&mut s1, &mut s2);
526 }
527 }
528
529 let mut factor = p;
531 for (k, row) in derivatives.iter_mut().enumerate().take(order + 1).skip(1) {
532 #[allow(clippy::cast_precision_loss)]
533 let scale = factor as f64;
534 for value in row.iter_mut() {
535 *value *= scale;
536 }
537 factor = factor.saturating_mul(p.saturating_sub(k));
538 }
539 derivatives
540 }
541
542 pub fn with_knot_inserted(&self, value: f64, count: usize) -> OgeomResult<Self> {
552 let mut knots = self.knots.clone();
553 let position = knots.partition_point(|k| *k <= value);
554 for _ in 0..count {
555 knots.insert(position, value);
556 }
557 Self::new(knots, self.degree)
558 }
559
560 pub fn reparameterized(&self, start: f64, end: f64) -> OgeomResult<Self> {
567 if !start.is_finite() || !end.is_finite() || end <= start {
568 ogeom_bail!(Construction, "target range [{start}, {end}] is empty");
569 }
570 let (a, b) = self.domain();
571 let scale = (end - start) / (b - a);
572 let knots: Vec<f64> = self
578 .knots
579 .iter()
580 .map(|&k| {
581 let mapped = (k - a).mul_add(scale, start);
582 if k == a {
583 start
584 } else if k == b {
585 end
586 } else if k < a {
587 mapped.min(start)
588 } else if k > b {
589 mapped.max(end)
590 } else {
591 mapped.clamp(start, end)
592 }
593 })
594 .collect();
595 Self::new(knots, self.degree)
596 }
597
598 #[must_use]
610 pub fn reversed(&self) -> Self {
611 let (a, b) = self.domain();
612 let sum = a + b;
613 let knots: Vec<f64> = self
616 .knots
617 .iter()
618 .rev()
619 .map(|&k| {
620 if k == a {
621 b
622 } else if k == b {
623 a
624 } else if k < a {
625 (sum - k).max(b)
626 } else if k > b {
627 (sum - k).min(a)
628 } else {
629 (sum - k).clamp(a, b)
630 }
631 })
632 .collect();
633 Self {
634 knots,
635 degree: self.degree,
636 }
637 }
638}
639
640#[cfg(test)]
641#[allow(clippy::unwrap_used)]
642mod tests {
643 use super::*;
644 use approx::assert_relative_eq;
645
646 const T: Tolerances = Tolerances::millimetres();
647
648 fn cubic() -> KnotVector {
649 KnotVector::new(
651 vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
652 3,
653 )
654 .unwrap()
655 }
656
657 #[test]
658 fn malformed_vectors_are_refused() {
659 assert!(KnotVector::new(vec![0.0, 1.0], 3).is_err(), "too short");
660 assert!(
661 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
662 "a start repeated degree + 2 times"
663 );
664 assert!(
665 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
666 "an end repeated degree + 2 times"
667 );
668 assert!(
669 KnotVector::new(vec![0.0, 0.0, 1.0, 0.5, 1.0, 1.0], 2).is_err(),
670 "not non-decreasing"
671 );
672 assert!(
673 KnotVector::new(vec![0.0, 0.0, f64::NAN, 1.0, 1.0, 1.0], 2).is_err(),
674 "non-finite"
675 );
676 assert!(KnotVector::new(vec![0.0; 8], 3).is_err(), "empty domain");
677 assert!(KnotVector::new(vec![], 0).is_err(), "degree zero");
678 assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_err());
680 assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_ok());
682 }
683
684 #[test]
685 fn clamped_uniform_has_the_expected_shape() {
686 let k = KnotVector::clamped_uniform(3, 7).unwrap();
687 assert_eq!(k.knots().len(), 11);
688 assert_eq!(k.control_point_count(), 7);
689 assert_eq!(k.domain(), (0.0, 1.0));
690 assert!(k.is_clamped());
691 assert_eq!(k.multiplicity_of(0.0), 4);
692 assert_eq!(k.multiplicity_of(1.0), 4);
693 assert_relative_eq!(k.knots()[4], 1.0 / 4.0);
694
695 let b = KnotVector::clamped_uniform(3, 4).unwrap();
697 assert_eq!(b.knots(), &[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
698 assert!(KnotVector::clamped_uniform(3, 3).is_err());
699 }
700
701 #[test]
702 fn span_lookup_handles_both_ends_of_the_domain() {
703 let k = cubic();
704 assert_eq!(k.span(0.0, T).unwrap(), 3, "domain start");
705 assert_eq!(k.span(0.1, T).unwrap(), 3);
706 assert_eq!(k.span(0.25, T).unwrap(), 4, "on a knot, span to its right");
707 assert_eq!(k.span(0.3, T).unwrap(), 4);
708 assert_eq!(k.span(0.9, T).unwrap(), 6);
709 assert_eq!(k.span(1.0, T).unwrap(), 6);
711 assert!(k.span(-0.1, T).is_err());
712 assert!(k.span(1.1, T).is_err());
713 assert!(k.span(f64::NAN, T).is_err());
714 }
715
716 #[test]
717 fn basis_functions_form_a_partition_of_unity() {
718 let k = cubic();
719 for i in 0..=200 {
720 let u = f64::from(i) / 200.0;
721 let span = k.span(u, T).unwrap();
722 let n = k.basis(span, u);
723 assert_eq!(n.len(), 4);
724 let sum: f64 = n.iter().sum();
725 assert_relative_eq!(sum, 1.0, epsilon = 1e-14);
726 assert!(n.iter().all(|v| *v >= -1e-15), "basis must be non-negative");
727 }
728 }
729
730 #[test]
731 fn basis_matches_the_bernstein_polynomials_for_a_bezier() {
732 let k = KnotVector::clamped_uniform(3, 4).unwrap();
734 for i in 0..=20 {
735 let u = f64::from(i) / 20.0;
736 let span = k.span(u, T).unwrap();
737 let n = k.basis(span, u);
738 let v = 1.0 - u;
739 let expected = [v * v * v, 3.0 * u * v * v, 3.0 * u * u * v, u * u * u];
740 for (got, want) in n.iter().zip(expected) {
741 assert_relative_eq!(*got, want, epsilon = 1e-14);
742 }
743 }
744 }
745
746 #[test]
747 fn basis_is_an_interpolant_at_a_clamped_end() {
748 let k = cubic();
749 let n = k.basis(k.span(0.0, T).unwrap(), 0.0);
750 assert_relative_eq!(n[0], 1.0, epsilon = 1e-15);
751 assert!(n[1..].iter().all(|v| v.abs() < 1e-15));
752
753 let n = k.basis(k.span(1.0, T).unwrap(), 1.0);
754 assert_relative_eq!(n[3], 1.0, epsilon = 1e-15);
755 assert!(n[..3].iter().all(|v| v.abs() < 1e-15));
756 }
757
758 #[test]
759 fn basis_derivatives_agree_with_finite_differences() {
760 let k = cubic();
761 let h = 1e-6;
762 for i in 1..20 {
763 let u = f64::from(i) / 20.0;
764 let span = k.span(u, T).unwrap();
765 let d = k.basis_derivatives(span, u, 2);
766
767 let plain = k.basis(span, u);
769 for (a, b) in d[0].iter().zip(plain.iter()) {
770 assert_relative_eq!(a, b, epsilon = 1e-14);
771 }
772
773 let ahead = k.basis(span, u + h);
776 let behind = k.basis(span, u - h);
777 for j in 0..=k.degree() {
778 let numeric = (ahead[j] - behind[j]) / (2.0 * h);
779 assert_relative_eq!(d[1][j], numeric, epsilon = 1e-5);
780 }
781 }
782 }
783
784 #[test]
785 fn basis_derivatives_sum_to_zero() {
786 let k = cubic();
789 for i in 0..=50 {
790 let u = f64::from(i) / 50.0;
791 let span = k.span(u, T).unwrap();
792 let d = k.basis_derivatives(span, u, 3);
793 assert_relative_eq!(d[0].iter().sum::<f64>(), 1.0, epsilon = 1e-13);
794 for (order, row) in d.iter().enumerate().skip(1) {
795 let sum: f64 = row.iter().sum();
796 assert!(sum.abs() < 1e-8, "order {order} sums to {sum}");
797 }
798 }
799 }
800
801 #[test]
802 fn derivatives_above_the_degree_are_zero() {
803 let k = cubic();
804 let span = k.span(0.4, T).unwrap();
805 let d = k.basis_derivatives(span, 0.4, 5);
806 assert_eq!(d.len(), 6);
807 for (order, row) in d.iter().enumerate().skip(k.degree() + 1) {
808 assert!(row.iter().all(|v| *v == 0.0), "order {order} is not zero");
809 }
810 }
811
812 #[test]
813 fn multiplicity_and_distinct_knots() {
814 let k = cubic();
815 assert_eq!(k.multiplicity_of(0.0), 4);
816 assert_eq!(k.multiplicity_of(0.5), 1);
817 assert_eq!(k.multiplicity_of(0.6), 0);
818 assert_eq!(k.multiplicity_at(0), 4);
819 assert_eq!(
820 k.distinct(),
821 vec![(0.0, 4), (0.25, 1), (0.5, 1), (0.75, 1), (1.0, 4)]
822 );
823 }
824
825 #[test]
826 fn knot_insertion_raises_multiplicity() {
827 let k = cubic();
828 let inserted = k.with_knot_inserted(0.5, 2).unwrap();
829 assert_eq!(inserted.multiplicity_of(0.5), 3);
830 assert_eq!(inserted.knots().len(), k.knots().len() + 2);
831 assert_eq!(inserted.domain(), k.domain());
832 assert!(k.with_knot_inserted(0.5, 3).is_err());
834 }
835
836 #[test]
837 fn an_unclamped_vector_reverses_and_rescales_without_changing_its_basis() {
838 let kv = KnotVector::new((0..8).map(f64::from).collect(), 3).unwrap();
840 let at = |kv: &KnotVector, u: f64| kv.basis(kv.span(u, T).unwrap(), u);
841 let rescaled = kv.reparameterized(0.0, 1.0).unwrap();
842 let reversed = kv.reversed();
843 for (u, s) in [(3.1, 0.1), (3.5, 0.5), (3.9, 0.9)] {
844 let original = at(&kv, u);
845 for (x, y) in original.iter().zip(at(&rescaled, s)) {
846 assert_relative_eq!(*x, y, epsilon = 1e-12);
847 }
848 for (x, y) in original.iter().zip(at(&reversed, 7.0 - u).iter().rev()) {
849 assert_relative_eq!(*x, *y, epsilon = 1e-12);
850 }
851 }
852 }
853
854 #[test]
855 fn reparameterization_preserves_clamping_exactly() {
856 let k = cubic();
857 let r = k.reparameterized(-2.0, 6.0).unwrap();
858 assert_eq!(r.domain(), (-2.0, 6.0));
859 assert!(r.is_clamped(), "the repeated end knots must stay identical");
860 assert_eq!(r.multiplicity_of(-2.0), 4);
861 assert_eq!(r.multiplicity_of(6.0), 4);
862 assert_relative_eq!(r.knots()[4], 0.0, epsilon = 1e-12);
864 assert!(k.reparameterized(1.0, 1.0).is_err());
865 assert!(k.reparameterized(1.0, f64::NAN).is_err());
866 }
867
868 #[test]
869 fn reversal_mirrors_the_spacing_and_keeps_the_domain() {
870 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 1.0], 2).unwrap();
872 let r = k.reversed();
873 assert_eq!(r.domain(), k.domain());
874 assert!(r.is_clamped());
875 assert_relative_eq!(r.knots()[3], 0.2, epsilon = 1e-15);
876 assert_relative_eq!(r.knots()[4], 0.9, epsilon = 1e-15);
877 for (got, want) in r.reversed().knots().iter().zip(k.knots()) {
880 assert_relative_eq!(got, want, epsilon = 1e-15);
881 }
882 let multiplicities: Vec<usize> = r.distinct().iter().map(|(_, m)| *m).collect();
886 let original: Vec<usize> = k.distinct().iter().map(|(_, m)| *m).collect();
887 assert_eq!(multiplicities, original);
888 }
889
890 #[test]
891 fn averaged_knots_follow_the_parameters() {
892 let params = [0.0, 0.1, 0.4, 0.9, 1.0];
893 let k = KnotVector::averaged(3, ¶ms).unwrap();
894 assert_eq!(k.control_point_count(), 5);
895 assert_eq!(k.domain(), (0.0, 1.0));
896 assert!(k.is_clamped());
897 assert_relative_eq!(k.knots()[4], (0.1 + 0.4 + 0.9) / 3.0, epsilon = 1e-15);
899 assert!(KnotVector::averaged(3, &[0.0, 1.0]).is_err());
900 assert!(
901 KnotVector::averaged(2, &[0.0, 0.5, 0.5, 1.0]).is_err(),
902 "not increasing"
903 );
904 }
905
906 #[test]
907 fn basis_at_a_repeated_interior_knot_is_still_a_partition_of_unity() {
908 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap();
911 for u in [0.0_f64, 0.25, 0.499_999, 0.5, 0.500_001, 0.75, 1.0] {
912 let span = k.span(u, T).unwrap();
913 let n = k.basis(span, u);
914 assert_relative_eq!(n.iter().sum::<f64>(), 1.0, epsilon = 1e-14);
915 assert!(n.iter().all(|v| v.is_finite()), "non-finite basis at {u}");
916 }
917 }
918
919 #[test]
922 fn the_small_basis_path_is_the_general_one_to_the_bit() {
923 for degree in 1..SMALL_ORDER {
924 let count = degree + 5;
925 let knots = KnotVector::clamped_uniform(degree, count).unwrap();
926 let (lo, hi) = knots.domain();
927 for step in 0..=40 {
928 let u = lo + (hi - lo) * f64::from(step) / 40.0;
929 let span = knots.span_unchecked(u);
930 for n in 0..=degree + 1 {
931 let fast = knots.basis_derivatives_small(span, u, n);
932 let slow = knots.basis_derivatives_any(span, u, n);
933 assert_eq!(fast.len(), slow.len());
934 for (a, b) in fast.iter().zip(&slow) {
935 let bits =
936 |r: &BasisValues| r.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
937 assert_eq!(bits(a), bits(b), "degree {degree} u {u} n {n}");
938 }
939 }
940 }
941 }
942 }
943}