1use geop_core_math::{
10 geop_error::{GeopError, GeopResult},
11 scalars::Scalar,
12 solvers::linear::{solve_banded, solve_pivoted},
13 vector::{Vector, Vector2, Vector3, Vector4},
14};
15
16use super::{NurbCurve, ParameterRefinable};
17use crate::spline::find_span;
18
19fn uniform_params(m: usize) -> Vec<Scalar> {
22 let mut t = vec![Scalar::ZERO; m];
23 t[m - 1] = Scalar::ONE;
24 for (k, tk) in t.iter_mut().enumerate().take(m - 1).skip(1) {
25 *tk = Scalar::from_ratio(k as i64, (m - 1) as i64).unwrap();
26 }
27 t
28}
29
30fn chord_length_params<const C: usize>(points: &[Vector<C>]) -> Vec<Scalar> {
51 let m = points.len();
52 let mut cumulative = vec![Scalar::ZERO; m];
53 for k in 1..m {
54 let chord = points[k].sub(&points[k - 1]).norm();
55 if chord.could_be_equal(Scalar::ZERO) {
56 return uniform_params(m);
57 }
58 cumulative[k] = cumulative[k - 1].add(chord);
59 }
60 let total = cumulative[m - 1];
61 if total.could_be_equal(Scalar::ZERO) {
62 return uniform_params(m);
63 }
64 let mut t = vec![Scalar::ZERO; m];
65 t[m - 1] = Scalar::ONE;
66 for k in 1..(m - 1) {
67 t[k] = match cumulative[k].div(total) {
72 Ok(x) => x.sharpen(),
73 Err(_) => return uniform_params(m),
74 };
75 }
76 if !t.windows(2).all(|w| w[0].definitely_less(w[1])) {
81 return uniform_params(m);
82 }
83 t
84}
85
86fn averaging_knots(t: &[Scalar], degree: usize) -> Vec<Scalar> {
92 let m = t.len();
93 let n = m - 1;
94 let p = degree;
95 let mut knots = vec![Scalar::ZERO; m + p + 1];
96 for i in 0..=p {
97 knots[i] = Scalar::ZERO;
98 let last = knots.len() - 1 - i;
99 knots[last] = Scalar::ONE;
100 }
101 let p_s = Scalar::from_i64(p as i64);
102 for j in 1..=(n - p) {
103 let mut sum = Scalar::ZERO;
104 for &tk in &t[j..(j + p)] {
105 sum = sum.add(tk);
106 }
107 knots[j + p] = sum.div(p_s).unwrap();
108 }
109 knots
110}
111
112pub(crate) fn basis_funs(span: usize, t: Scalar, degree: usize, knots: &[Scalar]) -> Vec<Scalar> {
115 let p = degree;
116 let mut n = vec![Scalar::ZERO; p + 1];
117 n[0] = Scalar::ONE;
118 let mut left = vec![Scalar::ZERO; p + 1];
119 let mut right = vec![Scalar::ZERO; p + 1];
120
121 for j in 1..=p {
122 left[j] = t.sub(knots[span + 1 - j]);
123 right[j] = knots[span + j].sub(t);
124 let mut saved = Scalar::ZERO;
125 for r in 0..j {
126 let denom = right[r + 1].add(left[j - r]);
127 let temp = if denom.could_be_equal(Scalar::ZERO) {
128 Scalar::ZERO
129 } else {
130 n[r].div(denom).unwrap_or(Scalar::ZERO)
131 };
132 n[r] = saved.add(right[r + 1].mul(temp));
133 saved = left[j - r].mul(temp);
134 }
135 n[j] = saved;
136 }
137 n
138}
139
140pub(crate) fn solve_interpolation_system<const C: usize>(
151 t: &[Scalar],
152 knots: &[Scalar],
153 degree: usize,
154 points: &[Vector<C>],
155) -> GeopResult<Vec<Vector<C>>> {
156 let m = points.len();
157 let p = degree;
158
159 let mut a = vec![vec![Scalar::ZERO; m]; m];
161 let mut first = vec![0; m];
162 let mut last = vec![0; m];
163 for (k, &tk) in t.iter().enumerate() {
164 let span = find_span(p, knots, m - 1, tk)?;
165 let funs = basis_funs(span, tk, p, knots);
166 for (j, &val) in funs.iter().enumerate() {
167 a[k][span - p + j] = val;
168 }
169 (first[k], last[k]) = (span - p, span);
170 }
171 if first.windows(2).any(|w| w[1] < w[0]) || last.windows(2).any(|w| w[1] < w[0]) {
172 return Err(GeopError::new(format!(
173 "NurbCurve::interpolate: parameters {t:?} do not run through the knot spans in order"
174 )));
175 }
176
177 solve_banded(a, &first, &last, points.to_vec())
178}
179
180fn with_unit_weights<const C: usize, const D: usize>(points: Vec<Vector<C>>) -> Vec<Vector<D>> {
183 points
184 .into_iter()
185 .map(|p| {
186 let mut v = Vector::<D>::zero();
187 for c in 0..C {
188 v[c] = p[c];
189 }
190 v[C] = Scalar::ONE;
191 v
192 })
193 .collect()
194}
195
196pub fn true_point_fractions(i: usize, intervals: usize) -> &'static [(i64, i64)] {
213 if i == 0 || i + 1 == intervals {
214 &[(1, 8), (1, 4), (3, 8), (1, 2), (5, 8), (3, 4), (7, 8)]
215 } else {
216 &[(1, 2)]
217 }
218}
219
220fn interpolate<const C: usize, const D: usize>(
226 points: &[Vector<C>],
227 between: Option<&[Vec<Vector<C>>]>,
228 degree: usize,
229) -> GeopResult<NurbCurve<D>>
230where
231 NurbCurve<D>: ParameterRefinable<C>,
232{
233 if points.len() < 2 {
234 return Err(GeopError::new(
235 "NurbCurve::interpolate: need at least 2 points",
236 ));
237 }
238
239 let m = points.len();
240 let p = degree.max(1).min(m - 1);
241
242 let centres: Vec<Vector<C>>;
248 let through = if between.is_some() {
249 centres = points.iter().map(|q| q.sharpen()).collect();
250 ¢res[..]
251 } else {
252 points
253 };
254
255 let t = chord_length_params(through);
256 let knots = averaging_knots(&t, p);
257 let control_points = with_unit_weights(solve_interpolation_system(&t, &knots, p, through)?);
258
259 let mut curve = NurbCurve::try_new(p, control_points, knots)?;
260 if let Some(between) = between {
261 if between.len() != m - 1 {
262 return Err(GeopError::new(format!(
263 "NurbCurve::interpolate_enclosing: expected true points for each of the {} \
264 intervals between the {m} samples, got {}",
265 m - 1,
266 between.len()
267 )));
268 }
269 let mut checks: Vec<(Scalar, Vector<C>)> =
274 t.iter().copied().zip(points.iter().copied()).collect();
275 for (w, qs) in t.windows(2).zip(between) {
276 for (j, &q) in qs.iter().enumerate() {
277 let frac = Scalar::from_ratio(j as i64 + 1, qs.len() as i64 + 1)?;
278 checks.push((Scalar::interpolate(w[0], w[1], frac).sharpen(), q));
279 }
280 }
281 widen_to_enclose(&mut curve, &checks)?;
282 }
283 Ok(curve)
284}
285
286fn widen_to_enclose<const C: usize, const D: usize>(
307 curve: &mut NurbCurve<D>,
308 checks: &[(Scalar, Vector<C>)],
309) -> GeopResult<()>
310where
311 NurbCurve<D>: ParameterRefinable<C>,
312{
313 let pad = enclosing_pad(curve, checks)?;
314 widen(curve, &pad);
315 Ok(())
316}
317
318fn enclosing_pad<const C: usize, const D: usize>(
321 curve: &NurbCurve<D>,
322 checks: &[(Scalar, Vector<C>)],
323) -> GeopResult<[Scalar; C]>
324where
325 NurbCurve<D>: ParameterRefinable<C>,
326{
327 let (lo, hi) = curve.domain();
328 let mut pad = [Scalar::ZERO; C];
329 for &(guess, q) in checks {
330 let position = curve.evaluate_cartesian(guess)?;
331 let tangent = curve.tangent_cartesian(guess)?;
332 let step = position
333 .sub(&q)
334 .prod_dot(&tangent)
335 .div(tangent.prod_dot(&tangent))
336 .unwrap_or(Scalar::ZERO);
337 let tau = guess.sub(step).sharpen();
338 let tau = if tau.definitely_less(lo) {
339 lo
340 } else if tau.definitely_greater(hi) {
341 hi
342 } else {
343 tau
344 };
345 let on_curve = curve.evaluate_cartesian(tau)?;
349 for k in 0..C {
350 let above = q[k].upper().sub(on_curve[k].upper()).upper();
351 let below = on_curve[k].lower().sub(q[k].lower()).upper();
352 pad[k] = pad[k].union(above).union(below).upper();
353 }
354 }
355 Ok(pad)
356}
357
358fn widen<const C: usize, const D: usize>(curve: &mut NurbCurve<D>, pad: &[Scalar; C]) {
362 for cp in &mut curve.control_points {
363 let w = cp[D - 1];
364 for k in 0..C {
365 let d = pad[k].mul(w);
366 cp[k] = cp[k].sub(d).union(cp[k].add(d));
367 }
368 }
369 curve.recompute_aabb();
370}
371
372fn check_params(params: &[Scalar], m: usize) -> GeopResult<()> {
375 let increasing = params.windows(2).all(|w| w[0].definitely_less(w[1]));
376 let ends = params
377 .first()
378 .is_some_and(|t| t.is_sharp() && t.could_be_equal(Scalar::ZERO))
379 && params
380 .last()
381 .is_some_and(|t| t.is_sharp() && t.could_be_equal(Scalar::ONE));
382 if params.len() != m || m < 2 || !increasing || !ends {
383 return Err(GeopError::new(format!(
384 "NurbCurve::interpolate_homogeneous: {m} values need as many parameters, strictly increasing from 0 to 1, not {params:?}"
385 )));
386 }
387 Ok(())
388}
389
390impl<const D: usize> NurbCurve<D> {
391 pub fn interpolate_homogeneous(
405 values: &[Vector<D>],
406 params: &[Scalar],
407 degree: usize,
408 ) -> GeopResult<Self> {
409 check_params(params, values.len())?;
410 let p = degree.max(1).min(values.len() - 1);
411 let knots = averaging_knots(params, p);
412 let control_points = solve_interpolation_system(params, &knots, p, values)?;
413 NurbCurve::try_new(p, control_points, knots)
414 }
415}
416
417impl NurbCurve<4> {
418 pub fn cubic_spline(
434 points: &[Vector3],
435 start: Option<Vector3>,
436 end: Option<Vector3>,
437 ) -> GeopResult<Self> {
438 let m = points.len();
439 if m < 2 {
440 return Err(GeopError::new(
441 "NurbCurve::cubic_spline: need at least 2 points",
442 ));
443 }
444 let t = chord_length_params(points);
445 let mut knots = vec![Scalar::ZERO; 4];
447 knots.extend_from_slice(&t[1..m - 1]);
448 knots.extend([Scalar::ONE; 4]);
449 let n = m + 2;
450 let speed = Scalar::from_f64(
451 points
452 .windows(2)
453 .map(|w| w[1].sub(&w[0]).norm().to_f64())
454 .sum(),
455 );
456 let mut a = vec![vec![Scalar::ZERO; n]; n];
457 let mut rhs = vec![Vector3::zero(); n];
458 a[0][0] = Scalar::ONE;
460 rhs[0] = points[0];
461 a[n - 1][n - 1] = Scalar::ONE;
462 rhs[n - 1] = points[m - 1];
463 for k in 1..m - 1 {
465 let span = find_span(3, &knots, n - 1, t[k])?;
466 for (j, value) in basis_funs(span, t[k], 3, &knots).into_iter().enumerate() {
467 a[k + 1][span - 3 + j] = value;
468 }
469 rhs[k + 1] = points[k];
470 }
471 let (u4, u5) = (knots[4], knots[5]);
475 let (v4, v5) = (Scalar::ONE.sub(knots[n - 1]), Scalar::ONE.sub(knots[n - 2]));
476 let mut condition = |row: usize,
477 tangent: Option<Vector3>,
478 h1: Scalar,
479 h2: Scalar,
480 ends: [usize; 3],
481 sign: Scalar|
482 -> GeopResult<()> {
483 let [p0, p1, p2] = ends;
484 match tangent {
485 Some(direction) => {
486 let derivative = direction.normalize()?.prod_scalar(speed.mul(sign));
487 a[row][p1] = Scalar::ONE;
488 a[row][p0] = Scalar::ONE.neg();
489 rhs[row] = derivative.prod_scalar(h1.div(Scalar::from_i64(3))?);
490 }
491 None => {
492 let (k1, k2) = (Scalar::ONE.div(h1)?, Scalar::ONE.div(h2)?);
493 a[row][p2] = k2;
494 a[row][p1] = k1.add(k2).neg();
495 a[row][p0] = k1;
496 }
497 }
498 Ok(())
499 };
500 condition(1, start, u4, u5, [0, 1, 2], Scalar::ONE)?;
501 condition(n - 2, end, v4, v5, [n - 1, n - 2, n - 3], Scalar::ONE.neg())?;
504 let control_points = solve_pivoted(a, rhs)?
505 .into_iter()
506 .map(|p| Vector4::from_array([p[0], p[1], p[2], Scalar::ONE]))
507 .collect();
508 NurbCurve::try_new(3, control_points, knots)
509 }
510
511 pub fn interpolate(points: &[Vector3], degree: usize) -> GeopResult<Self> {
516 interpolate::<3, 4>(points, None, degree)
517 }
518
519 pub fn interpolate_enclosing(
534 points: &[Vector3],
535 between: &[Vec<Vector3>],
536 degree: usize,
537 ) -> GeopResult<Self> {
538 interpolate::<3, 4>(points, Some(between), degree)
539 }
540
541 pub fn drift_by_interval(
546 points: &[Vector3],
547 between: &[Vec<Vector3>],
548 degree: usize,
549 ) -> GeopResult<Vec<f64>> {
550 let centres: Vec<Vector3> = points.iter().map(|q| q.sharpen()).collect();
551 let p = degree.max(1).min(centres.len() - 1);
552 let t = chord_length_params(¢res);
553 let knots = averaging_knots(&t, p);
554 let control_points =
555 with_unit_weights(solve_interpolation_system(&t, &knots, p, ¢res)?);
556 let curve = NurbCurve::<4>::try_new(p, control_points, knots)?;
557 let mut drifts = Vec::with_capacity(between.len());
558 for (w, qs) in t.windows(2).zip(between) {
559 let mut checks = Vec::with_capacity(qs.len());
560 for (j, &q) in qs.iter().enumerate() {
561 let frac = Scalar::from_ratio(j as i64 + 1, qs.len() as i64 + 1)?;
562 checks.push((Scalar::interpolate(w[0], w[1], frac).sharpen(), q));
563 }
564 let pad = enclosing_pad::<3, 4>(&curve, &checks)?;
565 drifts.push(pad.iter().map(|d| d.upper().to_f64()).fold(0.0, f64::max));
566 }
567 Ok(drifts)
568 }
569
570 pub fn enclosing_pad(&self, checks: &[(Scalar, Vector3)]) -> GeopResult<[Scalar; 3]> {
576 enclosing_pad(self, checks)
577 }
578
579 pub fn widen(&mut self, pad: &[Scalar; 3]) {
581 widen(self, pad)
582 }
583}
584
585impl NurbCurve<3> {
586 pub fn interpolate(points: &[Vector2], degree: usize) -> GeopResult<Self> {
589 interpolate::<2, 3>(points, None, degree)
590 }
591
592 pub fn interpolate_enclosing(
594 points: &[Vector2],
595 between: &[Vec<Vector2>],
596 degree: usize,
597 ) -> GeopResult<Self> {
598 interpolate::<2, 3>(points, Some(between), degree)
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 fn v3(x: f64, y: f64, z: f64) -> Vector3 {
607 Vector3::from_array([
608 Scalar::from_f64(x),
609 Scalar::from_f64(y),
610 Scalar::from_f64(z),
611 ])
612 }
613
614 fn v2(x: f64, y: f64) -> Vector2 {
615 Vector2::from_array([Scalar::from_f64(x), Scalar::from_f64(y)])
616 }
617
618 #[test]
626 fn interpolate_reproduces_endpoints() {
627 let points = vec![
630 v3(0.5, 0.5, 0.5),
631 v3(0.4713, 0.5219, 0.4102),
632 v3(0.4402, 0.5411, 0.3301),
633 v3(0.4001, 0.5502, 0.2604),
634 v3(0.5, 0.5, 0.2),
635 ];
636 for degree in [1, 2, 3] {
637 let curve = NurbCurve::<4>::interpolate(&points, degree).unwrap();
638 let (t0, t1) = curve.domain();
639 let start = curve.evaluate(t0).unwrap();
640 let end = curve.evaluate(t1).unwrap();
641 assert!(
642 start.could_be_equal(&points[0]),
643 "degree {degree}: start {start:?} != {:?}",
644 points[0]
645 );
646 assert!(
647 end.could_be_equal(points.last().unwrap()),
648 "degree {degree}: end {end:?} != {:?}",
649 points.last().unwrap()
650 );
651 }
652 }
653
654 #[test]
659 fn points_a_rounding_apart_interpolate() {
660 let i = |lo: f64, hi: f64| Scalar::from_f64(lo).union(Scalar::from_f64(hi));
661 let (x, y) = (
662 i(-0.4242640687119382, -0.4242640687119197),
663 i(-0.42426406871193406, -0.4242640687119227),
664 );
665 let points: Vec<Vector3> = [
666 (1.0000000000003741, 1.0000000000004499),
667 (1.000000000000271, 1.0000000000003466),
668 (1.0000000000001676, 1.0000000000002431),
669 (1.0000000000000644, 1.0000000000001399),
670 (0.9999999999999599, 1.0000000000000369),
671 ]
672 .iter()
673 .map(|&(lo, hi)| Vector3::from_array([x, y, i(lo, hi)]))
674 .collect();
675 let curve = NurbCurve::<4>::interpolate(&points, 3).unwrap();
676 let (t0, t1) = curve.domain();
677 assert!(curve.evaluate(t0).unwrap().could_be_equal(&points[0]));
678 assert!(curve.evaluate(t1).unwrap().could_be_equal(&points[4]));
679 }
680
681 #[test]
684 fn interpolate_2d_curve_matches_samples() {
685 let n = 20;
686 let points: Vec<_> = (0..=n)
687 .map(|i| {
688 let x = i as f64 / n as f64;
689 v2(x, x * x)
690 })
691 .collect();
692
693 let curve = NurbCurve::<3>::interpolate(&points, 3).unwrap();
694
695 let (t0, t1) = curve.domain();
696 let mid_t = t0.add(t1.sub(t0).mul(Scalar::from_f64(0.5)));
697 let p = curve.evaluate(mid_t).unwrap();
698 assert!(
699 p[1].sub(p[0].mul(p[0]))
700 .abs()
701 .could_be_less(Scalar::from_f64(1e-3))
702 );
703 }
704
705 #[test]
708 fn interpolate_line() {
709 let n = 50;
710 let points: Vec<_> = (0..=n).map(|i| v3(i as f64 / n as f64, 0.0, 0.0)).collect();
711
712 let curve = NurbCurve::<4>::interpolate(&points, 3).unwrap();
713
714 let p = curve.evaluate(Scalar::from_f64(0.5)).unwrap();
715 assert!(p[0].could_be_equal(Scalar::from_f64(0.5)));
716 assert!(p[1].could_be_equal(Scalar::ZERO));
717 assert!(p[2].could_be_equal(Scalar::ZERO));
718 }
719
720 #[test]
723 fn interpolate_passes_through_corner() {
724 let mut points = Vec::new();
725 for i in 0..=10 {
726 points.push(v3(i as f64 / 10.0, 0.0, 0.0));
727 }
728 for i in 1..=10 {
729 points.push(v3(1.0, i as f64 / 10.0, 0.0));
730 }
731
732 let curve = NurbCurve::<4>::interpolate(&points, 3).unwrap();
733
734 let (t0, t1) = curve.domain();
735 let start = curve.evaluate(t0).unwrap();
736 let end = curve.evaluate(t1).unwrap();
737 assert!(start[0].could_be_equal(Scalar::ZERO));
738 assert!(start[1].could_be_equal(Scalar::ZERO));
739 assert!(end[0].could_be_equal(Scalar::ONE));
740 assert!(end[1].could_be_equal(Scalar::ONE));
741 }
742
743 #[test]
748 fn interpolate_curve_matches_samples() {
749 let n = 20;
750 let points: Vec<_> = (0..=n)
751 .map(|i| {
752 let x = i as f64 / n as f64;
753 v3(x, x * x, 0.0)
754 })
755 .collect();
756
757 let curve = NurbCurve::<4>::interpolate(&points, 3).unwrap();
758
759 let (t0, t1) = curve.domain();
760 let mid_t = t0.add(t1.sub(t0).mul(Scalar::from_f64(0.5)));
761 let p = curve.evaluate(mid_t).unwrap();
762 assert!(
764 p[1].sub(p[0].mul(p[0]))
765 .abs()
766 .could_be_less(Scalar::from_f64(1e-3))
767 );
768 }
769
770 #[test]
774 fn interpolate_enclosing_contains_the_true_curve() {
775 use crate::contains::curve::curve_could_contain;
776 let circle = |a: f64| v3(a.cos(), a.sin(), 0.);
777 let n = 8;
778 let angle = |i: usize, of: usize| std::f64::consts::FRAC_PI_2 * i as f64 / of as f64;
779 let points: Vec<_> = (0..=n).map(|i| circle(angle(i, n))).collect();
780 let between: Vec<Vec<_>> = (0..n)
781 .map(|i| {
782 true_point_fractions(i, n)
783 .iter()
784 .map(|&(a, b)| circle(angle(i * b as usize + a as usize, n * b as usize)))
785 .collect()
786 })
787 .collect();
788
789 let exact = NurbCurve::<4>::interpolate(&points, 3).unwrap();
790 let enclosing = NurbCurve::<4>::interpolate_enclosing(&points, &between, 3).unwrap();
791 let eps = Scalar::from_f64(1e-6);
792 let dense: Vec<_> = (0..=200).map(|i| circle(angle(i, 200))).collect();
793 let found = |c: &NurbCurve<4>| {
794 dense
795 .iter()
796 .filter(|q| curve_could_contain(c, q, 5000, eps).unwrap().is_some())
797 .count()
798 };
799 assert!(
800 found(&exact) < dense.len(),
801 "the exact interpolant should drift"
802 );
803 let missed: Vec<(usize, Result<Option<Scalar>, String>)> = dense
804 .iter()
805 .enumerate()
806 .map(|(i, q)| {
807 (
808 i,
809 curve_could_contain(&enclosing, q, 5000, eps).map_err(|e| format!("{e}")),
810 )
811 })
812 .filter(|(_, r)| !matches!(r, Ok(Some(_))))
813 .collect();
814 assert!(missed.is_empty(), "true points not enclosed: {missed:?}");
815 }
816
817 #[test]
820 fn cubic_spline_meets_its_conditions() {
821 let points = [
822 v3(0.0, 0.0, 0.0),
823 v3(1.0, 0.5, 0.2),
824 v3(2.0, 0.3, 1.0),
825 v3(2.5, 1.5, 1.2),
826 ];
827 let parallel = |a: Vector3, b: Vector3| {
828 let c = a.prod_cross(&b);
829 (0..3).all(|k| c[k].abs().to_f64() < 1e-9) && a.prod_dot(&b).to_f64() > 0.0
830 };
831 let start = v3(0.0, 0.0, 1.0);
832 let curve = NurbCurve::<4>::cubic_spline(&points, Some(start), None).unwrap();
833 let (t0, t1) = curve.domain();
834 assert!(curve.evaluate(t0).unwrap().could_be_equal(&points[0]));
836 assert!(curve.evaluate(t1).unwrap().could_be_equal(&points[3]));
837 for (k, &p) in points.iter().enumerate().take(3).skip(1) {
838 let at = curve.knot_vector[3 + k];
839 let on = curve.evaluate(at).unwrap();
840 assert!(
841 on.could_be_equal(&p),
842 "point {k}: {on:?} at {at:?}, not {p:?}"
843 );
844 }
845 assert!(parallel(curve.tangent(t0).unwrap(), start));
846 let bend = curve.second_derivative(t1).unwrap();
848 assert!(
849 (0..3).all(|k| bend[k].could_be_equal(Scalar::ZERO)),
850 "{bend:?}"
851 );
852
853 let end = v3(1.0, 0.0, 0.0);
854 let curve = NurbCurve::<4>::cubic_spline(&points, None, Some(end)).unwrap();
855 assert!(parallel(curve.tangent(Scalar::ONE).unwrap(), end));
856 let bend = curve.second_derivative(Scalar::ZERO).unwrap();
857 assert!(
858 (0..3).all(|k| bend[k].could_be_equal(Scalar::ZERO)),
859 "{bend:?}"
860 );
861 }
862
863 #[test]
865 fn cubic_spline_of_two_points_is_straight() {
866 let (a, b) = (v3(0.0, 1.0, 2.0), v3(3.0, 1.0, -2.0));
867 let curve = NurbCurve::<4>::cubic_spline(&[a, b], None, None).unwrap();
868 let middle = curve.evaluate(Scalar::from_f64(0.5)).unwrap();
869 assert!(middle.could_be_equal(&v3(1.5, 1.0, 0.0)), "{middle:?}");
870 }
871}