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 span = knots.span(value, tol)?;
286 let p = knots.degree();
287 let existing = knots.multiplicity_of(value);
288 if existing + count > p {
289 ogeom_bail!(
290 Construction,
291 "inserting {count} copies of {value} would reach multiplicity {}, above degree {p}",
292 existing + count
293 );
294 }
295
296 let new_knots = knots.with_knot_inserted(value, count)?;
297 let last = control.len() - 1;
298 let k = knots.knots();
299 let (s, r) = (existing, count);
300
301 let mut points: Vec<P> = vec![P::zero(); control.len() + r];
302 points[..=span - p].copy_from_slice(&control[..=span - p]);
305 points[span - s + r..=last + r].copy_from_slice(&control[span - s..=last]);
306
307 let mut window: Vec<P> = (0..=p - s).map(|i| control[span - p + i]).collect();
310 let mut window_start = span - p;
311 for j in 1..=r {
312 window_start = span - p + j;
313 for i in 0..=p - j - s {
314 let left = k[window_start + i];
315 let right = k[i + span + 1];
316 let alpha = (value - left) / (right - left);
317 window[i] = window[i].lerp(window[i + 1], alpha);
318 }
319 points[window_start] = window[0];
320 points[span + r - j - s] = window[p - j - s];
321 }
322
323 if window_start + 1 < span - s {
325 let width = (span - s) - (window_start + 1);
326 points[window_start + 1..span - s].copy_from_slice(&window[1..=width]);
327 }
328
329 Ok((new_knots, points))
330}
331
332pub type Spline<P> = (KnotVector, Vec<P>);
334
335pub type BezierSegment<P> = ((f64, f64), Vec<P>);
337
338pub fn join<P: Blend>(a: &Spline<P>, b: &Spline<P>) -> OgeomResult<Spline<P>> {
353 let ((ak, ac), (bk, bc)) = (a, b);
354 check_shape(ak, ac)?;
355 check_shape(bk, bc)?;
356 let p = ak.degree();
357 if bk.degree() != p {
358 ogeom_bail!(
359 Construction,
360 "cannot join a degree {p} B-spline to a degree {} one",
361 bk.degree()
362 );
363 }
364 if !ak.is_clamped() || !bk.is_clamped() {
365 ogeom_bail!(Construction, "only clamped B-splines join");
366 }
367 let shift = ak.domain_end() - bk.domain_start();
368 let mut knots: Vec<f64> = ak.knots()[..ak.knots().len() - 1].to_vec();
369 knots.extend(bk.knots()[p + 1..].iter().map(|k| k + shift));
370 let mut control: Vec<P> = ac[..ac.len() - 1].to_vec();
371 control.extend_from_slice(bc);
372 Ok((KnotVector::new(knots, p)?, control))
373}
374
375pub fn extend<P: Blend>(
397 knots: &KnotVector,
398 control: &[P],
399 at_end: bool,
400 span: f64,
401 continuity: usize,
402 tol: Tolerances,
403) -> OgeomResult<Spline<P>> {
404 check_shape(knots, control)?;
405 if !knots.is_clamped() {
406 ogeom_bail!(Construction, "only clamped B-splines extend");
407 }
408 if !(span > 0.0 && span.is_finite()) {
409 ogeom_bail!(
410 Construction,
411 "an extension needs a positive, finite span; got {span}"
412 );
413 }
414 if !at_end {
415 let (rk, rc) = reverse(knots, control);
419 let (ek, ec) = extend(&rk, &rc, true, span, continuity, tol)?;
420 let (bk, bc) = reverse(&ek, &ec);
421 let (lo, hi) = knots.domain();
422 return Ok((bk.reparameterized(lo - span, hi)?, bc));
423 }
424 let p = knots.degree();
425 let k = continuity.min(p);
426 let end = knots.domain_end();
427 let jet = derivatives(knots, control, end, k, tol)?;
428 let mut bezier: Vec<P> = Vec::with_capacity(k + 1);
431 for j in 0..=k {
432 let mut b = P::zero();
433 let (mut factorial, mut power) = (1.0_f64, 1.0_f64);
434 for (i, derivative) in jet.iter().enumerate().take(j + 1) {
435 if i > 0 {
436 #[allow(clippy::cast_precision_loss)]
437 {
438 factorial *= i as f64;
439 }
440 power *= span;
441 }
442 #[allow(clippy::cast_precision_loss)]
443 let ratio = binomial_coefficient(j, i) as f64 / binomial_coefficient(k, i) as f64;
444 b = b.add(derivative.scale(ratio * power / factorial));
445 }
446 bezier.push(b);
447 }
448 let mut piece_knots: Vec<f64> = Vec::with_capacity(2 * (k + 1));
449 piece_knots.extend(core::iter::repeat_n(end, k + 1));
450 piece_knots.extend(core::iter::repeat_n(end + span, k + 1));
451 let mut piece: Spline<P> = (KnotVector::new(piece_knots, k)?, bezier);
452 for _ in k..p {
453 piece = elevate_degree(&piece.0, &piece.1, tol)?;
454 }
455 join(&(knots.clone(), control.to_vec()), &piece)
456}
457
458pub fn extend_to<P: Blend>(
468 knots: &KnotVector,
469 control: &[P],
470 at_end: bool,
471 target: P,
472 span: f64,
473 continuity: usize,
474 tol: Tolerances,
475) -> OgeomResult<Spline<P>> {
476 check_shape(knots, control)?;
477 if !knots.is_clamped() {
478 ogeom_bail!(Construction, "only clamped B-splines extend");
479 }
480 if !(span > 0.0 && span.is_finite()) {
481 ogeom_bail!(
482 Construction,
483 "an extension needs a positive, finite span; got {span}"
484 );
485 }
486 if !at_end {
487 let (rk, rc) = reverse(knots, control);
488 let (ek, ec) = extend_to(&rk, &rc, true, target, span, continuity, tol)?;
489 let (bk, bc) = reverse(&ek, &ec);
490 let (lo, hi) = knots.domain();
491 return Ok((bk.reparameterized(lo - span, hi)?, bc));
492 }
493 let mut base: Spline<P> = (knots.clone(), control.to_vec());
494 let k = continuity.min(base.0.degree());
495 let n = k + 1;
496 while base.0.degree() < n {
497 base = elevate_degree(&base.0, &base.1, tol)?;
498 }
499 let end = base.0.domain_end();
500 let jet = derivatives(&base.0, &base.1, end, k, tol)?;
501 let mut bezier: Vec<P> = Vec::with_capacity(n + 1);
504 for j in 0..=k {
505 let mut b = P::zero();
506 let (mut factorial, mut power) = (1.0_f64, 1.0_f64);
507 for (i, derivative) in jet.iter().enumerate().take(j + 1) {
508 if i > 0 {
509 #[allow(clippy::cast_precision_loss)]
510 {
511 factorial *= i as f64;
512 }
513 power *= span;
514 }
515 #[allow(clippy::cast_precision_loss)]
516 let ratio = binomial_coefficient(j, i) as f64 / binomial_coefficient(n, i) as f64;
517 b = b.add(derivative.scale(ratio * power / factorial));
518 }
519 bezier.push(b);
520 }
521 bezier.push(target);
522 let mut piece_knots: Vec<f64> = Vec::with_capacity(2 * (n + 1));
523 piece_knots.extend(core::iter::repeat_n(end, n + 1));
524 piece_knots.extend(core::iter::repeat_n(end + span, n + 1));
525 let mut piece: Spline<P> = (KnotVector::new(piece_knots, n)?, bezier);
526 for _ in n..base.0.degree() {
527 piece = elevate_degree(&piece.0, &piece.1, tol)?;
528 }
529 join(&base, &piece)
530}
531
532pub fn split<P: Blend>(
542 knots: &KnotVector,
543 control: &[P],
544 u: f64,
545 tol: Tolerances,
546) -> OgeomResult<(Spline<P>, Spline<P>)> {
547 check_shape(knots, control)?;
548 let (start, end) = knots.domain();
549 if u <= start + tol.parametric() || u >= end - tol.parametric() {
550 ogeom_bail!(
551 Domain,
552 "cannot split at {u}, an end of the domain [{start}, {end}]"
553 );
554 }
555 let p = knots.degree();
556 let existing = knots.multiplicity_of(u);
557 let (refined, points) = insert_knot(knots, control, u, p - existing, tol)?;
558
559 let cut = refined.knots().partition_point(|k| *k < u);
561 let left_points = points[..cut].to_vec();
562 let right_points = points[cut - 1..].to_vec();
563
564 let mut left_knots = refined.knots()[..cut + p].to_vec();
565 left_knots.push(u);
566 let mut right_knots = vec![u];
567 right_knots.extend_from_slice(&refined.knots()[cut..]);
568
569 Ok((
570 (KnotVector::new(left_knots, p)?, left_points),
571 (KnotVector::new(right_knots, p)?, right_points),
572 ))
573}
574
575pub fn to_bezier_segments<P: Blend>(
586 knots: &KnotVector,
587 control: &[P],
588 tol: Tolerances,
589) -> OgeomResult<Vec<BezierSegment<P>>> {
590 check_shape(knots, control)?;
591 let p = knots.degree();
592 let end = knots.domain().1;
593
594 let clamp_start = |knots: &KnotVector, control: &[P]| -> OgeomResult<Spline<P>> {
600 let (start, _) = knots.domain();
601 let needed = p.saturating_sub(knots.multiplicity_of(start));
602 insert_knot(knots, control, start, needed, tol)
603 };
604 let (mut current_knots, mut current_points) = clamp_start(knots, control)?;
605 if current_knots.multiplicity_of(end) < p {
606 let (reversed_knots, reversed_points) = reverse(¤t_knots, ¤t_points);
607 let (reversed_knots, reversed_points) = clamp_start(&reversed_knots, &reversed_points)?;
608 (current_knots, current_points) = reverse(&reversed_knots, &reversed_points);
609 }
610 let (start, end) = current_knots.domain();
611 for (value, multiplicity) in current_knots.clone().distinct() {
612 if value <= start || value >= end {
613 continue;
614 }
615 let needed = p.saturating_sub(multiplicity);
616 if needed > 0 {
617 let (k, c) = insert_knot(¤t_knots, ¤t_points, value, needed, tol)?;
618 current_knots = k;
619 current_points = c;
620 }
621 }
622
623 let breaks: Vec<f64> = core::iter::once(start)
624 .chain(
625 current_knots
626 .distinct()
627 .into_iter()
628 .filter(|(v, _)| *v > start && *v < end)
629 .map(|(v, _)| v),
630 )
631 .chain(core::iter::once(end))
632 .collect();
633
634 let mut out = Vec::with_capacity(breaks.len() - 1);
636 for w in breaks.windows(2) {
637 let span = current_knots.span(f64::midpoint(w[0], w[1]), tol)?;
638 out.push(((w[0], w[1]), current_points[span - p..=span].to_vec()));
639 }
640 Ok(out)
641}
642
643pub fn elevate_degree<P: Blend>(
655 knots: &KnotVector,
656 control: &[P],
657 tol: Tolerances,
658) -> OgeomResult<Spline<P>> {
659 check_shape(knots, control)?;
660 let p = knots.degree();
661 let segments = to_bezier_segments(knots, control, tol)?;
662
663 let mut points: Vec<P> = Vec::with_capacity(segments.len() * (p + 1) + 1);
664 let mut new_knots: Vec<f64> = Vec::new();
665
666 for (index, ((a, b), segment)) in segments.iter().enumerate() {
667 let mut elevated: Vec<P> = Vec::with_capacity(p + 2);
669 elevated.push(segment[0]);
670 #[allow(clippy::cast_precision_loss)]
671 for i in 1..=p {
672 let t = i as f64 / (p + 1) as f64;
673 elevated.push(segment[i - 1].lerp(segment[i], 1.0 - t));
674 }
675 elevated.push(segment[p]);
676
677 if index == 0 {
678 points.extend_from_slice(&elevated);
679 new_knots.extend(core::iter::repeat_n(*a, p + 2));
680 } else {
681 points.extend_from_slice(&elevated[1..]);
683 new_knots.extend(core::iter::repeat_n(*a, p + 1));
684 }
685 if index == segments.len() - 1 {
686 new_knots.extend(core::iter::repeat_n(*b, p + 2));
687 }
688 }
689
690 for (value, multiplicity) in knots.distinct() {
694 let (start, end) = knots.domain();
695 if value <= start || value >= end {
696 continue;
697 }
698 for _ in multiplicity..p {
699 (new_knots, points) = remove_knot_once(&new_knots, &points, p + 1, value);
700 }
701 }
702
703 Ok((KnotVector::new(new_knots, p + 1)?, points))
704}
705
706fn remove_knot_once<P: Blend>(
710 knots: &[f64],
711 control: &[P],
712 p: usize,
713 u: f64,
714) -> (Vec<f64>, Vec<P>) {
715 let Some(r) = knots.iter().rposition(|k| *k == u) else {
716 return (knots.to_vec(), control.to_vec());
717 };
718 let left = knots.iter().filter(|k| **k == u).count() - 1;
719 let mut reduced = knots.to_vec();
720 reduced.remove(r);
721 let k = r - 1;
725 let (lo, hi) = (k + 1 - p, k - left);
726 let alpha = |i: usize| (u - reduced[i]) / (reduced[i + p] - reduced[i]);
727 let mut q: Vec<P> = Vec::with_capacity(control.len() - 1);
728 q.extend_from_slice(&control[..lo]);
729 let mut forward: Vec<P> = Vec::with_capacity(hi - lo);
730 let mut previous = control[lo - 1];
731 for (i, point) in control.iter().enumerate().take(hi).skip(lo) {
732 let a = alpha(i);
733 previous = point.sub(previous.scale(1.0 - a)).scale(1.0 / a);
734 forward.push(previous);
735 }
736 let mut backward: Vec<P> = vec![P::zero(); hi - lo];
737 let mut next = control[hi + 1];
738 for i in (lo + 1..=hi).rev() {
739 let a = alpha(i);
740 next = control[i].sub(next.scale(a)).scale(1.0 / (1.0 - a));
741 backward[i - 1 - lo] = next;
742 }
743 let middle = (hi - lo) / 2;
744 for j in 0..hi - lo {
745 q.push(if j < middle { forward[j] } else { backward[j] });
746 }
747 q.extend_from_slice(&control[hi + 1..]);
748 (reduced, q)
749}
750
751#[must_use]
753pub fn reverse<P: Blend>(knots: &KnotVector, control: &[P]) -> Spline<P> {
754 let mut points = control.to_vec();
755 points.reverse();
756 (knots.reversed(), points)
757}
758
759pub fn evaluate_rational<P: Blend>(
767 knots: &KnotVector,
768 control: &[Weighted<P>],
769 u: f64,
770 tol: Tolerances,
771) -> OgeomResult<P> {
772 let h = evaluate(knots, control, u, tol)?;
773 if h.weight.abs() <= tol.confusion() {
774 ogeom_bail!(Numeric, "rational evaluation produced a vanishing weight");
775 }
776 Ok(h.point())
777}
778
779pub fn rational_derivatives<P: Blend>(
789 knots: &KnotVector,
790 control: &[Weighted<P>],
791 u: f64,
792 n: usize,
793 tol: Tolerances,
794) -> OgeomResult<Vec<P>> {
795 let homogeneous = derivatives(knots, control, u, n, tol)?;
796 if homogeneous[0].weight.abs() <= tol.confusion() {
797 ogeom_bail!(Numeric, "rational evaluation produced a vanishing weight");
798 }
799
800 let mut out: Vec<P> = Vec::with_capacity(n + 1);
802 for (order, term) in homogeneous.iter().enumerate() {
803 let mut value = term.scaled;
804 for i in 1..=order {
805 #[allow(clippy::cast_precision_loss)]
806 let binomial = binomial_coefficient(order, i) as f64;
807 value = value.sub(out[order - i].scale(binomial * homogeneous[i].weight));
808 }
809 out.push(value.scale(1.0 / homogeneous[0].weight));
810 }
811 Ok(out)
812}
813
814#[must_use]
817pub fn binomial_coefficient(n: usize, k: usize) -> u64 {
818 if k > n {
819 return 0;
820 }
821 let k = k.min(n - k);
822 let mut result = 1_u64;
823 for i in 0..k {
824 result = result * (n - i) as u64 / (i as u64 + 1);
825 }
826 result
827}
828
829#[cfg(test)]
830#[allow(clippy::unwrap_used)]
831mod join_tests {
832 use super::*;
833 use crate::Point;
834
835 #[test]
836 fn a_joined_spline_evaluates_as_its_two_halves_did() {
837 let tol = Tolerances::millimetres();
838 let control: Vec<Point> = (0..6)
839 .map(|i| Point::new(f64::from(i), f64::from(i * i % 5), 0.0))
840 .collect();
841 let knots = KnotVector::clamped_uniform(3, control.len()).unwrap();
842 let ((lk, lc), (rk, rc)) = split(&knots, &control, 0.4, tol).unwrap();
843 let (jk, jc) = join(&(lk, lc), &(rk, rc)).unwrap();
844 assert_eq!(
845 jk.domain(),
846 knots.domain(),
847 "the domain is the two laid end to end"
848 );
849 assert_eq!(
850 jc.len() + 3 + 1,
851 jk.knots().len(),
852 "the knots fit the controls"
853 );
854 for i in 0..=20 {
855 let u = f64::from(i) / 20.0;
856 let before = evaluate(&knots, &control, u, tol).unwrap();
857 let after = evaluate(&jk, &jc, u, tol).unwrap();
858 assert!(
859 before.is_equal(after, tol),
860 "at {u}: {before:?} became {after:?}"
861 );
862 }
863 }
864}
865
866#[cfg(test)]
867#[allow(clippy::unwrap_used)]
868mod tests {
869 use super::*;
870 use approx::assert_relative_eq;
871
872 const T: Tolerances = Tolerances::millimetres();
873
874 #[test]
878 fn an_extension_continues_the_curve_to_its_order() {
879 let knots = KnotVector::clamped_uniform(3, 6).unwrap();
880 let control = vec![
881 Point::new(0.0, 0.0, 0.0),
882 Point::new(1.0, 2.0, 0.5),
883 Point::new(2.5, 1.0, -0.5),
884 Point::new(4.0, 3.0, 1.0),
885 Point::new(5.0, 0.5, 0.0),
886 Point::new(6.0, 2.0, 2.0),
887 ];
888 let (lo, hi) = knots.domain();
889 for at_end in [true, false] {
890 let (ek, ec) = extend(&knots, &control, at_end, 0.4, 2, T).unwrap();
891 let (elo, ehi) = ek.domain();
892 if at_end {
893 assert!((elo - lo).abs() < 1e-12 && (ehi - (hi + 0.4)).abs() < 1e-12);
894 } else {
895 assert!((elo - (lo - 0.4)).abs() < 1e-12 && (ehi - hi).abs() < 1e-12);
896 }
897 for i in 0..=10 {
898 let u = lo + (hi - lo) * f64::from(i) / 10.0;
899 let was = evaluate(&knots, &control, u, T).unwrap();
900 let now = evaluate(&ek, &ec, u, T).unwrap();
901 assert!(
902 was.distance(now) < 1e-9,
903 "the original run at {u}: {was:?} vs {now:?}"
904 );
905 }
906 let join_at = if at_end { hi } else { lo };
909 let step = if at_end { 1e-7 } else { -1e-7 };
910 let inside = derivatives(&knots, &control, join_at - step, 2, T).unwrap();
911 let outside = derivatives(&ek, &ec, join_at + step, 2, T).unwrap();
912 for order in 0..=2 {
913 let (a, b) = (inside[order], outside[order]);
914 let gap = a.to_vector().sub(b.to_vector()).magnitude();
915 let scale = a.to_vector().magnitude().max(1.0);
916 assert!(
917 gap < scale * 1e-4,
918 "order {order} across the join: {a:?} vs {b:?}"
919 );
920 }
921 }
922 }
923
924 fn cubic_curve() -> (KnotVector, Vec<Point>) {
925 let control = vec![
926 Point::new(0.0, 0.0, 0.0),
927 Point::new(1.0, 2.0, 0.0),
928 Point::new(3.0, 3.0, 1.0),
929 Point::new(5.0, 1.0, 2.0),
930 Point::new(6.0, -1.0, 1.0),
931 Point::new(8.0, 0.0, 0.0),
932 ];
933 (
934 KnotVector::clamped_uniform(3, control.len()).unwrap(),
935 control,
936 )
937 }
938
939 fn sample(knots: &KnotVector, control: &[Point], n: usize) -> Vec<Point> {
940 let (a, b) = knots.domain();
941 (0..=n)
942 .map(|i| {
943 #[allow(clippy::cast_precision_loss)]
944 let u = a + (b - a) * (i as f64 / n as f64);
945 evaluate(knots, control, u, T).unwrap()
946 })
947 .collect()
948 }
949
950 #[test]
951 fn a_clamped_curve_interpolates_its_end_points() {
952 let (k, c) = cubic_curve();
953 let (a, b) = k.domain();
954 assert!(evaluate(&k, &c, a, T).unwrap().is_equal(c[0], T));
955 assert!(evaluate(&k, &c, b, T).unwrap().is_equal(c[c.len() - 1], T));
956 }
957
958 #[test]
959 fn de_boor_agrees_with_the_basis_function_sum() {
960 let (k, c) = cubic_curve();
962 for i in 0..=50 {
963 let u = f64::from(i) / 50.0;
964 let span = k.span(u, T).unwrap();
965 let basis = k.basis(span, u);
966 let mut sum = Vector::ZERO;
967 for j in 0..=k.degree() {
968 sum += c[span - k.degree() + j].to_vector() * basis[j];
969 }
970 let de_boor = evaluate(&k, &c, u, T).unwrap();
971 assert!(de_boor.is_equal(Point::from_vector(sum), T), "at u = {u}");
972 }
973 }
974
975 #[test]
976 fn shape_mismatches_and_out_of_domain_parameters_are_refused() {
977 let (k, c) = cubic_curve();
978 assert!(
979 evaluate(&k, &c[..3], 0.5, T).is_err(),
980 "too few control points"
981 );
982 assert!(evaluate(&k, &c, -0.1, T).is_err());
983 assert!(evaluate(&k, &c, 1.1, T).is_err());
984 }
985
986 #[test]
987 fn derivatives_agree_with_finite_differences() {
988 let (k, c) = cubic_curve();
989 let h = 1e-6;
990 for i in 1..20 {
991 let u = f64::from(i) / 20.0;
992 let d = derivatives(&k, &c, u, 2, T).unwrap();
993 assert!(d[0].is_equal(evaluate(&k, &c, u, T).unwrap(), T));
994
995 let ahead = evaluate(&k, &c, u + h, T).unwrap();
996 let behind = evaluate(&k, &c, u - h, T).unwrap();
997 let numeric = (ahead - behind) * (1.0 / (2.0 * h));
998 assert!(
999 (d[1].to_vector() - numeric).magnitude() < 1e-5,
1000 "first derivative disagrees at {u}"
1001 );
1002 }
1003 }
1004
1005 #[test]
1006 fn knot_insertion_does_not_move_the_curve() {
1007 let (k, c) = cubic_curve();
1008 let before = sample(&k, &c, 100);
1009 for (value, count) in [(0.25, 1), (0.5, 2), (0.75, 3), (0.1, 1)] {
1010 let (k2, c2) = insert_knot(&k, &c, value, count, T).unwrap();
1011 assert_eq!(c2.len(), c.len() + count);
1012 assert_eq!(k2.multiplicity_of(value), k.multiplicity_of(value) + count);
1013 let after = sample(&k2, &c2, 100);
1014 for (a, b) in before.iter().zip(&after) {
1015 assert!(
1016 a.is_equal(*b, T),
1017 "inserting {count} at {value} moved the curve"
1018 );
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn repeated_insertion_matches_a_single_multiple_insertion() {
1025 let (k, c) = cubic_curve();
1026 let (ka, ca) = insert_knot(&k, &c, 0.4, 3, T).unwrap();
1027
1028 let (k1, c1) = insert_knot(&k, &c, 0.4, 1, T).unwrap();
1029 let (k2, c2) = insert_knot(&k1, &c1, 0.4, 1, T).unwrap();
1030 let (kb, cb) = insert_knot(&k2, &c2, 0.4, 1, T).unwrap();
1031
1032 assert_eq!(ka.knots(), kb.knots());
1033 for (a, b) in ca.iter().zip(&cb) {
1034 assert!(a.is_equal(*b, T));
1035 }
1036 }
1037
1038 #[test]
1039 fn insertion_beyond_the_degree_is_refused() {
1040 let (k, c) = cubic_curve();
1041 assert!(insert_knot(&k, &c, 0.5, 4, T).is_err());
1042 assert!(insert_knot(&k, &c, 0.5, 3, T).is_ok());
1043 assert!(
1044 insert_knot(&k, &c, 2.0, 1, T).is_err(),
1045 "outside the domain"
1046 );
1047 }
1048
1049 #[test]
1050 fn splitting_reproduces_both_halves_of_the_original() {
1051 let (k, c) = cubic_curve();
1052 let cut = 0.4;
1053 let ((lk, lc), (rk, rc)) = split(&k, &c, cut, T).unwrap();
1054
1055 assert_relative_eq!(lk.domain().1, cut, epsilon = 1e-15);
1056 assert_relative_eq!(rk.domain().0, cut, epsilon = 1e-15);
1057 assert!(lk.is_clamped() && rk.is_clamped());
1058
1059 for i in 0..=40 {
1060 let t = f64::from(i) / 40.0;
1061 let left_u = lk.domain().0 + (cut - lk.domain().0) * t;
1062 let right_u = cut + (rk.domain().1 - cut) * t;
1063 assert!(
1064 evaluate(&lk, &lc, left_u, T)
1065 .unwrap()
1066 .is_equal(evaluate(&k, &c, left_u, T).unwrap(), T),
1067 "left half diverges at {left_u}"
1068 );
1069 assert!(
1070 evaluate(&rk, &rc, right_u, T)
1071 .unwrap()
1072 .is_equal(evaluate(&k, &c, right_u, T).unwrap(), T),
1073 "right half diverges at {right_u}"
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn splitting_at_an_end_of_the_domain_is_refused() {
1080 let (k, c) = cubic_curve();
1081 assert!(split(&k, &c, 0.0, T).is_err());
1082 assert!(split(&k, &c, 1.0, T).is_err());
1083 }
1084
1085 #[test]
1086 fn an_unclamped_curve_decomposes_and_elevates_unchanged() {
1087 let knots = KnotVector::new((0..10).map(f64::from).collect(), 3).unwrap();
1089 let control = vec![
1090 Point::new(0.0, 0.0, 0.0),
1091 Point::new(5.0, 1.0, 0.0),
1092 Point::new(-2.0, 3.0, 1.0),
1093 Point::new(7.0, 2.0, 2.0),
1094 Point::new(1.0, -1.0, 1.0),
1095 Point::new(3.0, 0.0, 0.0),
1096 ];
1097 let segments = to_bezier_segments(&knots, &control, T).unwrap();
1098 assert_eq!(segments.len(), 3);
1099 for ((a, b), points) in &segments {
1100 let bezier = KnotVector::clamped_uniform(3, points.len()).unwrap();
1101 for s in [0.0, 0.25, 0.5, 1.0] {
1102 let on_segment = evaluate(&bezier, points, s, T).unwrap();
1103 let on_curve = evaluate(&knots, &control, a + (b - a) * s, T).unwrap();
1104 assert!(on_segment.distance(on_curve) < 1e-12, "[{a}, {b}] at {s}");
1105 }
1106 }
1107 let (raised, points) = elevate_degree(&knots, &control, T).unwrap();
1108 assert_eq!(raised.domain(), knots.domain());
1109 for i in 0..=30 {
1110 let u = 3.0 + f64::from(i) / 10.0;
1111 let moved = evaluate(&raised, &points, u, T)
1112 .unwrap()
1113 .distance(evaluate(&knots, &control, u, T).unwrap());
1114 assert!(moved < 1e-12, "elevation moved the curve {moved} at {u}");
1115 }
1116 }
1117
1118 #[test]
1119 fn bezier_decomposition_covers_the_curve_exactly() {
1120 let (k, c) = cubic_curve();
1121 let segments = to_bezier_segments(&k, &c, T).unwrap();
1122 assert_eq!(segments.len(), 3);
1124 for (_, points) in &segments {
1125 assert_eq!(points.len(), k.degree() + 1);
1126 }
1127
1128 for ((a, b), points) in &segments {
1131 let bezier = KnotVector::clamped_uniform(k.degree(), points.len())
1132 .unwrap()
1133 .reparameterized(*a, *b)
1134 .unwrap();
1135 for i in 0..=20 {
1136 let u = a + (b - a) * (f64::from(i) / 20.0);
1137 assert!(
1138 evaluate(&bezier, points, u, T)
1139 .unwrap()
1140 .is_equal(evaluate(&k, &c, u, T).unwrap(), T),
1141 "segment [{a}, {b}] diverges at {u}"
1142 );
1143 }
1144 }
1145 }
1146
1147 #[test]
1148 fn degree_elevation_does_not_move_the_curve() {
1149 let (k, c) = cubic_curve();
1150 let before = sample(&k, &c, 100);
1151 let (k2, c2) = elevate_degree(&k, &c, T).unwrap();
1152 assert_eq!(k2.degree(), k.degree() + 1);
1153 assert_eq!(k2.domain(), k.domain());
1154
1155 let after = sample(&k2, &c2, 100);
1156 for (a, b) in before.iter().zip(&after) {
1157 assert!(a.is_equal(*b, T), "elevation moved the curve");
1158 }
1159 }
1160
1161 #[test]
1162 fn elevation_keeps_the_continuity_at_every_knot() {
1163 let (k, c) = cubic_curve();
1164 let (k2, c2) = elevate_degree(&k, &c, T).unwrap();
1165 let interior = |v: &KnotVector| -> Vec<(f64, usize)> {
1166 let (a, b) = v.domain();
1167 v.distinct()
1168 .into_iter()
1169 .filter(|(x, _)| *x > a && *x < b)
1170 .collect()
1171 };
1172 let raised: Vec<(f64, usize)> = interior(&k).into_iter().map(|(x, m)| (x, m + 1)).collect();
1173 assert_eq!(interior(&k2), raised);
1174 let before = sample(&k, &c, 100);
1175 for (a, b) in before.iter().zip(&sample(&k2, &c2, 100)) {
1176 assert!(a.distance(*b) < 1e-12, "elevation moved the curve");
1177 }
1178 for (x, _) in interior(&k2) {
1180 let left = derivatives(&k2, &c2, x - 1e-9, 2, T).unwrap()[2];
1181 let right = derivatives(&k2, &c2, x + 1e-9, 2, T).unwrap()[2];
1182 assert!(
1183 left.distance(right) < 1e-5,
1184 "{left:?} against {right:?} at {x}"
1185 );
1186 }
1187 }
1188
1189 #[test]
1190 fn elevation_twice_is_still_the_same_curve() {
1191 let (k, c) = cubic_curve();
1192 let before = sample(&k, &c, 60);
1193 let (k1, c1) = elevate_degree(&k, &c, T).unwrap();
1194 let (k2, c2) = elevate_degree(&k1, &c1, T).unwrap();
1195 assert_eq!(k2.degree(), 5);
1196 for (a, b) in before.iter().zip(&sample(&k2, &c2, 60)) {
1197 assert!(a.is_equal(*b, T));
1198 }
1199 }
1200
1201 #[test]
1202 fn reversal_traverses_the_same_points_backwards() {
1203 let (k, c) = cubic_curve();
1204 let (rk, rc) = reverse(&k, &c);
1205 let (a, b) = k.domain();
1206 for i in 0..=40 {
1207 let t = f64::from(i) / 40.0;
1208 let forward = evaluate(&k, &c, a + (b - a) * t, T).unwrap();
1209 let backward = evaluate(&rk, &rc, a + (b - a) * (1.0 - t), T).unwrap();
1210 assert!(forward.is_equal(backward, T), "at t = {t}");
1211 }
1212 }
1213
1214 fn quarter_circle() -> (KnotVector, Vec<Weighted<Point>>) {
1217 let w = core::f64::consts::FRAC_1_SQRT_2;
1218 let control = vec![
1219 Weighted::new(Point::new(1.0, 0.0, 0.0), 1.0, T).unwrap(),
1220 Weighted::new(Point::new(1.0, 1.0, 0.0), w, T).unwrap(),
1221 Weighted::new(Point::new(0.0, 1.0, 0.0), 1.0, T).unwrap(),
1222 ];
1223 (KnotVector::clamped_uniform(2, 3).unwrap(), control)
1224 }
1225
1226 #[test]
1227 fn a_rational_quadratic_traces_an_exact_circular_arc() {
1228 let (k, c) = quarter_circle();
1229 for i in 0..=100 {
1230 let u = f64::from(i) / 100.0;
1231 let p = evaluate_rational(&k, &c, u, T).unwrap();
1232 assert_relative_eq!(p.to_vector().magnitude(), 1.0, epsilon = 1e-14);
1235 assert_relative_eq!(p.z, 0.0, epsilon = 1e-15);
1236 }
1237 assert!(
1238 evaluate_rational(&k, &c, 0.0, T)
1239 .unwrap()
1240 .is_equal(Point::new(1.0, 0.0, 0.0), T)
1241 );
1242 assert!(
1243 evaluate_rational(&k, &c, 1.0, T)
1244 .unwrap()
1245 .is_equal(Point::new(0.0, 1.0, 0.0), T)
1246 );
1247 }
1248
1249 #[test]
1250 fn rational_derivatives_agree_with_finite_differences() {
1251 let (k, c) = quarter_circle();
1252 let h = 1e-6;
1253 for i in 1..20 {
1254 let u = f64::from(i) / 20.0;
1255 let d = rational_derivatives(&k, &c, u, 2, T).unwrap();
1256 assert!(d[0].is_equal(evaluate_rational(&k, &c, u, T).unwrap(), T));
1257
1258 let ahead = evaluate_rational(&k, &c, u + h, T).unwrap();
1259 let behind = evaluate_rational(&k, &c, u - h, T).unwrap();
1260 let numeric = (ahead - behind) * (1.0 / (2.0 * h));
1261 assert!(
1262 (d[1].to_vector() - numeric).magnitude() < 1e-5,
1263 "at u = {u}: {:?} vs {numeric:?}",
1264 d[1]
1265 );
1266 }
1267 }
1268
1269 #[test]
1270 fn the_tangent_of_a_circular_arc_is_perpendicular_to_its_radius() {
1271 let (k, c) = quarter_circle();
1272 for i in 0..=20 {
1273 let u = f64::from(i) / 20.0;
1274 let d = rational_derivatives(&k, &c, u, 1, T).unwrap();
1275 let radius = d[0].to_vector();
1276 let tangent = d[1].to_vector();
1277 assert!(
1278 radius.dot(tangent).abs() < 1e-12,
1279 "not perpendicular at {u}: {}",
1280 radius.dot(tangent)
1281 );
1282 }
1283 }
1284
1285 #[test]
1286 fn knot_insertion_preserves_a_rational_curve_too() {
1287 let (k, c) = quarter_circle();
1288 let (k2, c2) = insert_knot(&k, &c, 0.5, 1, T).unwrap();
1289 for i in 0..=50 {
1290 let u = f64::from(i) / 50.0;
1291 let a = evaluate_rational(&k, &c, u, T).unwrap();
1292 let b = evaluate_rational(&k2, &c2, u, T).unwrap();
1293 assert!(a.is_equal(b, T), "at {u}");
1294 assert_relative_eq!(b.to_vector().magnitude(), 1.0, epsilon = 1e-14);
1295 }
1296 }
1297
1298 #[test]
1299 fn degenerate_weights_are_refused() {
1300 assert!(Weighted::new(Point::ORIGIN, 0.0, T).is_err());
1301 assert!(Weighted::new(Point::ORIGIN, -1.0, T).is_err());
1302 assert!(Weighted::new(Point::ORIGIN, f64::NAN, T).is_err());
1303 assert!(Weighted::new(Point::ORIGIN, f64::INFINITY, T).is_err());
1304 assert!(Weighted::new(Point::ORIGIN, 2.0, T).is_ok());
1305 }
1306
1307 #[test]
1308 fn weighted_round_trips_through_its_homogeneous_form() {
1309 let p = Point::new(3.0, -1.0, 2.0);
1310 let w = Weighted::new(p, 2.5, T).unwrap();
1311 assert!(w.point().is_equal(p, T));
1312 assert!(w.scaled.is_equal(Point::new(7.5, -2.5, 5.0), T));
1313 }
1314
1315 #[test]
1316 fn binomial_coefficients() {
1317 assert_eq!(binomial_coefficient(0, 0), 1);
1318 assert_eq!(binomial_coefficient(5, 0), 1);
1319 assert_eq!(binomial_coefficient(5, 5), 1);
1320 assert_eq!(binomial_coefficient(5, 2), 10);
1321 assert_eq!(binomial_coefficient(10, 5), 252);
1322 assert_eq!(binomial_coefficient(3, 4), 0);
1323 }
1324
1325 #[test]
1326 fn scalar_and_planar_control_points_work_too() {
1327 let k = KnotVector::clamped_uniform(2, 4).unwrap();
1330 let scalars = vec![0.0_f64, 1.0, 3.0, 2.0];
1331 assert_relative_eq!(evaluate(&k, &scalars, 0.0, T).unwrap(), 0.0);
1332 assert_relative_eq!(evaluate(&k, &scalars, 1.0, T).unwrap(), 2.0);
1333
1334 let planar = vec![
1335 Point2::new(0.0, 0.0),
1336 Point2::new(1.0, 2.0),
1337 Point2::new(3.0, 1.0),
1338 Point2::new(4.0, 0.0),
1339 ];
1340 assert!(
1341 evaluate(&k, &planar, 0.0, T)
1342 .unwrap()
1343 .is_equal(planar[0], T)
1344 );
1345 assert!(
1346 evaluate(&k, &planar, 1.0, T)
1347 .unwrap()
1348 .is_equal(planar[3], T)
1349 );
1350 }
1351}
1352
1353#[derive(Debug, Clone, PartialEq)]
1360pub struct ControlGrid<P> {
1361 points: Vec<P>,
1362 u_count: usize,
1363 v_count: usize,
1364}
1365
1366impl<P: Blend> ControlGrid<P> {
1367 pub fn new(points: Vec<P>, u_count: usize, v_count: usize) -> OgeomResult<Self> {
1374 if u_count == 0 || v_count == 0 {
1375 ogeom_bail!(Dimension, "control grid must be at least 1x1");
1376 }
1377 if points.len() != u_count * v_count {
1378 ogeom_bail!(
1379 Dimension,
1380 "a {u_count}x{v_count} grid needs {} points, got {}",
1381 u_count * v_count,
1382 points.len()
1383 );
1384 }
1385 Ok(Self {
1386 points,
1387 u_count,
1388 v_count,
1389 })
1390 }
1391
1392 #[must_use]
1394 pub const fn u_count(&self) -> usize {
1395 self.u_count
1396 }
1397
1398 #[must_use]
1400 pub const fn v_count(&self) -> usize {
1401 self.v_count
1402 }
1403
1404 #[must_use]
1406 pub fn get(&self, i: usize, j: usize) -> Option<P> {
1407 if i >= self.u_count || j >= self.v_count {
1408 return None;
1409 }
1410 self.points.get(i * self.v_count + j).copied()
1411 }
1412
1413 #[must_use]
1415 pub fn points(&self) -> &[P] {
1416 &self.points
1417 }
1418
1419 #[must_use]
1421 pub fn transposed(&self) -> Self {
1422 let mut points = Vec::with_capacity(self.points.len());
1423 for j in 0..self.v_count {
1424 for i in 0..self.u_count {
1425 points.push(self.points[i * self.v_count + j]);
1426 }
1427 }
1428 Self {
1429 points,
1430 u_count: self.v_count,
1431 v_count: self.u_count,
1432 }
1433 }
1434
1435 #[must_use]
1437 pub fn map<Q: Blend>(&self, f: impl Fn(P) -> Q) -> ControlGrid<Q> {
1438 ControlGrid {
1439 points: self.points.iter().map(|p| f(*p)).collect(),
1440 u_count: self.u_count,
1441 v_count: self.v_count,
1442 }
1443 }
1444}
1445
1446fn check_grid_shape<P>(ku: &KnotVector, kv: &KnotVector, grid: &ControlGrid<P>) -> OgeomResult<()> {
1448 if grid.u_count != ku.control_point_count() || grid.v_count != kv.control_point_count() {
1449 ogeom_bail!(
1450 Dimension,
1451 "knot vectors describe a {}x{} grid, got {}x{}",
1452 ku.control_point_count(),
1453 kv.control_point_count(),
1454 grid.u_count,
1455 grid.v_count
1456 );
1457 }
1458 Ok(())
1459}
1460
1461pub fn evaluate_surface<P: Blend>(
1473 ku: &KnotVector,
1474 kv: &KnotVector,
1475 grid: &ControlGrid<P>,
1476 u: f64,
1477 v: f64,
1478 tol: Tolerances,
1479) -> OgeomResult<P> {
1480 check_grid_shape(ku, kv, grid)?;
1481 let (p, q) = (ku.degree(), kv.degree());
1482 let (su, sv) = (ku.span(u, tol)?, kv.span(v, tol)?);
1483 let (nu, nv) = (ku.basis(su, u), kv.basis(sv, v));
1484
1485 let mut total = P::zero();
1486 for (i, &weight_u) in nu.iter().enumerate() {
1487 let mut row = P::zero();
1490 for (j, &weight_v) in nv.iter().enumerate() {
1491 let Some(point) = grid.get(su - p + i, sv - q + j) else {
1492 ogeom_bail!(Dimension, "control grid index out of range");
1493 };
1494 row = row.add(point.scale(weight_v));
1495 }
1496 total = total.add(row.scale(weight_u));
1497 }
1498 Ok(total)
1499}
1500
1501pub fn surface_derivatives<P: Blend>(
1511 ku: &KnotVector,
1512 kv: &KnotVector,
1513 grid: &ControlGrid<P>,
1514 u: f64,
1515 v: f64,
1516 order: usize,
1517 tol: Tolerances,
1518) -> OgeomResult<DerivativeGrid<P>> {
1519 check_grid_shape(ku, kv, grid)?;
1520 let (p, q) = (ku.degree(), kv.degree());
1521 let (su, sv) = (ku.span(u, tol)?, kv.span(v, tol)?);
1522 let du = ku.basis_derivatives(su, u, order);
1523 let dv = kv.basis_derivatives(sv, v, order);
1524
1525 let mut out: DerivativeGrid<P> =
1526 core::iter::repeat_with(|| core::iter::repeat_with(P::zero).take(order + 1).collect())
1527 .take(order + 1)
1528 .collect();
1529 let mut across: SmallVec<[SmallVec<[P; 8]>; 4]> = SmallVec::new();
1533 for weights_v in dv.iter().take(order + 1) {
1534 let mut row: SmallVec<[P; 8]> = SmallVec::new();
1535 for i in 0..=p {
1536 let mut inner = P::zero();
1537 for (j, &weight_v) in weights_v.iter().enumerate() {
1538 let Some(point) = grid.get(su - p + i, sv - q + j) else {
1539 ogeom_bail!(Dimension, "control grid index out of range");
1540 };
1541 inner = inner.add(point.scale(weight_v));
1542 }
1543 row.push(inner);
1544 }
1545 across.push(row);
1546 }
1547 for (k, row) in out.iter_mut().enumerate() {
1548 for (l, cell) in row.iter_mut().enumerate().take(order + 1 - k) {
1549 let mut total = P::zero();
1553 for (i, &weight_u) in du[k].iter().enumerate() {
1554 total = total.add(across[l][i].scale(weight_u));
1555 }
1556 *cell = total;
1557 }
1558 }
1559 Ok(out)
1560}
1561
1562pub fn evaluate_rational_surface<P: Blend>(
1571 ku: &KnotVector,
1572 kv: &KnotVector,
1573 grid: &ControlGrid<Weighted<P>>,
1574 u: f64,
1575 v: f64,
1576 tol: Tolerances,
1577) -> OgeomResult<P> {
1578 let h = evaluate_surface(ku, kv, grid, u, v, tol)?;
1579 if h.weight.abs() <= tol.confusion() {
1580 ogeom_bail!(
1581 Numeric,
1582 "rational surface evaluation produced a vanishing weight"
1583 );
1584 }
1585 Ok(h.point())
1586}
1587
1588pub fn rational_surface_derivatives<P: Blend>(
1601 ku: &KnotVector,
1602 kv: &KnotVector,
1603 grid: &ControlGrid<Weighted<P>>,
1604 u: f64,
1605 v: f64,
1606 order: usize,
1607 tol: Tolerances,
1608) -> OgeomResult<DerivativeGrid<P>> {
1609 let h = surface_derivatives(ku, kv, grid, u, v, order, tol)?;
1610 let w0 = h[0][0].weight;
1611 if w0.abs() <= tol.confusion() {
1612 ogeom_bail!(
1613 Numeric,
1614 "rational surface evaluation produced a vanishing weight"
1615 );
1616 }
1617
1618 let mut s: DerivativeGrid<P> =
1619 core::iter::repeat_with(|| core::iter::repeat_with(P::zero).take(order + 1).collect())
1620 .take(order + 1)
1621 .collect();
1622 for k in 0..=order {
1623 for l in 0..=order - k {
1626 let mut value = h[k][l].scaled;
1627 #[allow(clippy::cast_precision_loss)]
1628 for i in 1..=k {
1629 let c = binomial_coefficient(k, i) as f64;
1630 value = value.sub(s[k - i][l].scale(c * h[i][0].weight));
1631 }
1632 #[allow(clippy::cast_precision_loss)]
1633 for j in 1..=l {
1634 let c = binomial_coefficient(l, j) as f64;
1635 value = value.sub(s[k][l - j].scale(c * h[0][j].weight));
1636 }
1637 #[allow(clippy::cast_precision_loss)]
1638 for i in 1..=k {
1639 let ci = binomial_coefficient(k, i) as f64;
1640 for j in 1..=l {
1641 let cj = binomial_coefficient(l, j) as f64;
1642 value = value.sub(s[k - i][l - j].scale(ci * cj * h[i][j].weight));
1643 }
1644 }
1645 s[k][l] = value.scale(1.0 / w0);
1646 }
1647 }
1648 Ok(s)
1649}
1650
1651#[cfg(test)]
1652#[allow(clippy::unwrap_used)]
1653mod surface_tests {
1654 use super::*;
1655 use approx::assert_relative_eq;
1656
1657 const T: Tolerances = Tolerances::millimetres();
1658
1659 fn patch() -> (KnotVector, KnotVector, ControlGrid<Point>) {
1661 let (nu, nv) = (5, 4);
1662 let mut points = Vec::with_capacity(nu * nv);
1663 for i in 0..nu {
1664 for j in 0..nv {
1665 #[allow(clippy::cast_precision_loss)]
1666 let (x, y) = (i as f64, j as f64);
1667 points.push(Point::new(x, y, (x * 0.7).sin() * (y * 0.5).cos()));
1668 }
1669 }
1670 (
1671 KnotVector::clamped_uniform(3, nu).unwrap(),
1672 KnotVector::clamped_uniform(2, nv).unwrap(),
1673 ControlGrid::new(points, nu, nv).unwrap(),
1674 )
1675 }
1676
1677 #[test]
1678 fn grid_shape_is_checked_on_construction() {
1679 assert!(ControlGrid::new(vec![Point::ORIGIN; 6], 2, 3).is_ok());
1680 assert!(ControlGrid::new(vec![Point::ORIGIN; 6], 3, 3).is_err());
1681 assert!(ControlGrid::new(Vec::<Point>::new(), 0, 3).is_err());
1682 }
1683
1684 #[test]
1685 fn grid_indexing_is_row_major_and_bounds_checked() {
1686 let g = ControlGrid::new(
1687 vec![
1688 Point::new(0.0, 0.0, 0.0),
1689 Point::new(0.0, 1.0, 0.0),
1690 Point::new(0.0, 2.0, 0.0),
1691 Point::new(1.0, 0.0, 0.0),
1692 Point::new(1.0, 1.0, 0.0),
1693 Point::new(1.0, 2.0, 0.0),
1694 ],
1695 2,
1696 3,
1697 )
1698 .unwrap();
1699 assert_eq!(g.get(1, 2), Some(Point::new(1.0, 2.0, 0.0)));
1700 assert_eq!(g.get(0, 1), Some(Point::new(0.0, 1.0, 0.0)));
1701 assert_eq!(g.get(2, 0), None);
1702 assert_eq!(g.get(0, 3), None);
1703 }
1704
1705 #[test]
1706 fn transposing_twice_is_the_identity() {
1707 let (_, _, g) = patch();
1708 let t = g.transposed();
1709 assert_eq!(t.u_count(), g.v_count());
1710 assert_eq!(t.v_count(), g.u_count());
1711 for i in 0..g.u_count() {
1712 for j in 0..g.v_count() {
1713 assert_eq!(t.get(j, i), g.get(i, j));
1714 }
1715 }
1716 assert_eq!(t.transposed(), g);
1717 }
1718
1719 #[test]
1720 fn a_clamped_patch_interpolates_its_corner_control_points() {
1721 let (ku, kv, g) = patch();
1722 let ((u0, u1), (v0, v1)) = (ku.domain(), kv.domain());
1723 let corners = [
1724 (u0, v0, g.get(0, 0).unwrap()),
1725 (u0, v1, g.get(0, g.v_count() - 1).unwrap()),
1726 (u1, v0, g.get(g.u_count() - 1, 0).unwrap()),
1727 (u1, v1, g.get(g.u_count() - 1, g.v_count() - 1).unwrap()),
1728 ];
1729 for (u, v, expected) in corners {
1730 assert!(
1731 evaluate_surface(&ku, &kv, &g, u, v, T)
1732 .unwrap()
1733 .is_equal(expected, T),
1734 "corner ({u}, {v})"
1735 );
1736 }
1737 }
1738
1739 #[test]
1740 fn surface_shape_mismatches_are_refused() {
1741 let (ku, kv, g) = patch();
1742 let wrong = ControlGrid::new(g.points().to_vec(), 4, 5).unwrap();
1743 assert!(evaluate_surface(&ku, &kv, &wrong, 0.5, 0.5, T).is_err());
1744 assert!(evaluate_surface(&ku, &kv, &g, 1.5, 0.5, T).is_err());
1745 assert!(evaluate_surface(&ku, &kv, &g, 0.5, -0.5, T).is_err());
1746 }
1747
1748 #[test]
1749 fn surface_partials_agree_with_finite_differences() {
1750 let (ku, kv, g) = patch();
1751 let h = 1e-6;
1752 for iu in 1..6 {
1753 for iv in 1..6 {
1754 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1755 let d = surface_derivatives(&ku, &kv, &g, u, v, 2, T).unwrap();
1756 assert!(d[0][0].is_equal(evaluate_surface(&ku, &kv, &g, u, v, T).unwrap(), T));
1757
1758 let du = (evaluate_surface(&ku, &kv, &g, u + h, v, T).unwrap()
1759 - evaluate_surface(&ku, &kv, &g, u - h, v, T).unwrap())
1760 * (1.0 / (2.0 * h));
1761 let dv = (evaluate_surface(&ku, &kv, &g, u, v + h, T).unwrap()
1762 - evaluate_surface(&ku, &kv, &g, u, v - h, T).unwrap())
1763 * (1.0 / (2.0 * h));
1764 assert!((d[1][0].to_vector() - du).magnitude() < 1e-5 * du.magnitude().max(1.0));
1765 assert!((d[0][1].to_vector() - dv).magnitude() < 1e-5 * dv.magnitude().max(1.0));
1766
1767 let mixed = (evaluate_surface(&ku, &kv, &g, u + h, v + h, T).unwrap()
1769 - evaluate_surface(&ku, &kv, &g, u + h, v - h, T).unwrap()
1770 - (evaluate_surface(&ku, &kv, &g, u - h, v + h, T).unwrap()
1771 - evaluate_surface(&ku, &kv, &g, u - h, v - h, T).unwrap()))
1772 * (1.0 / (4.0 * h * h));
1773 assert!(
1774 (d[1][1].to_vector() - mixed).magnitude() < 1e-3 * mixed.magnitude().max(1.0),
1775 "mixed partial wrong at ({u}, {v})"
1776 );
1777 }
1778 }
1779 }
1780
1781 fn rational_hemisphere() -> (KnotVector, KnotVector, ControlGrid<Weighted<Point>>) {
1784 let w = core::f64::consts::FRAC_1_SQRT_2;
1785 let rows: [[(Point, f64); 3]; 3] = [
1787 [
1788 (Point::new(1.0, 0.0, 0.0), 1.0),
1789 (Point::new(1.0, 1.0, 0.0), w),
1790 (Point::new(0.0, 1.0, 0.0), 1.0),
1791 ],
1792 [
1793 (Point::new(1.0, 0.0, 1.0), w),
1794 (Point::new(1.0, 1.0, 1.0), w * w),
1795 (Point::new(0.0, 1.0, 1.0), w),
1796 ],
1797 [
1798 (Point::new(0.0, 0.0, 1.0), 1.0),
1799 (Point::new(0.0, 0.0, 1.0), w),
1800 (Point::new(0.0, 0.0, 1.0), 1.0),
1801 ],
1802 ];
1803 let points: Vec<_> = rows
1804 .iter()
1805 .flatten()
1806 .map(|(p, w)| Weighted::new(*p, *w, T).unwrap())
1807 .collect();
1808 (
1809 KnotVector::clamped_uniform(2, 3).unwrap(),
1810 KnotVector::clamped_uniform(2, 3).unwrap(),
1811 ControlGrid::new(points, 3, 3).unwrap(),
1812 )
1813 }
1814
1815 #[test]
1816 fn a_rational_biquadratic_traces_an_exact_sphere() {
1817 let (ku, kv, g) = rational_hemisphere();
1818 for iu in 0..=10 {
1819 for iv in 0..=10 {
1820 let (u, v) = (f64::from(iu) / 10.0, f64::from(iv) / 10.0);
1821 let p = evaluate_rational_surface(&ku, &kv, &g, u, v, T).unwrap();
1822 assert_relative_eq!(
1823 p.to_vector().magnitude(),
1824 1.0,
1825 epsilon = 1e-13,
1826 max_relative = 1e-13
1827 );
1828 }
1829 }
1830 }
1831
1832 #[test]
1833 fn rational_surface_partials_agree_with_finite_differences() {
1834 let (ku, kv, g) = rational_hemisphere();
1835 let h = 1e-6;
1836 let at = |u: f64, v: f64| evaluate_rational_surface(&ku, &kv, &g, u, v, T).unwrap();
1837 for iu in 1..6 {
1838 for iv in 1..6 {
1839 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1840 let d = rational_surface_derivatives(&ku, &kv, &g, u, v, 2, T).unwrap();
1841 assert!(d[0][0].is_equal(at(u, v), T));
1842
1843 let du = (at(u + h, v) - at(u - h, v)) * (1.0 / (2.0 * h));
1844 let dv = (at(u, v + h) - at(u, v - h)) * (1.0 / (2.0 * h));
1845 assert!(
1846 (d[1][0].to_vector() - du).magnitude() < 1e-5 * du.magnitude().max(1.0),
1847 "du wrong at ({u}, {v})"
1848 );
1849 assert!(
1850 (d[0][1].to_vector() - dv).magnitude() < 1e-5 * dv.magnitude().max(1.0),
1851 "dv wrong at ({u}, {v})"
1852 );
1853
1854 let mixed =
1857 (at(u + h, v + h) - at(u + h, v - h) - (at(u - h, v + h) - at(u - h, v - h)))
1858 * (1.0 / (4.0 * h * h));
1859 assert!(
1860 (d[1][1].to_vector() - mixed).magnitude() < 1e-2 * mixed.magnitude().max(1.0),
1861 "mixed partial wrong at ({u}, {v}): {:?} vs {mixed:?}",
1862 d[1][1]
1863 );
1864 }
1865 }
1866 }
1867
1868 #[test]
1869 fn a_spheres_normal_is_radial() {
1870 let (ku, kv, g) = rational_hemisphere();
1873 for iu in 1..8 {
1874 for iv in 1..8 {
1875 let (u, v) = (f64::from(iu) / 8.0, f64::from(iv) / 8.0);
1876 let d = rational_surface_derivatives(&ku, &kv, &g, u, v, 1, T).unwrap();
1877 let radius = d[0][0].to_vector();
1878 let normal = d[1][0].to_vector().cross(d[0][1].to_vector());
1879 assert!(
1880 normal.magnitude() > 1e-6,
1881 "degenerate tangents at ({u}, {v})"
1882 );
1883 let sine =
1884 radius.cross(normal).magnitude() / (radius.magnitude() * normal.magnitude());
1885 assert!(sine < 1e-9, "normal not radial at ({u}, {v}): sine {sine}");
1886 }
1887 }
1888 }
1889
1890 #[test]
1891 fn uniform_weights_reduce_to_the_polynomial_surface() {
1892 let (ku, kv, g) = patch();
1893 let weighted = g.map(|p| Weighted {
1894 scaled: p.scale(2.0),
1895 weight: 2.0,
1896 });
1897 for iu in 0..=6 {
1898 for iv in 0..=6 {
1899 let (u, v) = (f64::from(iu) / 6.0, f64::from(iv) / 6.0);
1900 let plain = evaluate_surface(&ku, &kv, &g, u, v, T).unwrap();
1901 let rational = evaluate_rational_surface(&ku, &kv, &weighted, u, v, T).unwrap();
1902 assert!(plain.is_equal(rational, T));
1903 }
1904 }
1905 }
1906}