1use smallvec::SmallVec;
17
18pub type DerivativeGrid<P> = SmallVec<[SmallVec<[P; 4]>; 4]>;
22use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
23
24use crate::{KnotVector, Point, Point2, Vector, Vector2};
25
26pub trait Blend: Copy {
33 fn zero() -> Self;
35 fn scale(self, k: f64) -> Self;
37 fn add(self, other: Self) -> Self;
39
40 #[must_use]
42 fn lerp(self, other: Self, t: f64) -> Self {
43 self.scale(1.0 - t).add(other.scale(t))
44 }
45
46 #[must_use]
48 fn sub(self, other: Self) -> Self {
49 self.add(other.scale(-1.0))
50 }
51}
52
53impl Blend for f64 {
54 fn zero() -> Self {
55 0.0
56 }
57 fn scale(self, k: f64) -> Self {
58 self * k
59 }
60 fn add(self, other: Self) -> Self {
61 self + other
62 }
63}
64
65impl Blend for Vector {
66 fn zero() -> Self {
67 Self::ZERO
68 }
69 fn scale(self, k: f64) -> Self {
70 self * k
71 }
72 fn add(self, other: Self) -> Self {
73 self + other
74 }
75}
76
77impl Blend for Vector2 {
78 fn zero() -> Self {
79 Self::ZERO
80 }
81 fn scale(self, k: f64) -> Self {
82 self * k
83 }
84 fn add(self, other: Self) -> Self {
85 self + other
86 }
87}
88
89impl Blend for Point {
90 fn zero() -> Self {
91 Self::ORIGIN
92 }
93 fn scale(self, k: f64) -> Self {
94 Self::from_vector(self.to_vector() * k)
95 }
96 fn add(self, other: Self) -> Self {
97 Self::from_vector(self.to_vector() + other.to_vector())
98 }
99}
100
101impl Blend for Point2 {
102 fn zero() -> Self {
103 Self::ORIGIN
104 }
105 fn scale(self, k: f64) -> Self {
106 Self::from_vector(self.to_vector() * k)
107 }
108 fn add(self, other: Self) -> Self {
109 Self::from_vector(self.to_vector() + other.to_vector())
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
119pub struct Weighted<P> {
120 pub scaled: P,
122 pub weight: f64,
124}
125
126impl<P: Blend> Weighted<P> {
127 pub fn new(point: P, weight: f64, tol: Tolerances) -> OgeomResult<Self> {
136 if !weight.is_finite() || weight <= tol.confusion() {
137 ogeom_bail!(
138 Construction,
139 "control point weight {weight} must be finite and positive"
140 );
141 }
142 Ok(Self {
143 scaled: point.scale(weight),
144 weight,
145 })
146 }
147
148 #[must_use]
150 pub fn point(self) -> P {
151 self.scaled.scale(1.0 / self.weight)
152 }
153}
154
155impl<P: Blend> Blend for Weighted<P> {
156 fn zero() -> Self {
157 Self {
158 scaled: P::zero(),
159 weight: 0.0,
160 }
161 }
162 fn scale(self, k: f64) -> Self {
163 Self {
164 scaled: self.scaled.scale(k),
165 weight: self.weight * k,
166 }
167 }
168 fn add(self, other: Self) -> Self {
169 Self {
170 scaled: self.scaled.add(other.scaled),
171 weight: self.weight + other.weight,
172 }
173 }
174}
175
176fn check_shape<P>(knots: &KnotVector, control: &[P]) -> OgeomResult<()> {
178 if control.len() != knots.control_point_count() {
179 ogeom_bail!(
180 Dimension,
181 "knot vector describes {} control points, got {}",
182 knots.control_point_count(),
183 control.len()
184 );
185 }
186 Ok(())
187}
188
189pub fn evaluate<P: Blend>(
203 knots: &KnotVector,
204 control: &[P],
205 u: f64,
206 tol: Tolerances,
207) -> OgeomResult<P> {
208 check_shape(knots, control)?;
209 let span = knots.span(u, tol)?;
210 let p = knots.degree();
211
212 let mut d: smallvec::SmallVec<[P; 8]> = (0..=p).map(|i| control[span - p + i]).collect();
215 let k = knots.knots();
216 for r in 1..=p {
217 for j in (r..=p).rev() {
218 let left = k[span + j - p];
219 let right = k[span + j + 1 - r];
220 let alpha = (u - left) / (right - left);
224 d[j] = d[j - 1].lerp(d[j], alpha);
225 }
226 }
227 Ok(d[p])
228}
229
230pub fn derivatives<P: Blend>(
239 knots: &KnotVector,
240 control: &[P],
241 u: f64,
242 n: usize,
243 tol: Tolerances,
244) -> OgeomResult<Vec<P>> {
245 check_shape(knots, control)?;
246 let span = knots.span(u, tol)?;
247 let p = knots.degree();
248 let basis = knots.basis_derivatives(span, u, n);
249
250 Ok((0..=n)
251 .map(|order| {
252 let mut sum = P::zero();
253 for i in 0..=p {
254 sum = sum.add(control[span - p + i].scale(basis[order][i]));
255 }
256 sum
257 })
258 .collect())
259}
260
261pub fn insert_knot<P: Blend>(
275 knots: &KnotVector,
276 control: &[P],
277 value: f64,
278 count: usize,
279 tol: Tolerances,
280) -> OgeomResult<Spline<P>> {
281 check_shape(knots, control)?;
282 if count == 0 {
283 return Ok((knots.clone(), control.to_vec()));
284 }
285 let mut span = knots.span(value, tol)?;
286 let p = knots.degree();
287 let existing = knots.multiplicity_of(value);
288 let k = knots.knots();
294 let opened = k.partition_point(|x| *x <= value).saturating_sub(1);
295 if opened > span
296 && opened + 1 < k.len()
297 && opened >= existing
298 && opened - existing < control.len()
299 {
300 span = opened;
301 }
302 if existing + count > p {
303 ogeom_bail!(
304 Construction,
305 "inserting {count} copies of {value} would reach multiplicity {}, above degree {p}",
306 existing + count
307 );
308 }
309
310 let new_knots = knots.with_knot_inserted(value, count)?;
311 let last = control.len() - 1;
312 let (s, r) = (existing, count);
313
314 let mut points: Vec<P> = vec![P::zero(); control.len() + r];
315 points[..=span - p].copy_from_slice(&control[..=span - p]);
318 points[span - s + r..=last + r].copy_from_slice(&control[span - s..=last]);
319
320 let mut window: Vec<P> = (0..=p - s).map(|i| control[span - p + i]).collect();
323 let mut window_start = span - p;
324 for j in 1..=r {
325 window_start = span - p + j;
326 for i in 0..=p - j - s {
327 let left = k[window_start + i];
328 let right = k[i + span + 1];
329 let alpha = (value - left) / (right - left);
330 window[i] = window[i].lerp(window[i + 1], alpha);
331 }
332 points[window_start] = window[0];
333 points[span + r - j - s] = window[p - j - s];
334 }
335
336 if window_start + 1 < span - s {
338 let width = (span - s) - (window_start + 1);
339 points[window_start + 1..span - s].copy_from_slice(&window[1..=width]);
340 }
341
342 Ok((new_knots, points))
343}
344
345pub type Spline<P> = (KnotVector, Vec<P>);
347
348pub type BezierSegment<P> = ((f64, f64), Vec<P>);
350
351pub fn join<P: Blend>(a: &Spline<P>, b: &Spline<P>) -> OgeomResult<Spline<P>> {
366 let ((ak, ac), (bk, bc)) = (a, b);
367 check_shape(ak, ac)?;
368 check_shape(bk, bc)?;
369 let p = ak.degree();
370 if bk.degree() != p {
371 ogeom_bail!(
372 Construction,
373 "cannot join a degree {p} B-spline to a degree {} one",
374 bk.degree()
375 );
376 }
377 if !ak.is_clamped() || !bk.is_clamped() {
378 ogeom_bail!(Construction, "only clamped B-splines join");
379 }
380 let shift = ak.domain_end() - bk.domain_start();
381 let mut knots: Vec<f64> = ak.knots()[..ak.knots().len() - 1].to_vec();
382 knots.extend(bk.knots()[p + 1..].iter().map(|k| k + shift));
383 let mut control: Vec<P> = ac[..ac.len() - 1].to_vec();
384 control.extend_from_slice(bc);
385 Ok((KnotVector::new(knots, p)?, control))
386}
387
388pub fn extend<P: Blend>(
410 knots: &KnotVector,
411 control: &[P],
412 at_end: bool,
413 span: f64,
414 continuity: usize,
415 tol: Tolerances,
416) -> OgeomResult<Spline<P>> {
417 check_shape(knots, control)?;
418 if !knots.is_clamped() {
419 ogeom_bail!(Construction, "only clamped B-splines extend");
420 }
421 if !(span > 0.0 && span.is_finite()) {
422 ogeom_bail!(
423 Construction,
424 "an extension needs a positive, finite span; got {span}"
425 );
426 }
427 if !at_end {
428 let (rk, rc) = reverse(knots, control);
432 let (ek, ec) = extend(&rk, &rc, true, span, continuity, tol)?;
433 let (bk, bc) = reverse(&ek, &ec);
434 let (lo, hi) = knots.domain();
435 return Ok((bk.reparameterized(lo - span, hi)?, bc));
436 }
437 let p = knots.degree();
438 let k = continuity.min(p);
439 let end = knots.domain_end();
440 let jet = derivatives(knots, control, end, k, tol)?;
441 let mut bezier: Vec<P> = Vec::with_capacity(k + 1);
444 for j in 0..=k {
445 let mut b = P::zero();
446 let (mut factorial, mut power) = (1.0_f64, 1.0_f64);
447 for (i, derivative) in jet.iter().enumerate().take(j + 1) {
448 if i > 0 {
449 #[allow(clippy::cast_precision_loss)]
450 {
451 factorial *= i as f64;
452 }
453 power *= span;
454 }
455 #[allow(clippy::cast_precision_loss)]
456 let ratio = binomial_coefficient(j, i) as f64 / binomial_coefficient(k, i) as f64;
457 b = b.add(derivative.scale(ratio * power / factorial));
458 }
459 bezier.push(b);
460 }
461 let mut piece_knots: Vec<f64> = Vec::with_capacity(2 * (k + 1));
462 piece_knots.extend(core::iter::repeat_n(end, k + 1));
463 piece_knots.extend(core::iter::repeat_n(end + span, k + 1));
464 let mut piece: Spline<P> = (KnotVector::new(piece_knots, k)?, bezier);
465 for _ in k..p {
466 piece = elevate_degree(&piece.0, &piece.1, tol)?;
467 }
468 join(&(knots.clone(), control.to_vec()), &piece)
469}
470
471pub fn extend_to<P: Blend>(
481 knots: &KnotVector,
482 control: &[P],
483 at_end: bool,
484 target: P,
485 span: f64,
486 continuity: usize,
487 tol: Tolerances,
488) -> OgeomResult<Spline<P>> {
489 check_shape(knots, control)?;
490 if !knots.is_clamped() {
491 ogeom_bail!(Construction, "only clamped B-splines extend");
492 }
493 if !(span > 0.0 && span.is_finite()) {
494 ogeom_bail!(
495 Construction,
496 "an extension needs a positive, finite span; got {span}"
497 );
498 }
499 if !at_end {
500 let (rk, rc) = reverse(knots, control);
501 let (ek, ec) = extend_to(&rk, &rc, true, target, span, continuity, tol)?;
502 let (bk, bc) = reverse(&ek, &ec);
503 let (lo, hi) = knots.domain();
504 return Ok((bk.reparameterized(lo - span, hi)?, bc));
505 }
506 let mut base: Spline<P> = (knots.clone(), control.to_vec());
507 let k = continuity.min(base.0.degree());
508 let n = k + 1;
509 while base.0.degree() < n {
510 base = elevate_degree(&base.0, &base.1, tol)?;
511 }
512 let end = base.0.domain_end();
513 let jet = derivatives(&base.0, &base.1, end, k, tol)?;
514 let mut bezier: Vec<P> = Vec::with_capacity(n + 1);
517 for j in 0..=k {
518 let mut b = P::zero();
519 let (mut factorial, mut power) = (1.0_f64, 1.0_f64);
520 for (i, derivative) in jet.iter().enumerate().take(j + 1) {
521 if i > 0 {
522 #[allow(clippy::cast_precision_loss)]
523 {
524 factorial *= i as f64;
525 }
526 power *= span;
527 }
528 #[allow(clippy::cast_precision_loss)]
529 let ratio = binomial_coefficient(j, i) as f64 / binomial_coefficient(n, i) as f64;
530 b = b.add(derivative.scale(ratio * power / factorial));
531 }
532 bezier.push(b);
533 }
534 bezier.push(target);
535 let mut piece_knots: Vec<f64> = Vec::with_capacity(2 * (n + 1));
536 piece_knots.extend(core::iter::repeat_n(end, n + 1));
537 piece_knots.extend(core::iter::repeat_n(end + span, n + 1));
538 let mut piece: Spline<P> = (KnotVector::new(piece_knots, n)?, bezier);
539 for _ in n..base.0.degree() {
540 piece = elevate_degree(&piece.0, &piece.1, tol)?;
541 }
542 join(&base, &piece)
543}
544
545pub fn split<P: Blend>(
555 knots: &KnotVector,
556 control: &[P],
557 u: f64,
558 tol: Tolerances,
559) -> OgeomResult<(Spline<P>, Spline<P>)> {
560 check_shape(knots, control)?;
561 let (start, end) = knots.domain();
562 if u <= start + tol.parametric() || u >= end - tol.parametric() {
563 ogeom_bail!(
564 Domain,
565 "cannot split at {u}, an end of the domain [{start}, {end}]"
566 );
567 }
568 let p = knots.degree();
569 let existing = knots.multiplicity_of(u);
570 let (refined, points) = insert_knot(knots, control, u, p - existing, tol)?;
571
572 let cut = refined.knots().partition_point(|k| *k < u);
574 let left_points = points[..cut].to_vec();
575 let right_points = points[cut - 1..].to_vec();
576
577 let mut left_knots = refined.knots()[..cut + p].to_vec();
578 left_knots.push(u);
579 let mut right_knots = vec![u];
580 right_knots.extend_from_slice(&refined.knots()[cut..]);
581
582 Ok((
583 (KnotVector::new(left_knots, p)?, left_points),
584 (KnotVector::new(right_knots, p)?, right_points),
585 ))
586}
587
588pub fn to_bezier_segments<P: Blend>(
599 knots: &KnotVector,
600 control: &[P],
601 tol: Tolerances,
602) -> OgeomResult<Vec<BezierSegment<P>>> {
603 check_shape(knots, control)?;
604 let p = knots.degree();
605 let end = knots.domain().1;
606
607 let clamp_start = |knots: &KnotVector, control: &[P]| -> OgeomResult<Spline<P>> {
613 let (start, _) = knots.domain();
614 let needed = p.saturating_sub(knots.multiplicity_of(start));
615 insert_knot(knots, control, start, needed, tol)
616 };
617 let (mut current_knots, mut current_points) = clamp_start(knots, control)?;
618 if current_knots.multiplicity_of(end) < p {
619 let (reversed_knots, reversed_points) = reverse(¤t_knots, ¤t_points);
620 let (reversed_knots, reversed_points) = clamp_start(&reversed_knots, &reversed_points)?;
621 (current_knots, current_points) = reverse(&reversed_knots, &reversed_points);
622 }
623 let (start, end) = current_knots.domain();
624 for (value, multiplicity) in current_knots.clone().distinct() {
625 if value <= start || value >= end {
626 continue;
627 }
628 let needed = p.saturating_sub(multiplicity);
629 if needed > 0 {
630 let (k, c) = insert_knot(¤t_knots, ¤t_points, value, needed, tol)?;
631 current_knots = k;
632 current_points = c;
633 }
634 }
635
636 let breaks: Vec<f64> = core::iter::once(start)
637 .chain(
638 current_knots
639 .distinct()
640 .into_iter()
641 .filter(|(v, _)| *v > start && *v < end)
642 .map(|(v, _)| v),
643 )
644 .chain(core::iter::once(end))
645 .collect();
646
647 let mut out = Vec::with_capacity(breaks.len() - 1);
649 for w in breaks.windows(2) {
650 let span = current_knots.span(f64::midpoint(w[0], w[1]), tol)?;
651 out.push(((w[0], w[1]), current_points[span - p..=span].to_vec()));
652 }
653 Ok(out)
654}
655
656pub fn elevate_degree<P: Blend>(
668 knots: &KnotVector,
669 control: &[P],
670 tol: Tolerances,
671) -> OgeomResult<Spline<P>> {
672 check_shape(knots, control)?;
673 let p = knots.degree();
674 let segments = to_bezier_segments(knots, control, tol)?;
675
676 let mut points: Vec<P> = Vec::with_capacity(segments.len() * (p + 1) + 1);
677 let mut new_knots: Vec<f64> = Vec::new();
678
679 for (index, ((a, b), segment)) in segments.iter().enumerate() {
680 let mut elevated: Vec<P> = Vec::with_capacity(p + 2);
682 elevated.push(segment[0]);
683 #[allow(clippy::cast_precision_loss)]
684 for i in 1..=p {
685 let t = i as f64 / (p + 1) as f64;
686 elevated.push(segment[i - 1].lerp(segment[i], 1.0 - t));
687 }
688 elevated.push(segment[p]);
689
690 if index == 0 {
691 points.extend_from_slice(&elevated);
692 new_knots.extend(core::iter::repeat_n(*a, p + 2));
693 } else {
694 points.extend_from_slice(&elevated[1..]);
696 new_knots.extend(core::iter::repeat_n(*a, p + 1));
697 }
698 if index == segments.len() - 1 {
699 new_knots.extend(core::iter::repeat_n(*b, p + 2));
700 }
701 }
702
703 for (value, multiplicity) in knots.distinct() {
707 let (start, end) = knots.domain();
708 if value <= start || value >= end {
709 continue;
710 }
711 for _ in multiplicity..p {
712 (new_knots, points) = remove_knot_once(&new_knots, &points, p + 1, value);
713 }
714 }
715
716 Ok((KnotVector::new(new_knots, p + 1)?, points))
717}
718
719fn remove_knot_once<P: Blend>(
723 knots: &[f64],
724 control: &[P],
725 p: usize,
726 u: f64,
727) -> (Vec<f64>, Vec<P>) {
728 let Some(r) = knots.iter().rposition(|k| *k == u) else {
729 return (knots.to_vec(), control.to_vec());
730 };
731 let left = knots.iter().filter(|k| **k == u).count() - 1;
732 let mut reduced = knots.to_vec();
733 reduced.remove(r);
734 let k = r - 1;
738 let (lo, hi) = (k + 1 - p, k - left);
739 let alpha = |i: usize| (u - reduced[i]) / (reduced[i + p] - reduced[i]);
740 let mut q: Vec<P> = Vec::with_capacity(control.len() - 1);
741 q.extend_from_slice(&control[..lo]);
742 let mut forward: Vec<P> = Vec::with_capacity(hi - lo);
743 let mut previous = control[lo - 1];
744 for (i, point) in control.iter().enumerate().take(hi).skip(lo) {
745 let a = alpha(i);
746 previous = point.sub(previous.scale(1.0 - a)).scale(1.0 / a);
747 forward.push(previous);
748 }
749 let mut backward: Vec<P> = vec![P::zero(); hi - lo];
750 let mut next = control[hi + 1];
751 for i in (lo + 1..=hi).rev() {
752 let a = alpha(i);
753 next = control[i].sub(next.scale(a)).scale(1.0 / (1.0 - a));
754 backward[i - 1 - lo] = next;
755 }
756 let middle = (hi - lo) / 2;
757 for j in 0..hi - lo {
758 q.push(if j < middle { forward[j] } else { backward[j] });
759 }
760 q.extend_from_slice(&control[hi + 1..]);
761 (reduced, q)
762}
763
764#[must_use]
766pub fn reverse<P: Blend>(knots: &KnotVector, control: &[P]) -> Spline<P> {
767 let mut points = control.to_vec();
768 points.reverse();
769 (knots.reversed(), points)
770}
771
772pub fn evaluate_rational<P: Blend>(
780 knots: &KnotVector,
781 control: &[Weighted<P>],
782 u: f64,
783 tol: Tolerances,
784) -> OgeomResult<P> {
785 let h = evaluate(knots, control, u, tol)?;
786 if h.weight.abs() <= tol.confusion() {
787 ogeom_bail!(Numeric, "rational evaluation produced a vanishing weight");
788 }
789 Ok(h.point())
790}
791
792pub fn rational_derivatives<P: Blend>(
802 knots: &KnotVector,
803 control: &[Weighted<P>],
804 u: f64,
805 n: usize,
806 tol: Tolerances,
807) -> OgeomResult<Vec<P>> {
808 let homogeneous = derivatives(knots, control, u, n, tol)?;
809 if homogeneous[0].weight.abs() <= tol.confusion() {
810 ogeom_bail!(Numeric, "rational evaluation produced a vanishing weight");
811 }
812
813 let mut out: Vec<P> = Vec::with_capacity(n + 1);
815 for (order, term) in homogeneous.iter().enumerate() {
816 let mut value = term.scaled;
817 for i in 1..=order {
818 #[allow(clippy::cast_precision_loss)]
819 let binomial = binomial_coefficient(order, i) as f64;
820 value = value.sub(out[order - i].scale(binomial * homogeneous[i].weight));
821 }
822 out.push(value.scale(1.0 / homogeneous[0].weight));
823 }
824 Ok(out)
825}
826
827#[must_use]
830pub fn binomial_coefficient(n: usize, k: usize) -> u64 {
831 if k > n {
832 return 0;
833 }
834 let k = k.min(n - k);
835 let mut result = 1_u64;
836 for i in 0..k {
837 result = result * (n - i) as u64 / (i as u64 + 1);
838 }
839 result
840}
841
842#[cfg(test)]
843#[allow(clippy::unwrap_used)]
844mod join_tests {
845 use super::*;
846 use crate::Point;
847
848 #[test]
849 fn a_joined_spline_evaluates_as_its_two_halves_did() {
850 let tol = Tolerances::millimetres();
851 let control: Vec<Point> = (0..6)
852 .map(|i| Point::new(f64::from(i), f64::from(i * i % 5), 0.0))
853 .collect();
854 let knots = KnotVector::clamped_uniform(3, control.len()).unwrap();
855 let ((lk, lc), (rk, rc)) = split(&knots, &control, 0.4, tol).unwrap();
856 let (jk, jc) = join(&(lk, lc), &(rk, rc)).unwrap();
857 assert_eq!(
858 jk.domain(),
859 knots.domain(),
860 "the domain is the two laid end to end"
861 );
862 assert_eq!(
863 jc.len() + 3 + 1,
864 jk.knots().len(),
865 "the knots fit the controls"
866 );
867 for i in 0..=20 {
868 let u = f64::from(i) / 20.0;
869 let before = evaluate(&knots, &control, u, tol).unwrap();
870 let after = evaluate(&jk, &jc, u, tol).unwrap();
871 assert!(
872 before.is_equal(after, tol),
873 "at {u}: {before:?} became {after:?}"
874 );
875 }
876 }
877}
878
879#[cfg(test)]
880#[allow(clippy::unwrap_used)]
881mod tests {
882 use super::*;
883 use approx::assert_relative_eq;
884
885 const T: Tolerances = Tolerances::millimetres();
886
887 #[test]
891 fn an_extension_continues_the_curve_to_its_order() {
892 let knots = KnotVector::clamped_uniform(3, 6).unwrap();
893 let control = vec![
894 Point::new(0.0, 0.0, 0.0),
895 Point::new(1.0, 2.0, 0.5),
896 Point::new(2.5, 1.0, -0.5),
897 Point::new(4.0, 3.0, 1.0),
898 Point::new(5.0, 0.5, 0.0),
899 Point::new(6.0, 2.0, 2.0),
900 ];
901 let (lo, hi) = knots.domain();
902 for at_end in [true, false] {
903 let (ek, ec) = extend(&knots, &control, at_end, 0.4, 2, T).unwrap();
904 let (elo, ehi) = ek.domain();
905 if at_end {
906 assert!((elo - lo).abs() < 1e-12 && (ehi - (hi + 0.4)).abs() < 1e-12);
907 } else {
908 assert!((elo - (lo - 0.4)).abs() < 1e-12 && (ehi - hi).abs() < 1e-12);
909 }
910 for i in 0..=10 {
911 let u = lo + (hi - lo) * f64::from(i) / 10.0;
912 let was = evaluate(&knots, &control, u, T).unwrap();
913 let now = evaluate(&ek, &ec, u, T).unwrap();
914 assert!(
915 was.distance(now) < 1e-9,
916 "the original run at {u}: {was:?} vs {now:?}"
917 );
918 }
919 let join_at = if at_end { hi } else { lo };
922 let step = if at_end { 1e-7 } else { -1e-7 };
923 let inside = derivatives(&knots, &control, join_at - step, 2, T).unwrap();
924 let outside = derivatives(&ek, &ec, join_at + step, 2, T).unwrap();
925 for order in 0..=2 {
926 let (a, b) = (inside[order], outside[order]);
927 let gap = a.to_vector().sub(b.to_vector()).magnitude();
928 let scale = a.to_vector().magnitude().max(1.0);
929 assert!(
930 gap < scale * 1e-4,
931 "order {order} across the join: {a:?} vs {b:?}"
932 );
933 }
934 }
935 }
936
937 fn cubic_curve() -> (KnotVector, Vec<Point>) {
938 let control = vec![
939 Point::new(0.0, 0.0, 0.0),
940 Point::new(1.0, 2.0, 0.0),
941 Point::new(3.0, 3.0, 1.0),
942 Point::new(5.0, 1.0, 2.0),
943 Point::new(6.0, -1.0, 1.0),
944 Point::new(8.0, 0.0, 0.0),
945 ];
946 (
947 KnotVector::clamped_uniform(3, control.len()).unwrap(),
948 control,
949 )
950 }
951
952 fn sample(knots: &KnotVector, control: &[Point], n: usize) -> Vec<Point> {
953 let (a, b) = knots.domain();
954 (0..=n)
955 .map(|i| {
956 #[allow(clippy::cast_precision_loss)]
957 let u = a + (b - a) * (i as f64 / n as f64);
958 evaluate(knots, control, u, T).unwrap()
959 })
960 .collect()
961 }
962
963 #[test]
964 fn a_clamped_curve_interpolates_its_end_points() {
965 let (k, c) = cubic_curve();
966 let (a, b) = k.domain();
967 assert!(evaluate(&k, &c, a, T).unwrap().is_equal(c[0], T));
968 assert!(evaluate(&k, &c, b, T).unwrap().is_equal(c[c.len() - 1], T));
969 }
970
971 #[test]
972 fn de_boor_agrees_with_the_basis_function_sum() {
973 let (k, c) = cubic_curve();
975 for i in 0..=50 {
976 let u = f64::from(i) / 50.0;
977 let span = k.span(u, T).unwrap();
978 let basis = k.basis(span, u);
979 let mut sum = Vector::ZERO;
980 for j in 0..=k.degree() {
981 sum += c[span - k.degree() + j].to_vector() * basis[j];
982 }
983 let de_boor = evaluate(&k, &c, u, T).unwrap();
984 assert!(de_boor.is_equal(Point::from_vector(sum), T), "at u = {u}");
985 }
986 }
987
988 #[test]
989 fn shape_mismatches_and_out_of_domain_parameters_are_refused() {
990 let (k, c) = cubic_curve();
991 assert!(
992 evaluate(&k, &c[..3], 0.5, T).is_err(),
993 "too few control points"
994 );
995 assert!(evaluate(&k, &c, -0.1, T).is_err());
996 assert!(evaluate(&k, &c, 1.1, T).is_err());
997 }
998
999 #[test]
1000 fn derivatives_agree_with_finite_differences() {
1001 let (k, c) = cubic_curve();
1002 let h = 1e-6;
1003 for i in 1..20 {
1004 let u = f64::from(i) / 20.0;
1005 let d = derivatives(&k, &c, u, 2, T).unwrap();
1006 assert!(d[0].is_equal(evaluate(&k, &c, u, T).unwrap(), T));
1007
1008 let ahead = evaluate(&k, &c, u + h, T).unwrap();
1009 let behind = evaluate(&k, &c, u - h, T).unwrap();
1010 let numeric = (ahead - behind) * (1.0 / (2.0 * h));
1011 assert!(
1012 (d[1].to_vector() - numeric).magnitude() < 1e-5,
1013 "first derivative disagrees at {u}"
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn knot_insertion_does_not_move_the_curve() {
1020 let (k, c) = cubic_curve();
1021 let before = sample(&k, &c, 100);
1022 for (value, count) in [(0.25, 1), (0.5, 2), (0.75, 3), (0.1, 1)] {
1023 let (k2, c2) = insert_knot(&k, &c, value, count, T).unwrap();
1024 assert_eq!(c2.len(), c.len() + count);
1025 assert_eq!(k2.multiplicity_of(value), k.multiplicity_of(value) + count);
1026 let after = sample(&k2, &c2, 100);
1027 for (a, b) in before.iter().zip(&after) {
1028 assert!(
1029 a.is_equal(*b, T),
1030 "inserting {count} at {value} moved the curve"
1031 );
1032 }
1033 }
1034 }
1035
1036 #[test]
1040 fn insertion_at_an_unclamped_domain_end_does_not_move_the_curve() {
1041 let k = KnotVector::new((0..=9).map(f64::from).collect(), 3).unwrap();
1042 let c: Vec<Point> = [
1043 (0.0, 0.0),
1044 (4.0, 0.0),
1045 (5.0, 3.0),
1046 (2.0, 5.0),
1047 (-1.0, 3.0),
1048 (0.0, 1.0),
1049 ]
1050 .into_iter()
1051 .map(|(x, y)| Point::new(x, y, 0.5 * x))
1052 .collect();
1053 let (lo, hi) = k.domain();
1054 for value in [lo, hi] {
1055 for count in 1..=2 {
1056 let (k2, c2) = insert_knot(&k, &c, value, count, T).unwrap();
1057 for i in 0..=40 {
1058 let u = lo + (hi - lo) * f64::from(i) / 40.0;
1059 let (a, b) = (
1060 evaluate(&k, &c, u, T).unwrap(),
1061 evaluate(&k2, &c2, u, T).unwrap(),
1062 );
1063 assert!(
1064 a.is_equal(b, T),
1065 "{count} at {value} moved u {u}: {a:?} {b:?}"
1066 );
1067 }
1068 }
1069 }
1070 }
1071
1072 #[test]
1073 fn repeated_insertion_matches_a_single_multiple_insertion() {
1074 let (k, c) = cubic_curve();
1075 let (ka, ca) = insert_knot(&k, &c, 0.4, 3, T).unwrap();
1076
1077 let (k1, c1) = insert_knot(&k, &c, 0.4, 1, T).unwrap();
1078 let (k2, c2) = insert_knot(&k1, &c1, 0.4, 1, T).unwrap();
1079 let (kb, cb) = insert_knot(&k2, &c2, 0.4, 1, T).unwrap();
1080
1081 assert_eq!(ka.knots(), kb.knots());
1082 for (a, b) in ca.iter().zip(&cb) {
1083 assert!(a.is_equal(*b, T));
1084 }
1085 }
1086
1087 #[test]
1088 fn insertion_beyond_the_degree_is_refused() {
1089 let (k, c) = cubic_curve();
1090 assert!(insert_knot(&k, &c, 0.5, 4, T).is_err());
1091 assert!(insert_knot(&k, &c, 0.5, 3, T).is_ok());
1092 assert!(
1093 insert_knot(&k, &c, 2.0, 1, T).is_err(),
1094 "outside the domain"
1095 );
1096 }
1097
1098 #[test]
1099 fn splitting_reproduces_both_halves_of_the_original() {
1100 let (k, c) = cubic_curve();
1101 let cut = 0.4;
1102 let ((lk, lc), (rk, rc)) = split(&k, &c, cut, T).unwrap();
1103
1104 assert_relative_eq!(lk.domain().1, cut, epsilon = 1e-15);
1105 assert_relative_eq!(rk.domain().0, cut, epsilon = 1e-15);
1106 assert!(lk.is_clamped() && rk.is_clamped());
1107
1108 for i in 0..=40 {
1109 let t = f64::from(i) / 40.0;
1110 let left_u = lk.domain().0 + (cut - lk.domain().0) * t;
1111 let right_u = cut + (rk.domain().1 - cut) * t;
1112 assert!(
1113 evaluate(&lk, &lc, left_u, T)
1114 .unwrap()
1115 .is_equal(evaluate(&k, &c, left_u, T).unwrap(), T),
1116 "left half diverges at {left_u}"
1117 );
1118 assert!(
1119 evaluate(&rk, &rc, right_u, T)
1120 .unwrap()
1121 .is_equal(evaluate(&k, &c, right_u, T).unwrap(), T),
1122 "right half diverges at {right_u}"
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 fn splitting_at_an_end_of_the_domain_is_refused() {
1129 let (k, c) = cubic_curve();
1130 assert!(split(&k, &c, 0.0, T).is_err());
1131 assert!(split(&k, &c, 1.0, T).is_err());
1132 }
1133
1134 #[test]
1135 fn an_unclamped_curve_decomposes_and_elevates_unchanged() {
1136 let knots = KnotVector::new((0..10).map(f64::from).collect(), 3).unwrap();
1138 let control = vec![
1139 Point::new(0.0, 0.0, 0.0),
1140 Point::new(5.0, 1.0, 0.0),
1141 Point::new(-2.0, 3.0, 1.0),
1142 Point::new(7.0, 2.0, 2.0),
1143 Point::new(1.0, -1.0, 1.0),
1144 Point::new(3.0, 0.0, 0.0),
1145 ];
1146 let segments = to_bezier_segments(&knots, &control, T).unwrap();
1147 assert_eq!(segments.len(), 3);
1148 for ((a, b), points) in &segments {
1149 let bezier = KnotVector::clamped_uniform(3, points.len()).unwrap();
1150 for s in [0.0, 0.25, 0.5, 1.0] {
1151 let on_segment = evaluate(&bezier, points, s, T).unwrap();
1152 let on_curve = evaluate(&knots, &control, a + (b - a) * s, T).unwrap();
1153 assert!(on_segment.distance(on_curve) < 1e-12, "[{a}, {b}] at {s}");
1154 }
1155 }
1156 let (raised, points) = elevate_degree(&knots, &control, T).unwrap();
1157 assert_eq!(raised.domain(), knots.domain());
1158 for i in 0..=30 {
1159 let u = 3.0 + f64::from(i) / 10.0;
1160 let moved = evaluate(&raised, &points, u, T)
1161 .unwrap()
1162 .distance(evaluate(&knots, &control, u, T).unwrap());
1163 assert!(moved < 1e-12, "elevation moved the curve {moved} at {u}");
1164 }
1165 }
1166
1167 #[test]
1168 fn bezier_decomposition_covers_the_curve_exactly() {
1169 let (k, c) = cubic_curve();
1170 let segments = to_bezier_segments(&k, &c, T).unwrap();
1171 assert_eq!(segments.len(), 3);
1173 for (_, points) in &segments {
1174 assert_eq!(points.len(), k.degree() + 1);
1175 }
1176
1177 for ((a, b), points) in &segments {
1180 let bezier = KnotVector::clamped_uniform(k.degree(), points.len())
1181 .unwrap()
1182 .reparameterized(*a, *b)
1183 .unwrap();
1184 for i in 0..=20 {
1185 let u = a + (b - a) * (f64::from(i) / 20.0);
1186 assert!(
1187 evaluate(&bezier, points, u, T)
1188 .unwrap()
1189 .is_equal(evaluate(&k, &c, u, T).unwrap(), T),
1190 "segment [{a}, {b}] diverges at {u}"
1191 );
1192 }
1193 }
1194 }
1195
1196 #[test]
1197 fn degree_elevation_does_not_move_the_curve() {
1198 let (k, c) = cubic_curve();
1199 let before = sample(&k, &c, 100);
1200 let (k2, c2) = elevate_degree(&k, &c, T).unwrap();
1201 assert_eq!(k2.degree(), k.degree() + 1);
1202 assert_eq!(k2.domain(), k.domain());
1203
1204 let after = sample(&k2, &c2, 100);
1205 for (a, b) in before.iter().zip(&after) {
1206 assert!(a.is_equal(*b, T), "elevation moved the curve");
1207 }
1208 }
1209
1210 #[test]
1211 fn elevation_keeps_the_continuity_at_every_knot() {
1212 let (k, c) = cubic_curve();
1213 let (k2, c2) = elevate_degree(&k, &c, T).unwrap();
1214 let interior = |v: &KnotVector| -> Vec<(f64, usize)> {
1215 let (a, b) = v.domain();
1216 v.distinct()
1217 .into_iter()
1218 .filter(|(x, _)| *x > a && *x < b)
1219 .collect()
1220 };
1221 let raised: Vec<(f64, usize)> = interior(&k).into_iter().map(|(x, m)| (x, m + 1)).collect();
1222 assert_eq!(interior(&k2), raised);
1223 let before = sample(&k, &c, 100);
1224 for (a, b) in before.iter().zip(&sample(&k2, &c2, 100)) {
1225 assert!(a.distance(*b) < 1e-12, "elevation moved the curve");
1226 }
1227 for (x, _) in interior(&k2) {
1229 let left = derivatives(&k2, &c2, x - 1e-9, 2, T).unwrap()[2];
1230 let right = derivatives(&k2, &c2, x + 1e-9, 2, T).unwrap()[2];
1231 assert!(
1232 left.distance(right) < 1e-5,
1233 "{left:?} against {right:?} at {x}"
1234 );
1235 }
1236 }
1237
1238 #[test]
1239 fn elevation_twice_is_still_the_same_curve() {
1240 let (k, c) = cubic_curve();
1241 let before = sample(&k, &c, 60);
1242 let (k1, c1) = elevate_degree(&k, &c, T).unwrap();
1243 let (k2, c2) = elevate_degree(&k1, &c1, T).unwrap();
1244 assert_eq!(k2.degree(), 5);
1245 for (a, b) in before.iter().zip(&sample(&k2, &c2, 60)) {
1246 assert!(a.is_equal(*b, T));
1247 }
1248 }
1249
1250 #[test]
1251 fn reversal_traverses_the_same_points_backwards() {
1252 let (k, c) = cubic_curve();
1253 let (rk, rc) = reverse(&k, &c);
1254 let (a, b) = k.domain();
1255 for i in 0..=40 {
1256 let t = f64::from(i) / 40.0;
1257 let forward = evaluate(&k, &c, a + (b - a) * t, T).unwrap();
1258 let backward = evaluate(&rk, &rc, a + (b - a) * (1.0 - t), T).unwrap();
1259 assert!(forward.is_equal(backward, T), "at t = {t}");
1260 }
1261 }
1262
1263 fn quarter_circle() -> (KnotVector, Vec<Weighted<Point>>) {
1266 let w = core::f64::consts::FRAC_1_SQRT_2;
1267 let control = vec![
1268 Weighted::new(Point::new(1.0, 0.0, 0.0), 1.0, T).unwrap(),
1269 Weighted::new(Point::new(1.0, 1.0, 0.0), w, T).unwrap(),
1270 Weighted::new(Point::new(0.0, 1.0, 0.0), 1.0, T).unwrap(),
1271 ];
1272 (KnotVector::clamped_uniform(2, 3).unwrap(), control)
1273 }
1274
1275 #[test]
1276 fn a_rational_quadratic_traces_an_exact_circular_arc() {
1277 let (k, c) = quarter_circle();
1278 for i in 0..=100 {
1279 let u = f64::from(i) / 100.0;
1280 let p = evaluate_rational(&k, &c, u, T).unwrap();
1281 assert_relative_eq!(p.to_vector().magnitude(), 1.0, epsilon = 1e-14);
1284 assert_relative_eq!(p.z, 0.0, epsilon = 1e-15);
1285 }
1286 assert!(
1287 evaluate_rational(&k, &c, 0.0, T)
1288 .unwrap()
1289 .is_equal(Point::new(1.0, 0.0, 0.0), T)
1290 );
1291 assert!(
1292 evaluate_rational(&k, &c, 1.0, T)
1293 .unwrap()
1294 .is_equal(Point::new(0.0, 1.0, 0.0), T)
1295 );
1296 }
1297
1298 #[test]
1299 fn rational_derivatives_agree_with_finite_differences() {
1300 let (k, c) = quarter_circle();
1301 let h = 1e-6;
1302 for i in 1..20 {
1303 let u = f64::from(i) / 20.0;
1304 let d = rational_derivatives(&k, &c, u, 2, T).unwrap();
1305 assert!(d[0].is_equal(evaluate_rational(&k, &c, u, T).unwrap(), T));
1306
1307 let ahead = evaluate_rational(&k, &c, u + h, T).unwrap();
1308 let behind = evaluate_rational(&k, &c, u - h, T).unwrap();
1309 let numeric = (ahead - behind) * (1.0 / (2.0 * h));
1310 assert!(
1311 (d[1].to_vector() - numeric).magnitude() < 1e-5,
1312 "at u = {u}: {:?} vs {numeric:?}",
1313 d[1]
1314 );
1315 }
1316 }
1317
1318 #[test]
1319 fn the_tangent_of_a_circular_arc_is_perpendicular_to_its_radius() {
1320 let (k, c) = quarter_circle();
1321 for i in 0..=20 {
1322 let u = f64::from(i) / 20.0;
1323 let d = rational_derivatives(&k, &c, u, 1, T).unwrap();
1324 let radius = d[0].to_vector();
1325 let tangent = d[1].to_vector();
1326 assert!(
1327 radius.dot(tangent).abs() < 1e-12,
1328 "not perpendicular at {u}: {}",
1329 radius.dot(tangent)
1330 );
1331 }
1332 }
1333
1334 #[test]
1335 fn knot_insertion_preserves_a_rational_curve_too() {
1336 let (k, c) = quarter_circle();
1337 let (k2, c2) = insert_knot(&k, &c, 0.5, 1, T).unwrap();
1338 for i in 0..=50 {
1339 let u = f64::from(i) / 50.0;
1340 let a = evaluate_rational(&k, &c, u, T).unwrap();
1341 let b = evaluate_rational(&k2, &c2, u, T).unwrap();
1342 assert!(a.is_equal(b, T), "at {u}");
1343 assert_relative_eq!(b.to_vector().magnitude(), 1.0, epsilon = 1e-14);
1344 }
1345 }
1346
1347 #[test]
1348 fn degenerate_weights_are_refused() {
1349 assert!(Weighted::new(Point::ORIGIN, 0.0, T).is_err());
1350 assert!(Weighted::new(Point::ORIGIN, -1.0, T).is_err());
1351 assert!(Weighted::new(Point::ORIGIN, f64::NAN, T).is_err());
1352 assert!(Weighted::new(Point::ORIGIN, f64::INFINITY, T).is_err());
1353 assert!(Weighted::new(Point::ORIGIN, 2.0, T).is_ok());
1354 }
1355
1356 #[test]
1357 fn weighted_round_trips_through_its_homogeneous_form() {
1358 let p = Point::new(3.0, -1.0, 2.0);
1359 let w = Weighted::new(p, 2.5, T).unwrap();
1360 assert!(w.point().is_equal(p, T));
1361 assert!(w.scaled.is_equal(Point::new(7.5, -2.5, 5.0), T));
1362 }
1363
1364 #[test]
1365 fn binomial_coefficients() {
1366 assert_eq!(binomial_coefficient(0, 0), 1);
1367 assert_eq!(binomial_coefficient(5, 0), 1);
1368 assert_eq!(binomial_coefficient(5, 5), 1);
1369 assert_eq!(binomial_coefficient(5, 2), 10);
1370 assert_eq!(binomial_coefficient(10, 5), 252);
1371 assert_eq!(binomial_coefficient(3, 4), 0);
1372 }
1373
1374 #[test]
1375 fn scalar_and_planar_control_points_work_too() {
1376 let k = KnotVector::clamped_uniform(2, 4).unwrap();
1379 let scalars = vec![0.0_f64, 1.0, 3.0, 2.0];
1380 assert_relative_eq!(evaluate(&k, &scalars, 0.0, T).unwrap(), 0.0);
1381 assert_relative_eq!(evaluate(&k, &scalars, 1.0, T).unwrap(), 2.0);
1382
1383 let planar = vec![
1384 Point2::new(0.0, 0.0),
1385 Point2::new(1.0, 2.0),
1386 Point2::new(3.0, 1.0),
1387 Point2::new(4.0, 0.0),
1388 ];
1389 assert!(
1390 evaluate(&k, &planar, 0.0, T)
1391 .unwrap()
1392 .is_equal(planar[0], T)
1393 );
1394 assert!(
1395 evaluate(&k, &planar, 1.0, T)
1396 .unwrap()
1397 .is_equal(planar[3], T)
1398 );
1399 }
1400}
1401
1402#[derive(Debug, Clone, PartialEq)]
1409pub struct ControlGrid<P> {
1410 points: Vec<P>,
1411 u_count: usize,
1412 v_count: usize,
1413}
1414
1415impl<P: Blend> ControlGrid<P> {
1416 pub fn new(points: Vec<P>, u_count: usize, v_count: usize) -> OgeomResult<Self> {
1423 if u_count == 0 || v_count == 0 {
1424 ogeom_bail!(Dimension, "control grid must be at least 1x1");
1425 }
1426 if points.len() != u_count * v_count {
1427 ogeom_bail!(
1428 Dimension,
1429 "a {u_count}x{v_count} grid needs {} points, got {}",
1430 u_count * v_count,
1431 points.len()
1432 );
1433 }
1434 Ok(Self {
1435 points,
1436 u_count,
1437 v_count,
1438 })
1439 }
1440
1441 #[must_use]
1443 pub const fn u_count(&self) -> usize {
1444 self.u_count
1445 }
1446
1447 #[must_use]
1449 pub const fn v_count(&self) -> usize {
1450 self.v_count
1451 }
1452
1453 #[must_use]
1455 pub fn get(&self, i: usize, j: usize) -> Option<P> {
1456 if i >= self.u_count || j >= self.v_count {
1457 return None;
1458 }
1459 self.points.get(i * self.v_count + j).copied()
1460 }
1461
1462 #[must_use]
1464 pub fn points(&self) -> &[P] {
1465 &self.points
1466 }
1467
1468 #[must_use]
1470 pub fn transposed(&self) -> Self {
1471 let mut points = Vec::with_capacity(self.points.len());
1472 for j in 0..self.v_count {
1473 for i in 0..self.u_count {
1474 points.push(self.points[i * self.v_count + j]);
1475 }
1476 }
1477 Self {
1478 points,
1479 u_count: self.v_count,
1480 v_count: self.u_count,
1481 }
1482 }
1483
1484 #[must_use]
1486 pub fn map<Q: Blend>(&self, f: impl Fn(P) -> Q) -> ControlGrid<Q> {
1487 ControlGrid {
1488 points: self.points.iter().map(|p| f(*p)).collect(),
1489 u_count: self.u_count,
1490 v_count: self.v_count,
1491 }
1492 }
1493}
1494
1495fn check_grid_shape<P>(ku: &KnotVector, kv: &KnotVector, grid: &ControlGrid<P>) -> OgeomResult<()> {
1497 if grid.u_count != ku.control_point_count() || grid.v_count != kv.control_point_count() {
1498 ogeom_bail!(
1499 Dimension,
1500 "knot vectors describe a {}x{} grid, got {}x{}",
1501 ku.control_point_count(),
1502 kv.control_point_count(),
1503 grid.u_count,
1504 grid.v_count
1505 );
1506 }
1507 Ok(())
1508}
1509
1510pub fn evaluate_surface<P: Blend>(
1522 ku: &KnotVector,
1523 kv: &KnotVector,
1524 grid: &ControlGrid<P>,
1525 u: f64,
1526 v: f64,
1527 tol: Tolerances,
1528) -> OgeomResult<P> {
1529 check_grid_shape(ku, kv, grid)?;
1530 let (p, q) = (ku.degree(), kv.degree());
1531 let (su, sv) = (ku.span(u, tol)?, kv.span(v, tol)?);
1532 let (nu, nv) = (ku.basis(su, u), kv.basis(sv, v));
1533
1534 let mut total = P::zero();
1535 for (i, &weight_u) in nu.iter().enumerate() {
1536 let mut row = P::zero();
1539 for (j, &weight_v) in nv.iter().enumerate() {
1540 let Some(point) = grid.get(su - p + i, sv - q + j) else {
1541 ogeom_bail!(Dimension, "control grid index out of range");
1542 };
1543 row = row.add(point.scale(weight_v));
1544 }
1545 total = total.add(row.scale(weight_u));
1546 }
1547 Ok(total)
1548}
1549
1550pub fn surface_derivatives<P: Blend>(
1560 ku: &KnotVector,
1561 kv: &KnotVector,
1562 grid: &ControlGrid<P>,
1563 u: f64,
1564 v: f64,
1565 order: usize,
1566 tol: Tolerances,
1567) -> OgeomResult<DerivativeGrid<P>> {
1568 check_grid_shape(ku, kv, grid)?;
1569 let (p, q) = (ku.degree(), kv.degree());
1570 let (su, sv) = (ku.span(u, tol)?, kv.span(v, tol)?);
1571 let du = ku.basis_derivatives(su, u, order);
1572 let dv = kv.basis_derivatives(sv, v, order);
1573
1574 let mut out: DerivativeGrid<P> =
1575 core::iter::repeat_with(|| core::iter::repeat_with(P::zero).take(order + 1).collect())
1576 .take(order + 1)
1577 .collect();
1578 let mut across: SmallVec<[SmallVec<[P; 8]>; 4]> = SmallVec::new();
1582 for weights_v in dv.iter().take(order + 1) {
1583 let mut row: SmallVec<[P; 8]> = SmallVec::new();
1584 for i in 0..=p {
1585 let mut inner = P::zero();
1586 for (j, &weight_v) in weights_v.iter().enumerate() {
1587 let Some(point) = grid.get(su - p + i, sv - q + j) else {
1588 ogeom_bail!(Dimension, "control grid index out of range");
1589 };
1590 inner = inner.add(point.scale(weight_v));
1591 }
1592 row.push(inner);
1593 }
1594 across.push(row);
1595 }
1596 for (k, row) in out.iter_mut().enumerate() {
1597 for (l, cell) in row.iter_mut().enumerate().take(order + 1 - k) {
1598 let mut total = P::zero();
1602 for (i, &weight_u) in du[k].iter().enumerate() {
1603 total = total.add(across[l][i].scale(weight_u));
1604 }
1605 *cell = total;
1606 }
1607 }
1608 Ok(out)
1609}
1610
1611pub fn evaluate_rational_surface<P: Blend>(
1620 ku: &KnotVector,
1621 kv: &KnotVector,
1622 grid: &ControlGrid<Weighted<P>>,
1623 u: f64,
1624 v: f64,
1625 tol: Tolerances,
1626) -> OgeomResult<P> {
1627 let h = evaluate_surface(ku, kv, grid, u, v, tol)?;
1628 if h.weight.abs() <= tol.confusion() {
1629 ogeom_bail!(
1630 Numeric,
1631 "rational surface evaluation produced a vanishing weight"
1632 );
1633 }
1634 Ok(h.point())
1635}
1636
1637pub fn rational_surface_derivatives<P: Blend>(
1650 ku: &KnotVector,
1651 kv: &KnotVector,
1652 grid: &ControlGrid<Weighted<P>>,
1653 u: f64,
1654 v: f64,
1655 order: usize,
1656 tol: Tolerances,
1657) -> OgeomResult<DerivativeGrid<P>> {
1658 let h = surface_derivatives(ku, kv, grid, u, v, order, tol)?;
1659 let w0 = h[0][0].weight;
1660 if w0.abs() <= tol.confusion() {
1661 ogeom_bail!(
1662 Numeric,
1663 "rational surface evaluation produced a vanishing weight"
1664 );
1665 }
1666
1667 let mut s: DerivativeGrid<P> =
1668 core::iter::repeat_with(|| core::iter::repeat_with(P::zero).take(order + 1).collect())
1669 .take(order + 1)
1670 .collect();
1671 for k in 0..=order {
1672 for l in 0..=order - k {
1675 let mut value = h[k][l].scaled;
1676 #[allow(clippy::cast_precision_loss)]
1677 for i in 1..=k {
1678 let c = binomial_coefficient(k, i) as f64;
1679 value = value.sub(s[k - i][l].scale(c * h[i][0].weight));
1680 }
1681 #[allow(clippy::cast_precision_loss)]
1682 for j in 1..=l {
1683 let c = binomial_coefficient(l, j) as f64;
1684 value = value.sub(s[k][l - j].scale(c * h[0][j].weight));
1685 }
1686 #[allow(clippy::cast_precision_loss)]
1687 for i in 1..=k {
1688 let ci = binomial_coefficient(k, i) as f64;
1689 for j in 1..=l {
1690 let cj = binomial_coefficient(l, j) as f64;
1691 value = value.sub(s[k - i][l - j].scale(ci * cj * h[i][j].weight));
1692 }
1693 }
1694 s[k][l] = value.scale(1.0 / w0);
1695 }
1696 }
1697 Ok(s)
1698}
1699
1700#[cfg(test)]
1701#[allow(clippy::unwrap_used)]
1702mod surface_tests {
1703 use super::*;
1704 use approx::assert_relative_eq;
1705
1706 const T: Tolerances = Tolerances::millimetres();
1707
1708 fn patch() -> (KnotVector, KnotVector, ControlGrid<Point>) {
1710 let (nu, nv) = (5, 4);
1711 let mut points = Vec::with_capacity(nu * nv);
1712 for i in 0..nu {
1713 for j in 0..nv {
1714 #[allow(clippy::cast_precision_loss)]
1715 let (x, y) = (i as f64, j as f64);
1716 points.push(Point::new(x, y, (x * 0.7).sin() * (y * 0.5).cos()));
1717 }
1718 }
1719 (
1720 KnotVector::clamped_uniform(3, nu).unwrap(),
1721 KnotVector::clamped_uniform(2, nv).unwrap(),
1722 ControlGrid::new(points, nu, nv).unwrap(),
1723 )
1724 }
1725
1726 #[test]
1727 fn grid_shape_is_checked_on_construction() {
1728 assert!(ControlGrid::new(vec![Point::ORIGIN; 6], 2, 3).is_ok());
1729 assert!(ControlGrid::new(vec![Point::ORIGIN; 6], 3, 3).is_err());
1730 assert!(ControlGrid::new(Vec::<Point>::new(), 0, 3).is_err());
1731 }
1732
1733 #[test]
1734 fn grid_indexing_is_row_major_and_bounds_checked() {
1735 let g = ControlGrid::new(
1736 vec![
1737 Point::new(0.0, 0.0, 0.0),
1738 Point::new(0.0, 1.0, 0.0),
1739 Point::new(0.0, 2.0, 0.0),
1740 Point::new(1.0, 0.0, 0.0),
1741 Point::new(1.0, 1.0, 0.0),
1742 Point::new(1.0, 2.0, 0.0),
1743 ],
1744 2,
1745 3,
1746 )
1747 .unwrap();
1748 assert_eq!(g.get(1, 2), Some(Point::new(1.0, 2.0, 0.0)));
1749 assert_eq!(g.get(0, 1), Some(Point::new(0.0, 1.0, 0.0)));
1750 assert_eq!(g.get(2, 0), None);
1751 assert_eq!(g.get(0, 3), None);
1752 }
1753
1754 #[test]
1755 fn transposing_twice_is_the_identity() {
1756 let (_, _, g) = patch();
1757 let t = g.transposed();
1758 assert_eq!(t.u_count(), g.v_count());
1759 assert_eq!(t.v_count(), g.u_count());
1760 for i in 0..g.u_count() {
1761 for j in 0..g.v_count() {
1762 assert_eq!(t.get(j, i), g.get(i, j));
1763 }
1764 }
1765 assert_eq!(t.transposed(), g);
1766 }
1767
1768 #[test]
1769 fn a_clamped_patch_interpolates_its_corner_control_points() {
1770 let (ku, kv, g) = patch();
1771 let ((u0, u1), (v0, v1)) = (ku.domain(), kv.domain());
1772 let corners = [
1773 (u0, v0, g.get(0, 0).unwrap()),
1774 (u0, v1, g.get(0, g.v_count() - 1).unwrap()),
1775 (u1, v0, g.get(g.u_count() - 1, 0).unwrap()),
1776 (u1, v1, g.get(g.u_count() - 1, g.v_count() - 1).unwrap()),
1777 ];
1778 for (u, v, expected) in corners {
1779 assert!(
1780 evaluate_surface(&ku, &kv, &g, u, v, T)
1781 .unwrap()
1782 .is_equal(expected, T),
1783 "corner ({u}, {v})"
1784 );
1785 }
1786 }
1787
1788 #[test]
1789 fn surface_shape_mismatches_are_refused() {
1790 let (ku, kv, g) = patch();
1791 let wrong = ControlGrid::new(g.points().to_vec(), 4, 5).unwrap();
1792 assert!(evaluate_surface(&ku, &kv, &wrong, 0.5, 0.5, T).is_err());
1793 assert!(evaluate_surface(&ku, &kv, &g, 1.5, 0.5, T).is_err());
1794 assert!(evaluate_surface(&ku, &kv, &g, 0.5, -0.5, T).is_err());
1795 }
1796
1797 #[test]
1798 fn surface_partials_agree_with_finite_differences() {
1799 let (ku, kv, g) = patch();
1800 let h = 1e-6;
1801 for iu in 1..6 {
1802 for iv in 1..6 {
1803 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1804 let d = surface_derivatives(&ku, &kv, &g, u, v, 2, T).unwrap();
1805 assert!(d[0][0].is_equal(evaluate_surface(&ku, &kv, &g, u, v, T).unwrap(), T));
1806
1807 let du = (evaluate_surface(&ku, &kv, &g, u + h, v, T).unwrap()
1808 - evaluate_surface(&ku, &kv, &g, u - h, v, T).unwrap())
1809 * (1.0 / (2.0 * h));
1810 let dv = (evaluate_surface(&ku, &kv, &g, u, v + h, T).unwrap()
1811 - evaluate_surface(&ku, &kv, &g, u, v - h, T).unwrap())
1812 * (1.0 / (2.0 * h));
1813 assert!((d[1][0].to_vector() - du).magnitude() < 1e-5 * du.magnitude().max(1.0));
1814 assert!((d[0][1].to_vector() - dv).magnitude() < 1e-5 * dv.magnitude().max(1.0));
1815
1816 let mixed = (evaluate_surface(&ku, &kv, &g, u + h, v + h, T).unwrap()
1818 - evaluate_surface(&ku, &kv, &g, u + h, v - h, T).unwrap()
1819 - (evaluate_surface(&ku, &kv, &g, u - h, v + h, T).unwrap()
1820 - evaluate_surface(&ku, &kv, &g, u - h, v - h, T).unwrap()))
1821 * (1.0 / (4.0 * h * h));
1822 assert!(
1823 (d[1][1].to_vector() - mixed).magnitude() < 1e-3 * mixed.magnitude().max(1.0),
1824 "mixed partial wrong at ({u}, {v})"
1825 );
1826 }
1827 }
1828 }
1829
1830 fn rational_hemisphere() -> (KnotVector, KnotVector, ControlGrid<Weighted<Point>>) {
1833 let w = core::f64::consts::FRAC_1_SQRT_2;
1834 let rows: [[(Point, f64); 3]; 3] = [
1836 [
1837 (Point::new(1.0, 0.0, 0.0), 1.0),
1838 (Point::new(1.0, 1.0, 0.0), w),
1839 (Point::new(0.0, 1.0, 0.0), 1.0),
1840 ],
1841 [
1842 (Point::new(1.0, 0.0, 1.0), w),
1843 (Point::new(1.0, 1.0, 1.0), w * w),
1844 (Point::new(0.0, 1.0, 1.0), w),
1845 ],
1846 [
1847 (Point::new(0.0, 0.0, 1.0), 1.0),
1848 (Point::new(0.0, 0.0, 1.0), w),
1849 (Point::new(0.0, 0.0, 1.0), 1.0),
1850 ],
1851 ];
1852 let points: Vec<_> = rows
1853 .iter()
1854 .flatten()
1855 .map(|(p, w)| Weighted::new(*p, *w, T).unwrap())
1856 .collect();
1857 (
1858 KnotVector::clamped_uniform(2, 3).unwrap(),
1859 KnotVector::clamped_uniform(2, 3).unwrap(),
1860 ControlGrid::new(points, 3, 3).unwrap(),
1861 )
1862 }
1863
1864 #[test]
1865 fn a_rational_biquadratic_traces_an_exact_sphere() {
1866 let (ku, kv, g) = rational_hemisphere();
1867 for iu in 0..=10 {
1868 for iv in 0..=10 {
1869 let (u, v) = (f64::from(iu) / 10.0, f64::from(iv) / 10.0);
1870 let p = evaluate_rational_surface(&ku, &kv, &g, u, v, T).unwrap();
1871 assert_relative_eq!(
1872 p.to_vector().magnitude(),
1873 1.0,
1874 epsilon = 1e-13,
1875 max_relative = 1e-13
1876 );
1877 }
1878 }
1879 }
1880
1881 #[test]
1882 fn rational_surface_partials_agree_with_finite_differences() {
1883 let (ku, kv, g) = rational_hemisphere();
1884 let h = 1e-6;
1885 let at = |u: f64, v: f64| evaluate_rational_surface(&ku, &kv, &g, u, v, T).unwrap();
1886 for iu in 1..6 {
1887 for iv in 1..6 {
1888 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1889 let d = rational_surface_derivatives(&ku, &kv, &g, u, v, 2, T).unwrap();
1890 assert!(d[0][0].is_equal(at(u, v), T));
1891
1892 let du = (at(u + h, v) - at(u - h, v)) * (1.0 / (2.0 * h));
1893 let dv = (at(u, v + h) - at(u, v - h)) * (1.0 / (2.0 * h));
1894 assert!(
1895 (d[1][0].to_vector() - du).magnitude() < 1e-5 * du.magnitude().max(1.0),
1896 "du wrong at ({u}, {v})"
1897 );
1898 assert!(
1899 (d[0][1].to_vector() - dv).magnitude() < 1e-5 * dv.magnitude().max(1.0),
1900 "dv wrong at ({u}, {v})"
1901 );
1902
1903 let mixed =
1906 (at(u + h, v + h) - at(u + h, v - h) - (at(u - h, v + h) - at(u - h, v - h)))
1907 * (1.0 / (4.0 * h * h));
1908 assert!(
1909 (d[1][1].to_vector() - mixed).magnitude() < 1e-2 * mixed.magnitude().max(1.0),
1910 "mixed partial wrong at ({u}, {v}): {:?} vs {mixed:?}",
1911 d[1][1]
1912 );
1913 }
1914 }
1915 }
1916
1917 #[test]
1918 fn a_spheres_normal_is_radial() {
1919 let (ku, kv, g) = rational_hemisphere();
1922 for iu in 1..8 {
1923 for iv in 1..8 {
1924 let (u, v) = (f64::from(iu) / 8.0, f64::from(iv) / 8.0);
1925 let d = rational_surface_derivatives(&ku, &kv, &g, u, v, 1, T).unwrap();
1926 let radius = d[0][0].to_vector();
1927 let normal = d[1][0].to_vector().cross(d[0][1].to_vector());
1928 assert!(
1929 normal.magnitude() > 1e-6,
1930 "degenerate tangents at ({u}, {v})"
1931 );
1932 let sine =
1933 radius.cross(normal).magnitude() / (radius.magnitude() * normal.magnitude());
1934 assert!(sine < 1e-9, "normal not radial at ({u}, {v}): sine {sine}");
1935 }
1936 }
1937 }
1938
1939 #[test]
1940 fn uniform_weights_reduce_to_the_polynomial_surface() {
1941 let (ku, kv, g) = patch();
1942 let weighted = g.map(|p| Weighted {
1943 scaled: p.scale(2.0),
1944 weight: 2.0,
1945 });
1946 for iu in 0..=6 {
1947 for iv in 0..=6 {
1948 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1949 let plain = evaluate_surface(&ku, &kv, &g, u, v, T).unwrap();
1950 let rational = evaluate_rational_surface(&ku, &kv, &weighted, u, v, T).unwrap();
1951 assert!(plain.is_equal(rational, T));
1952 }
1953 }
1954 }
1955}