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] {
293 return last;
294 }
295 if u <= self.knots[self.degree] {
296 return self.degree;
297 }
298 let mut low = self.degree;
299 let mut high = last + 1;
300 while high - low > 1 {
301 let mid = usize::midpoint(low, high);
302 if u < self.knots[mid] {
303 high = mid;
304 } else {
305 low = mid;
306 }
307 }
308 low
309 }
310
311 #[must_use]
321 pub fn basis(&self, span: usize, u: f64) -> BasisValues {
322 let p = self.degree;
323 let mut n = BasisValues::with_capacity(p + 1);
324 n.push(1.0);
325 let mut left = BasisValues::with_capacity(p + 1);
326 let mut right = BasisValues::with_capacity(p + 1);
327 left.push(0.0);
328 right.push(0.0);
329
330 for j in 1..=p {
331 left.push(u - self.knots[span + 1 - j]);
332 right.push(self.knots[span + j] - u);
333 let mut saved = 0.0;
334 n.push(0.0);
335 for r in 0..j {
336 let denominator = right[r + 1] + left[j - r];
340 let temp = n[r] / denominator;
341 n[r] = saved + right[r + 1] * temp;
342 saved = left[j - r] * temp;
343 }
344 n[j] = saved;
345 }
346 n
347 }
348
349 #[must_use]
355 pub fn basis_derivatives(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
356 if self.degree < SMALL_ORDER {
357 return self.basis_derivatives_small(span, u, n);
358 }
359 self.basis_derivatives_any(span, u, n)
360 }
361
362 fn basis_derivatives_small(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
368 const M: usize = SMALL_ORDER;
369 let p = self.degree;
370 let order = n.min(p);
371 let mut ndu = [[0.0_f64; M]; M];
372 ndu[0][0] = 1.0;
373 let mut left = [0.0_f64; M];
374 let mut right = [0.0_f64; M];
375 for j in 1..=p {
376 left[j] = u - self.knots[span + 1 - j];
377 right[j] = self.knots[span + j] - u;
378 let mut saved = 0.0;
379 for r in 0..j {
380 ndu[j][r] = right[r + 1] + left[j - r];
381 let temp = ndu[r][j - 1] / ndu[j][r];
382 ndu[r][j] = saved + right[r + 1] * temp;
383 saved = left[j - r] * temp;
384 }
385 ndu[j][j] = saved;
386 }
387
388 let mut rows = [[0.0_f64; M]; M];
389 for (j, slot) in rows[0].iter_mut().enumerate().take(p + 1) {
390 *slot = ndu[j][p];
391 }
392 let mut a = [[0.0_f64; M]; 2];
393 for r in 0..=p {
394 let (mut s1, mut s2) = (0_usize, 1_usize);
395 a[0][0] = 1.0;
396 for k in 1..=order {
397 let mut d = 0.0;
398 let rk = r as isize - k as isize;
399 let pk = p - k;
400 if r >= k {
401 a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
402 d = a[s2][0] * ndu[rk as usize][pk];
403 }
404 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
405 let j2 = if r as isize - 1 <= pk as isize {
406 k - 1
407 } else {
408 p - r
409 };
410 for j in j1..=j2 {
411 let index = (rk + j as isize) as usize;
412 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
413 d += a[s2][j] * ndu[index][pk];
414 }
415 if r <= pk {
416 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
417 d += a[s2][k] * ndu[r][pk];
418 }
419 rows[k][r] = d;
420 core::mem::swap(&mut s1, &mut s2);
421 }
422 }
423 let mut factor = p;
424 for (k, row) in rows.iter_mut().enumerate().take(order + 1).skip(1) {
425 #[allow(clippy::cast_precision_loss)]
426 let scale = factor as f64;
427 for value in row.iter_mut().take(p + 1) {
428 *value *= scale;
429 }
430 factor = factor.saturating_mul(p.saturating_sub(k));
431 }
432 (0..=n)
433 .map(|k| {
434 if k < M {
435 BasisValues::from_slice(&rows[k][..=p])
436 } else {
437 BasisValues::from_elem(0.0, p + 1)
438 }
439 })
440 .collect()
441 }
442
443 fn basis_derivatives_any(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
444 let p = self.degree;
445 let order = n.min(p);
446
447 let mut ndu: SmallVec<[BasisValues; 8]> =
453 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
454 .take(p + 1)
455 .collect();
456 ndu[0][0] = 1.0;
457 let mut left = BasisValues::from_elem(0.0, p + 1);
458 let mut right = BasisValues::from_elem(0.0, p + 1);
459
460 for j in 1..=p {
461 left[j] = u - self.knots[span + 1 - j];
462 right[j] = self.knots[span + j] - u;
463 let mut saved = 0.0;
464 for r in 0..j {
465 ndu[j][r] = right[r + 1] + left[j - r];
466 let temp = ndu[r][j - 1] / ndu[j][r];
467 ndu[r][j] = saved + right[r + 1] * temp;
468 saved = left[j - r] * temp;
469 }
470 ndu[j][j] = saved;
471 }
472
473 let mut derivatives: DerivativeRows =
474 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
475 .take(n + 1)
476 .collect();
477 for (j, slot) in derivatives[0].iter_mut().enumerate() {
478 *slot = ndu[j][p];
479 }
480 let mut a = [
485 BasisValues::from_elem(0.0, p + 1),
486 BasisValues::from_elem(0.0, p + 1),
487 ];
488 for r in 0..=p {
489 let (mut s1, mut s2) = (0_usize, 1_usize);
490 a[0][0] = 1.0;
491 for k in 1..=order {
492 let mut d = 0.0;
493 let rk = r as isize - k as isize;
494 let pk = p - k;
495 if r >= k {
496 a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
497 d = a[s2][0] * ndu[rk as usize][pk];
498 }
499 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
500 let j2 = if r as isize - 1 <= pk as isize {
501 k - 1
502 } else {
503 p - r
504 };
505 for j in j1..=j2 {
506 let index = (rk + j as isize) as usize;
507 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
508 d += a[s2][j] * ndu[index][pk];
509 }
510 if r <= pk {
511 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
512 d += a[s2][k] * ndu[r][pk];
513 }
514 derivatives[k][r] = d;
515 core::mem::swap(&mut s1, &mut s2);
516 }
517 }
518
519 let mut factor = p;
521 for (k, row) in derivatives.iter_mut().enumerate().take(order + 1).skip(1) {
522 #[allow(clippy::cast_precision_loss)]
523 let scale = factor as f64;
524 for value in row.iter_mut() {
525 *value *= scale;
526 }
527 factor = factor.saturating_mul(p.saturating_sub(k));
528 }
529 derivatives
530 }
531
532 pub fn with_knot_inserted(&self, value: f64, count: usize) -> OgeomResult<Self> {
542 let mut knots = self.knots.clone();
543 let position = knots.partition_point(|k| *k <= value);
544 for _ in 0..count {
545 knots.insert(position, value);
546 }
547 Self::new(knots, self.degree)
548 }
549
550 pub fn reparameterized(&self, start: f64, end: f64) -> OgeomResult<Self> {
557 if !start.is_finite() || !end.is_finite() || end <= start {
558 ogeom_bail!(Construction, "target range [{start}, {end}] is empty");
559 }
560 let (a, b) = self.domain();
561 let scale = (end - start) / (b - a);
562 let knots: Vec<f64> = self
568 .knots
569 .iter()
570 .map(|&k| {
571 let mapped = (k - a).mul_add(scale, start);
572 if k == a {
573 start
574 } else if k == b {
575 end
576 } else if k < a {
577 mapped.min(start)
578 } else if k > b {
579 mapped.max(end)
580 } else {
581 mapped.clamp(start, end)
582 }
583 })
584 .collect();
585 Self::new(knots, self.degree)
586 }
587
588 #[must_use]
600 pub fn reversed(&self) -> Self {
601 let (a, b) = self.domain();
602 let sum = a + b;
603 let knots: Vec<f64> = self
606 .knots
607 .iter()
608 .rev()
609 .map(|&k| {
610 if k == a {
611 b
612 } else if k == b {
613 a
614 } else if k < a {
615 (sum - k).max(b)
616 } else if k > b {
617 (sum - k).min(a)
618 } else {
619 (sum - k).clamp(a, b)
620 }
621 })
622 .collect();
623 Self {
624 knots,
625 degree: self.degree,
626 }
627 }
628}
629
630#[cfg(test)]
631#[allow(clippy::unwrap_used)]
632mod tests {
633 use super::*;
634 use approx::assert_relative_eq;
635
636 const T: Tolerances = Tolerances::millimetres();
637
638 fn cubic() -> KnotVector {
639 KnotVector::new(
641 vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
642 3,
643 )
644 .unwrap()
645 }
646
647 #[test]
648 fn malformed_vectors_are_refused() {
649 assert!(KnotVector::new(vec![0.0, 1.0], 3).is_err(), "too short");
650 assert!(
651 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
652 "a start repeated degree + 2 times"
653 );
654 assert!(
655 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
656 "an end repeated degree + 2 times"
657 );
658 assert!(
659 KnotVector::new(vec![0.0, 0.0, 1.0, 0.5, 1.0, 1.0], 2).is_err(),
660 "not non-decreasing"
661 );
662 assert!(
663 KnotVector::new(vec![0.0, 0.0, f64::NAN, 1.0, 1.0, 1.0], 2).is_err(),
664 "non-finite"
665 );
666 assert!(KnotVector::new(vec![0.0; 8], 3).is_err(), "empty domain");
667 assert!(KnotVector::new(vec![], 0).is_err(), "degree zero");
668 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());
670 assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_ok());
672 }
673
674 #[test]
675 fn clamped_uniform_has_the_expected_shape() {
676 let k = KnotVector::clamped_uniform(3, 7).unwrap();
677 assert_eq!(k.knots().len(), 11);
678 assert_eq!(k.control_point_count(), 7);
679 assert_eq!(k.domain(), (0.0, 1.0));
680 assert!(k.is_clamped());
681 assert_eq!(k.multiplicity_of(0.0), 4);
682 assert_eq!(k.multiplicity_of(1.0), 4);
683 assert_relative_eq!(k.knots()[4], 1.0 / 4.0);
684
685 let b = KnotVector::clamped_uniform(3, 4).unwrap();
687 assert_eq!(b.knots(), &[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
688 assert!(KnotVector::clamped_uniform(3, 3).is_err());
689 }
690
691 #[test]
692 fn span_lookup_handles_both_ends_of_the_domain() {
693 let k = cubic();
694 assert_eq!(k.span(0.0, T).unwrap(), 3, "domain start");
695 assert_eq!(k.span(0.1, T).unwrap(), 3);
696 assert_eq!(k.span(0.25, T).unwrap(), 4, "on a knot, span to its right");
697 assert_eq!(k.span(0.3, T).unwrap(), 4);
698 assert_eq!(k.span(0.9, T).unwrap(), 6);
699 assert_eq!(k.span(1.0, T).unwrap(), 6);
701 assert!(k.span(-0.1, T).is_err());
702 assert!(k.span(1.1, T).is_err());
703 assert!(k.span(f64::NAN, T).is_err());
704 }
705
706 #[test]
707 fn basis_functions_form_a_partition_of_unity() {
708 let k = cubic();
709 for i in 0..=200 {
710 let u = f64::from(i) / 200.0;
711 let span = k.span(u, T).unwrap();
712 let n = k.basis(span, u);
713 assert_eq!(n.len(), 4);
714 let sum: f64 = n.iter().sum();
715 assert_relative_eq!(sum, 1.0, epsilon = 1e-14);
716 assert!(n.iter().all(|v| *v >= -1e-15), "basis must be non-negative");
717 }
718 }
719
720 #[test]
721 fn basis_matches_the_bernstein_polynomials_for_a_bezier() {
722 let k = KnotVector::clamped_uniform(3, 4).unwrap();
724 for i in 0..=20 {
725 let u = f64::from(i) / 20.0;
726 let span = k.span(u, T).unwrap();
727 let n = k.basis(span, u);
728 let v = 1.0 - u;
729 let expected = [v * v * v, 3.0 * u * v * v, 3.0 * u * u * v, u * u * u];
730 for (got, want) in n.iter().zip(expected) {
731 assert_relative_eq!(*got, want, epsilon = 1e-14);
732 }
733 }
734 }
735
736 #[test]
737 fn basis_is_an_interpolant_at_a_clamped_end() {
738 let k = cubic();
739 let n = k.basis(k.span(0.0, T).unwrap(), 0.0);
740 assert_relative_eq!(n[0], 1.0, epsilon = 1e-15);
741 assert!(n[1..].iter().all(|v| v.abs() < 1e-15));
742
743 let n = k.basis(k.span(1.0, T).unwrap(), 1.0);
744 assert_relative_eq!(n[3], 1.0, epsilon = 1e-15);
745 assert!(n[..3].iter().all(|v| v.abs() < 1e-15));
746 }
747
748 #[test]
749 fn basis_derivatives_agree_with_finite_differences() {
750 let k = cubic();
751 let h = 1e-6;
752 for i in 1..20 {
753 let u = f64::from(i) / 20.0;
754 let span = k.span(u, T).unwrap();
755 let d = k.basis_derivatives(span, u, 2);
756
757 let plain = k.basis(span, u);
759 for (a, b) in d[0].iter().zip(plain.iter()) {
760 assert_relative_eq!(a, b, epsilon = 1e-14);
761 }
762
763 let ahead = k.basis(span, u + h);
766 let behind = k.basis(span, u - h);
767 for j in 0..=k.degree() {
768 let numeric = (ahead[j] - behind[j]) / (2.0 * h);
769 assert_relative_eq!(d[1][j], numeric, epsilon = 1e-5);
770 }
771 }
772 }
773
774 #[test]
775 fn basis_derivatives_sum_to_zero() {
776 let k = cubic();
779 for i in 0..=50 {
780 let u = f64::from(i) / 50.0;
781 let span = k.span(u, T).unwrap();
782 let d = k.basis_derivatives(span, u, 3);
783 assert_relative_eq!(d[0].iter().sum::<f64>(), 1.0, epsilon = 1e-13);
784 for (order, row) in d.iter().enumerate().skip(1) {
785 let sum: f64 = row.iter().sum();
786 assert!(sum.abs() < 1e-8, "order {order} sums to {sum}");
787 }
788 }
789 }
790
791 #[test]
792 fn derivatives_above_the_degree_are_zero() {
793 let k = cubic();
794 let span = k.span(0.4, T).unwrap();
795 let d = k.basis_derivatives(span, 0.4, 5);
796 assert_eq!(d.len(), 6);
797 for (order, row) in d.iter().enumerate().skip(k.degree() + 1) {
798 assert!(row.iter().all(|v| *v == 0.0), "order {order} is not zero");
799 }
800 }
801
802 #[test]
803 fn multiplicity_and_distinct_knots() {
804 let k = cubic();
805 assert_eq!(k.multiplicity_of(0.0), 4);
806 assert_eq!(k.multiplicity_of(0.5), 1);
807 assert_eq!(k.multiplicity_of(0.6), 0);
808 assert_eq!(k.multiplicity_at(0), 4);
809 assert_eq!(
810 k.distinct(),
811 vec![(0.0, 4), (0.25, 1), (0.5, 1), (0.75, 1), (1.0, 4)]
812 );
813 }
814
815 #[test]
816 fn knot_insertion_raises_multiplicity() {
817 let k = cubic();
818 let inserted = k.with_knot_inserted(0.5, 2).unwrap();
819 assert_eq!(inserted.multiplicity_of(0.5), 3);
820 assert_eq!(inserted.knots().len(), k.knots().len() + 2);
821 assert_eq!(inserted.domain(), k.domain());
822 assert!(k.with_knot_inserted(0.5, 3).is_err());
824 }
825
826 #[test]
827 fn an_unclamped_vector_reverses_and_rescales_without_changing_its_basis() {
828 let kv = KnotVector::new((0..8).map(f64::from).collect(), 3).unwrap();
830 let at = |kv: &KnotVector, u: f64| kv.basis(kv.span(u, T).unwrap(), u);
831 let rescaled = kv.reparameterized(0.0, 1.0).unwrap();
832 let reversed = kv.reversed();
833 for (u, s) in [(3.1, 0.1), (3.5, 0.5), (3.9, 0.9)] {
834 let original = at(&kv, u);
835 for (x, y) in original.iter().zip(at(&rescaled, s)) {
836 assert_relative_eq!(*x, y, epsilon = 1e-12);
837 }
838 for (x, y) in original.iter().zip(at(&reversed, 7.0 - u).iter().rev()) {
839 assert_relative_eq!(*x, *y, epsilon = 1e-12);
840 }
841 }
842 }
843
844 #[test]
845 fn reparameterization_preserves_clamping_exactly() {
846 let k = cubic();
847 let r = k.reparameterized(-2.0, 6.0).unwrap();
848 assert_eq!(r.domain(), (-2.0, 6.0));
849 assert!(r.is_clamped(), "the repeated end knots must stay identical");
850 assert_eq!(r.multiplicity_of(-2.0), 4);
851 assert_eq!(r.multiplicity_of(6.0), 4);
852 assert_relative_eq!(r.knots()[4], 0.0, epsilon = 1e-12);
854 assert!(k.reparameterized(1.0, 1.0).is_err());
855 assert!(k.reparameterized(1.0, f64::NAN).is_err());
856 }
857
858 #[test]
859 fn reversal_mirrors_the_spacing_and_keeps_the_domain() {
860 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 1.0], 2).unwrap();
862 let r = k.reversed();
863 assert_eq!(r.domain(), k.domain());
864 assert!(r.is_clamped());
865 assert_relative_eq!(r.knots()[3], 0.2, epsilon = 1e-15);
866 assert_relative_eq!(r.knots()[4], 0.9, epsilon = 1e-15);
867 for (got, want) in r.reversed().knots().iter().zip(k.knots()) {
870 assert_relative_eq!(got, want, epsilon = 1e-15);
871 }
872 let multiplicities: Vec<usize> = r.distinct().iter().map(|(_, m)| *m).collect();
876 let original: Vec<usize> = k.distinct().iter().map(|(_, m)| *m).collect();
877 assert_eq!(multiplicities, original);
878 }
879
880 #[test]
881 fn averaged_knots_follow_the_parameters() {
882 let params = [0.0, 0.1, 0.4, 0.9, 1.0];
883 let k = KnotVector::averaged(3, ¶ms).unwrap();
884 assert_eq!(k.control_point_count(), 5);
885 assert_eq!(k.domain(), (0.0, 1.0));
886 assert!(k.is_clamped());
887 assert_relative_eq!(k.knots()[4], (0.1 + 0.4 + 0.9) / 3.0, epsilon = 1e-15);
889 assert!(KnotVector::averaged(3, &[0.0, 1.0]).is_err());
890 assert!(
891 KnotVector::averaged(2, &[0.0, 0.5, 0.5, 1.0]).is_err(),
892 "not increasing"
893 );
894 }
895
896 #[test]
897 fn basis_at_a_repeated_interior_knot_is_still_a_partition_of_unity() {
898 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap();
901 for u in [0.0_f64, 0.25, 0.499_999, 0.5, 0.500_001, 0.75, 1.0] {
902 let span = k.span(u, T).unwrap();
903 let n = k.basis(span, u);
904 assert_relative_eq!(n.iter().sum::<f64>(), 1.0, epsilon = 1e-14);
905 assert!(n.iter().all(|v| v.is_finite()), "non-finite basis at {u}");
906 }
907 }
908
909 #[test]
912 fn the_small_basis_path_is_the_general_one_to_the_bit() {
913 for degree in 1..SMALL_ORDER {
914 let count = degree + 5;
915 let knots = KnotVector::clamped_uniform(degree, count).unwrap();
916 let (lo, hi) = knots.domain();
917 for step in 0..=40 {
918 let u = lo + (hi - lo) * f64::from(step) / 40.0;
919 let span = knots.span_unchecked(u);
920 for n in 0..=degree + 1 {
921 let fast = knots.basis_derivatives_small(span, u, n);
922 let slow = knots.basis_derivatives_any(span, u, n);
923 assert_eq!(fast.len(), slow.len());
924 for (a, b) in fast.iter().zip(&slow) {
925 let bits =
926 |r: &BasisValues| r.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
927 assert_eq!(bits(a), bits(b), "degree {degree} u {u} n {n}");
928 }
929 }
930 }
931 }
932 }
933}