Skip to main content

bevy_math/cubic_splines/
mod.rs

1//! Provides types for building cubic splines for rendering curves and use with animation easing.
2
3#[cfg(feature = "curve")]
4mod curve_impls;
5use crate::{
6    ops::{self, FloatPow},
7    Vec2, VectorSpace,
8};
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11use thiserror::Error;
12#[cfg(feature = "alloc")]
13use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
14
15/// A spline composed of a single cubic Bezier curve.
16///
17/// Useful for user-drawn curves with local control, or animation easing. See
18/// [`CubicSegment::new_bezier_easing`] for use in easing.
19///
20/// ### Interpolation
21///
22/// The curve only passes through the first and last control point in each set of four points. The curve
23/// is divided into "segments" by every fourth control point.
24///
25/// ### Tangency
26///
27/// Tangents are manually defined by the two intermediate control points within each set of four points.
28/// You can think of the control points the curve passes through as "anchors", and as the intermediate
29/// control points as the anchors displaced along their tangent vectors
30///
31/// ### Continuity
32///
33/// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is
34/// C2, meaning the tangent vector changes smoothly between each set of four control points, but this
35/// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving
36/// the intermediate control points to align the tangent vectors between segments, and can result in a
37/// loss of local control.
38///
39/// ### Usage
40///
41/// ```
42/// # use bevy_math::{*, prelude::*};
43/// let points = [[
44///     vec2(-1.0, -20.0),
45///     vec2(3.0, 2.0),
46///     vec2(5.0, 3.0),
47///     vec2(9.0, 8.0),
48/// ]];
49/// let bezier = CubicBezier::new(points).to_curve().unwrap();
50/// let positions: Vec<_> = bezier.iter_positions(100).collect();
51/// ```
52#[derive(Clone, Debug)]
53#[cfg(feature = "alloc")]
54#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
55pub struct CubicBezier<P: VectorSpace> {
56    /// The control points of the Bezier curve.
57    pub control_points: Vec<[P; 4]>,
58}
59
60#[cfg(feature = "alloc")]
61impl<P: VectorSpace> CubicBezier<P> {
62    /// Create a new cubic Bezier curve from sets of control points.
63    pub fn new(control_points: impl IntoIterator<Item = [P; 4]>) -> Self {
64        Self {
65            control_points: control_points.into_iter().collect(),
66        }
67    }
68}
69
70#[cfg(feature = "alloc")]
71impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBezier<P> {
72    type Error = CubicBezierError;
73
74    #[inline]
75    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
76        let segments = self
77            .control_points
78            .iter()
79            .map(|p| CubicSegment::new_bezier(*p))
80            .collect_vec();
81
82        if segments.is_empty() {
83            Err(CubicBezierError)
84        } else {
85            Ok(CubicCurve { segments })
86        }
87    }
88}
89/// An error returned during cubic curve generation for cubic Bezier curves indicating that a
90/// segment of control points was not present.
91#[derive(Clone, Debug, Error)]
92#[error("Unable to generate cubic curve: at least one set of control points is required")]
93pub struct CubicBezierError;
94
95/// A spline interpolated continuously between the nearest two control points, with the position and
96/// velocity of the curve specified at both control points. This curve passes through all control
97/// points, with the specified velocity which includes direction and parametric speed.
98///
99/// Useful for smooth interpolation when you know the position and velocity at two points in time,
100/// such as network prediction.
101///
102/// ### Interpolation
103///
104/// The curve passes through every control point.
105///
106/// ### Tangency
107///
108/// Tangents are explicitly defined at each control point.
109///
110/// ### Continuity
111///
112/// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also
113/// has no sudden jumps.
114///
115/// ### Parametrization
116///
117/// The first segment of the curve connects the first two control points, the second connects the second and
118/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
119/// the final curve segment connects the last control point to the first.
120///
121/// ### Usage
122///
123/// ```
124/// # use bevy_math::{*, prelude::*};
125/// let points = [
126///     vec2(-1.0, -20.0),
127///     vec2(3.0, 2.0),
128///     vec2(5.0, 3.0),
129///     vec2(9.0, 8.0),
130/// ];
131/// let tangents = [
132///     vec2(0.0, 1.0),
133///     vec2(0.0, 1.0),
134///     vec2(0.0, 1.0),
135///     vec2(0.0, 1.0),
136/// ];
137/// let hermite = CubicHermite::new(points, tangents).to_curve().unwrap();
138/// let positions: Vec<_> = hermite.iter_positions(100).collect();
139/// ```
140///
141/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
142#[derive(Clone, Debug)]
143#[cfg(feature = "alloc")]
144#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
145pub struct CubicHermite<P: VectorSpace> {
146    /// The control points of the Hermite curve.
147    pub control_points: Vec<(P, P)>,
148}
149
150#[cfg(feature = "alloc")]
151impl<P: VectorSpace> CubicHermite<P> {
152    /// Create a new Hermite curve from sets of control points.
153    pub fn new(
154        control_points: impl IntoIterator<Item = P>,
155        tangents: impl IntoIterator<Item = P>,
156    ) -> Self {
157        Self {
158            control_points: control_points.into_iter().zip(tangents).collect(),
159        }
160    }
161
162    /// The characteristic matrix for this spline construction.
163    ///
164    /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
165    /// combination of `p_i`, `v_i`, `p_{i+1}`, and `v_{i+1}`, where `(p_i, v_i)` and
166    /// `(p_{i+1}, v_{i+1})` are consecutive control points with tangents.
167    #[inline]
168    fn char_matrix(&self) -> [[f32; 4]; 4] {
169        [
170            [1., 0., 0., 0.],
171            [0., 1., 0., 0.],
172            [-3., -2., 3., -1.],
173            [2., 1., -2., 1.],
174        ]
175    }
176}
177
178#[cfg(feature = "alloc")]
179impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicHermite<P> {
180    type Error = InsufficientDataError;
181
182    #[inline]
183    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
184        let segments = self
185            .control_points
186            .array_windows()
187            .map(|&[(p0, v0), (p1, v1)]| {
188                CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
189            })
190            .collect_vec();
191
192        if segments.is_empty() {
193            Err(InsufficientDataError {
194                expected: 2,
195                given: self.control_points.len(),
196            })
197        } else {
198            Ok(CubicCurve { segments })
199        }
200    }
201}
202
203#[cfg(feature = "alloc")]
204impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicHermite<P> {
205    type Error = InsufficientDataError;
206
207    #[inline]
208    fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
209        let segments = self
210            .control_points
211            .iter()
212            .circular_tuple_windows()
213            .map(|(&j0, &j1)| {
214                let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1);
215                CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
216            })
217            .collect_vec();
218
219        if segments.is_empty() {
220            Err(InsufficientDataError {
221                expected: 2,
222                given: self.control_points.len(),
223            })
224        } else {
225            Ok(CubicCurve { segments })
226        }
227    }
228}
229
230/// A spline interpolated continuously across the nearest four control points, with the position of
231/// the curve specified at every control point and the tangents computed automatically. The associated [`CubicCurve`]
232/// has one segment between each pair of adjacent control points.
233///
234/// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5.
235///
236/// ### Interpolation
237///
238/// The curve passes through every control point.
239///
240/// ### Tangency
241///
242/// Tangents are automatically computed based on the positions of control points.
243///
244/// ### Continuity
245///
246/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent
247/// vector is also well-defined everywhere, without sudden jumps.
248///
249/// ### Parametrization
250///
251/// The first segment of the curve connects the first two control points, the second connects the second and
252/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
253/// the final curve segment connects the last control point to the first.
254///
255/// ### Usage
256///
257/// ```
258/// # use bevy_math::{*, prelude::*};
259/// let points = [
260///     vec2(-1.0, -20.0),
261///     vec2(3.0, 2.0),
262///     vec2(5.0, 3.0),
263///     vec2(9.0, 8.0),
264/// ];
265/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve().unwrap();
266/// let positions: Vec<_> = cardinal.iter_positions(100).collect();
267/// ```
268///
269/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
270#[derive(Clone, Debug)]
271#[cfg(feature = "alloc")]
272#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
273pub struct CubicCardinalSpline<P: VectorSpace> {
274    /// Tension
275    pub tension: f32,
276    /// The control points of the Cardinal spline
277    pub control_points: Vec<P>,
278}
279
280#[cfg(feature = "alloc")]
281impl<P: VectorSpace> CubicCardinalSpline<P> {
282    /// Build a new Cardinal spline.
283    pub fn new(tension: f32, control_points: impl IntoIterator<Item = P>) -> Self {
284        Self {
285            tension,
286            control_points: control_points.into_iter().collect(),
287        }
288    }
289
290    /// Build a new Catmull-Rom spline, the special case of a Cardinal spline where tension = 1/2.
291    pub fn new_catmull_rom(control_points: impl IntoIterator<Item = P>) -> Self {
292        Self {
293            tension: 0.5,
294            control_points: control_points.into_iter().collect(),
295        }
296    }
297
298    /// The characteristic matrix for this spline construction.
299    ///
300    /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
301    /// combination of four consecutive control points.
302    #[inline]
303    fn char_matrix(&self) -> [[f32; 4]; 4] {
304        let s = self.tension;
305        [
306            [0., 1., 0., 0.],
307            [-s, 0., s, 0.],
308            [2. * s, s - 3., 3. - 2. * s, -s],
309            [-s, 2. - s, s - 2., s],
310        ]
311    }
312}
313
314#[cfg(feature = "alloc")]
315impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicCardinalSpline<P> {
316    type Error = InsufficientDataError;
317
318    #[inline]
319    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
320        let length = self.control_points.len();
321
322        // Early return to avoid accessing an invalid index
323        if length < 2 {
324            return Err(InsufficientDataError {
325                expected: 2,
326                given: self.control_points.len(),
327            });
328        }
329
330        // Extend the list of control points by mirroring the last second-to-last control points on each end;
331        // this allows tangents for the endpoints to be provided, and the overall effect is that the tangent
332        // at an endpoint is proportional to twice the vector between it and its adjacent control point.
333        //
334        // The expression used here is P_{-1} := P_0 - (P_1 - P_0) = 2P_0 - P_1. (Analogously at the other end.)
335        let mirrored_first = self.control_points[0] * 2. - self.control_points[1];
336        let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2];
337        let extended_control_points = once(&mirrored_first)
338            .chain(self.control_points.iter())
339            .chain(once(&mirrored_last));
340
341        let segments = extended_control_points
342            .tuple_windows()
343            .map(|(&p0, &p1, &p2, &p3)| {
344                CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
345            })
346            .collect_vec();
347
348        Ok(CubicCurve { segments })
349    }
350}
351
352#[cfg(feature = "alloc")]
353impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
354    type Error = InsufficientDataError;
355
356    #[inline]
357    fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
358        let len = self.control_points.len();
359
360        if len < 2 {
361            return Err(InsufficientDataError {
362                expected: 2,
363                given: self.control_points.len(),
364            });
365        }
366
367        // This would ordinarily be the last segment, but we pick it out so that we can make it first
368        // in order to get a desirable parametrization where the first segment connects the first two
369        // control points instead of the second and third.
370        let first_segment = {
371            // We take the indices mod `len` in case `len` is very small.
372            let p0 = self.control_points[len - 1];
373            let p1 = self.control_points[0];
374            let p2 = self.control_points[1 % len];
375            let p3 = self.control_points[2 % len];
376            CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
377        };
378
379        let later_segments = self
380            .control_points
381            .iter()
382            .circular_tuple_windows()
383            .map(|(&p0, &p1, &p2, &p3)| {
384                CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
385            })
386            .take(len - 1);
387
388        let mut segments = Vec::with_capacity(len);
389        segments.push(first_segment);
390        segments.extend(later_segments);
391
392        Ok(CubicCurve { segments })
393    }
394}
395
396/// A spline interpolated continuously across the nearest four control points. The curve does not
397/// necessarily pass through any of the control points.
398///
399/// ### Interpolation
400///
401/// The curve does not necessarily pass through its control points.
402///
403/// ### Tangency
404/// Tangents are automatically computed based on the positions of control points.
405///
406/// ### Continuity
407///
408/// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along
409/// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this
410/// spline makes it useful for camera paths.
411///
412/// ### Parametrization
413///
414/// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`]
415/// is used to form a cyclic curve, the three additional segments used to close the curve come last.
416///
417/// ### Usage
418///
419/// ```
420/// # use bevy_math::{*, prelude::*};
421/// let points = [
422///     vec2(-1.0, -20.0),
423///     vec2(3.0, 2.0),
424///     vec2(5.0, 3.0),
425///     vec2(9.0, 8.0),
426/// ];
427/// let b_spline = CubicBSpline::new(points).to_curve().unwrap();
428/// let positions: Vec<_> = b_spline.iter_positions(100).collect();
429/// ```
430///
431/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
432#[derive(Clone, Debug)]
433#[cfg(feature = "alloc")]
434#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
435pub struct CubicBSpline<P: VectorSpace> {
436    /// The control points of the spline
437    pub control_points: Vec<P>,
438}
439#[cfg(feature = "alloc")]
440impl<P: VectorSpace> CubicBSpline<P> {
441    /// Build a new B-Spline.
442    pub fn new(control_points: impl IntoIterator<Item = P>) -> Self {
443        Self {
444            control_points: control_points.into_iter().collect(),
445        }
446    }
447
448    /// The characteristic matrix for this spline construction.
449    ///
450    /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
451    /// combination of four consecutive control points.
452    #[inline]
453    fn char_matrix(&self) -> [[f32; 4]; 4] {
454        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
455        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
456        // See section 4.1 and equations 7 and 8.
457        let mut char_matrix = [
458            [1.0, 4.0, 1.0, 0.0],
459            [-3.0, 0.0, 3.0, 0.0],
460            [3.0, -6.0, 3.0, 0.0],
461            [-1.0, 3.0, -3.0, 1.0],
462        ];
463
464        char_matrix
465            .iter_mut()
466            .for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0));
467
468        char_matrix
469    }
470}
471
472#[cfg(feature = "alloc")]
473impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBSpline<P> {
474    type Error = InsufficientDataError;
475
476    #[inline]
477    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
478        let segments = self
479            .control_points
480            .array_windows()
481            .map(|&p| CubicSegment::coefficients(p, self.char_matrix()))
482            .collect_vec();
483
484        if segments.is_empty() {
485            Err(InsufficientDataError {
486                expected: 4,
487                given: self.control_points.len(),
488            })
489        } else {
490            Ok(CubicCurve { segments })
491        }
492    }
493}
494
495#[cfg(feature = "alloc")]
496impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicBSpline<P> {
497    type Error = InsufficientDataError;
498
499    #[inline]
500    fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
501        let segments = self
502            .control_points
503            .iter()
504            .circular_tuple_windows()
505            .map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix()))
506            .collect_vec();
507
508        // Note that the parametrization is consistent with the one for `to_curve` but with
509        // the extra curve segments all tacked on at the end. This might be slightly counter-intuitive,
510        // since it means the first segment doesn't go "between" the first two control points, but
511        // between the second and third instead.
512
513        if segments.is_empty() {
514            Err(InsufficientDataError {
515                expected: 2,
516                given: self.control_points.len(),
517            })
518        } else {
519            Ok(CubicCurve { segments })
520        }
521    }
522}
523
524/// Error during construction of [`CubicNurbs`]
525#[derive(Clone, Debug, Error)]
526pub enum CubicNurbsError {
527    /// Provided the wrong number of knots.
528    #[error("Wrong number of knots: expected {expected}, provided {provided}")]
529    KnotsNumberMismatch {
530        /// Expected number of knots
531        expected: usize,
532        /// Provided number of knots
533        provided: usize,
534    },
535    /// The provided knots had a descending knot pair. Subsequent knots must
536    /// either increase or stay the same.
537    #[error("Invalid knots: contains descending knot pair")]
538    DescendingKnots,
539    /// The provided knots were all equal. Knots must contain at least one increasing pair.
540    #[error("Invalid knots: all knots are equal")]
541    ConstantKnots,
542    /// Provided a different number of weights and control points.
543    #[error("Incorrect number of weights: expected {expected}, provided {provided}")]
544    WeightsNumberMismatch {
545        /// Expected number of weights
546        expected: usize,
547        /// Provided number of weights
548        provided: usize,
549    },
550    /// The number of control points provided is less than 4.
551    #[error("Not enough control points, at least 4 are required, {provided} were provided")]
552    NotEnoughControlPoints {
553        /// The number of control points provided
554        provided: usize,
555    },
556}
557
558/// Non-uniform Rational B-Splines (NURBS) are a powerful generalization of the [`CubicBSpline`] which can
559/// represent a much more diverse class of curves (like perfect circles and ellipses).
560///
561/// ### Non-uniformity
562///
563/// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'.
564/// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of
565/// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs
566/// each control the length of one segment of the curve. Multiple repeated knot values are called
567/// "knot multiplicity". Knot multiplicity in the intermediate knots causes a "zero-length" segment,
568/// and can create sharp corners.
569///
570/// ### Rationality
571///
572/// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to
573/// be assigned a weighting, which controls how much it affects the curve compared to the other points.
574///
575/// ### Interpolation
576///
577/// The curve will not pass through the control points except where a knot has multiplicity four.
578///
579/// ### Tangency
580///
581/// Tangents are automatically computed based on the position of control points.
582///
583/// ### Continuity
584///
585/// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the
586/// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration
587/// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the
588/// continuity to C1, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least
589/// C0, meaning it has no jumps or holes.
590///
591/// ### Usage
592///
593/// ```
594/// # use bevy_math::{*, prelude::*};
595/// let points = [
596///     vec2(-1.0, -20.0),
597///     vec2(3.0, 2.0),
598///     vec2(5.0, 3.0),
599///     vec2(9.0, 8.0),
600/// ];
601/// let weights = [1.0, 1.0, 2.0, 1.0];
602/// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0];
603/// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots))
604///     .expect("NURBS construction failed!")
605///     .to_curve()
606///     .unwrap();
607/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
608/// ```
609#[derive(Clone, Debug)]
610#[cfg(feature = "alloc")]
611#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
612pub struct CubicNurbs<P: VectorSpace> {
613    /// The control points of the NURBS
614    pub control_points: Vec<P>,
615    /// Weights
616    pub weights: Vec<f32>,
617    /// Knots
618    pub knots: Vec<f32>,
619}
620
621#[cfg(feature = "alloc")]
622impl<P: VectorSpace<Scalar = f32>> CubicNurbs<P> {
623    /// Build a Non-Uniform Rational B-Spline.
624    ///
625    /// If provided, weights must be the same length as the control points. Defaults to equal weights.
626    ///
627    /// If provided, the number of knots must be n + 4 elements, where n is the amount of control
628    /// points. Defaults to open uniform knots: [`Self::open_uniform_knots`]. Knots cannot
629    /// all be equal.
630    ///
631    /// At least 4 points must be provided, otherwise an error will be returned.
632    pub fn new(
633        control_points: impl IntoIterator<Item = P>,
634        weights: Option<impl IntoIterator<Item = f32>>,
635        knots: Option<impl IntoIterator<Item = f32>>,
636    ) -> Result<Self, CubicNurbsError> {
637        let mut control_points: Vec<P> = control_points.into_iter().collect();
638        let control_points_len = control_points.len();
639
640        if control_points_len < 4 {
641            return Err(CubicNurbsError::NotEnoughControlPoints {
642                provided: control_points_len,
643            });
644        }
645
646        let weights: Vec<f32> = weights
647            .map(|ws| ws.into_iter().collect())
648            .unwrap_or_else(|| vec![1.0; control_points_len]);
649
650        let mut knots: Vec<f32> = knots.map(|ks| ks.into_iter().collect()).unwrap_or_else(|| {
651            Self::open_uniform_knots(control_points_len)
652                .expect("The amount of control points was checked")
653        });
654
655        let expected_knots_len = Self::knots_len(control_points_len);
656
657        // Check the number of knots is correct
658        if knots.len() != expected_knots_len {
659            return Err(CubicNurbsError::KnotsNumberMismatch {
660                expected: expected_knots_len,
661                provided: knots.len(),
662            });
663        }
664
665        // Ensure the knots are non-descending (previous element is less than or equal
666        // to the next)
667        if knots.array_windows().any(|[a, b]| a > b) {
668            return Err(CubicNurbsError::DescendingKnots);
669        }
670
671        // Ensure the knots are non-constant
672        if knots.array_windows().all(|[a, b]| a == b) {
673            return Err(CubicNurbsError::ConstantKnots);
674        }
675
676        // Check that the number of weights equals the number of control points
677        if weights.len() != control_points_len {
678            return Err(CubicNurbsError::WeightsNumberMismatch {
679                expected: control_points_len,
680                provided: weights.len(),
681            });
682        }
683
684        // To align the evaluation behavior of nurbs with the other splines,
685        // make the intervals between knots form an exact cover of [0, N], where N is
686        // the number of segments of the final curve.
687        let curve_length = (control_points.len() - 3) as f32;
688        let min = *knots.first().unwrap();
689        let max = *knots.last().unwrap();
690        let knot_delta = max - min;
691        knots = knots
692            .into_iter()
693            .map(|k| k - min)
694            .map(|k| k * curve_length / knot_delta)
695            .collect();
696
697        control_points
698            .iter_mut()
699            .zip(weights.iter())
700            .for_each(|(p, w)| *p = *p * *w);
701
702        Ok(Self {
703            control_points,
704            weights,
705            knots,
706        })
707    }
708
709    /// Generates uniform knots that will generate the same curve as [`CubicBSpline`].
710    ///
711    /// "Uniform" means that the difference between two subsequent knots is the same.
712    ///
713    /// Will return `None` if there are less than 4 control points.
714    pub fn uniform_knots(control_points: usize) -> Option<Vec<f32>> {
715        if control_points < 4 {
716            return None;
717        }
718        Some(
719            (0..Self::knots_len(control_points))
720                .map(|v| v as f32)
721                .collect(),
722        )
723    }
724
725    /// Generates open uniform knots, which makes the ends of the curve pass through the
726    /// start and end points.
727    ///
728    /// The start and end knots have multiplicity 4, and intermediate knots have multiplicity 0 and
729    /// difference of 1.
730    ///
731    /// Will return `None` if there are less than 4 control points.
732    pub fn open_uniform_knots(control_points: usize) -> Option<Vec<f32>> {
733        if control_points < 4 {
734            return None;
735        }
736        let last_knots_value = control_points - 3;
737        Some(
738            core::iter::repeat_n(0.0, 4)
739                .chain((1..last_knots_value).map(|v| v as f32))
740                .chain(core::iter::repeat_n(last_knots_value as f32, 4))
741                .collect(),
742        )
743    }
744
745    #[inline]
746    const fn knots_len(control_points_len: usize) -> usize {
747        control_points_len + 4
748    }
749
750    /// Generates a non-uniform B-spline characteristic matrix from a sequence of six knots. Each six
751    /// knots describe the relationship between four successive control points. For padding reasons,
752    /// this takes a vector of 8 knots, but only six are actually used.
753    fn generate_matrix(knots: &[f32; 8]) -> [[f32; 4]; 4] {
754        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
755        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
756        // See section 3.1.
757
758        let t = knots;
759        // In the notation of the paper:
760        // t[1] := t_i-2
761        // t[2] := t_i-1
762        // t[3] := t_i   (the lower extent of the current knot span)
763        // t[4] := t_i+1 (the upper extent of the current knot span)
764        // t[5] := t_i+2
765        // t[6] := t_i+3
766
767        let m00 = (t[4] - t[3]).squared() / ((t[4] - t[2]) * (t[4] - t[1]));
768        let m02 = (t[3] - t[2]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
769        let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2]));
770        let m22 = 3.0 * (t[4] - t[3]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
771        let m33 = (t[4] - t[3]).squared() / ((t[6] - t[3]) * (t[5] - t[3]));
772        let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).squared() / ((t[5] - t[3]) * (t[5] - t[2]));
773        [
774            [m00, 1.0 - m00 - m02, m02, 0.0],
775            [-3.0 * m00, 3.0 * m00 - m12, m12, 0.0],
776            [3.0 * m00, -3.0 * m00 - m22, m22, 0.0],
777            [-m00, m00 - m32 - m33, m32, m33],
778        ]
779    }
780}
781
782#[cfg(feature = "alloc")]
783impl<P: VectorSpace<Scalar = f32>> RationalGenerator<P> for CubicNurbs<P> {
784    type Error = InsufficientDataError;
785
786    #[inline]
787    fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error> {
788        let segments = self
789            .control_points
790            .array_windows()
791            .zip(self.weights.array_windows())
792            .zip(self.knots.array_windows())
793            .filter(|(_, knots)| knots[4] - knots[3] > 0.0)
794            .map(|((&points, &weights), knots)| {
795                // This is curve segment i. It uses control points P_i, P_i+2, P_i+2 and P_i+3,
796                // It is associated with knot span i+3 (which is the interval between knots i+3
797                // and i+4) and its characteristic matrix uses knots i+1 through i+6 (because
798                // those define the two knot spans on either side).
799                let span = knots[4] - knots[3];
800                let matrix = Self::generate_matrix(knots);
801                RationalSegment::coefficients(points, weights, span, matrix)
802            })
803            .collect_vec();
804        if segments.is_empty() {
805            Err(InsufficientDataError {
806                expected: 4,
807                given: self.control_points.len(),
808            })
809        } else {
810            Ok(RationalCurve { segments })
811        }
812    }
813}
814
815/// A spline interpolated linearly between the nearest 2 points.
816///
817/// ### Interpolation
818///
819/// The curve passes through every control point.
820///
821/// ### Tangency
822///
823/// The curve is not generally differentiable at control points.
824///
825/// ### Continuity
826///
827/// The curve is C0 continuous, meaning it has no holes or jumps.
828///
829/// ### Parametrization
830///
831/// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is
832/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first.
833///
834/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
835#[derive(Clone, Debug)]
836#[cfg(feature = "alloc")]
837#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
838pub struct LinearSpline<P: VectorSpace> {
839    /// The control points of the linear spline.
840    pub points: Vec<P>,
841}
842
843#[cfg(feature = "alloc")]
844impl<P: VectorSpace> LinearSpline<P> {
845    /// Create a new linear spline from a list of points to be interpolated.
846    pub fn new(points: impl IntoIterator<Item = P>) -> Self {
847        Self {
848            points: points.into_iter().collect(),
849        }
850    }
851}
852
853#[cfg(feature = "alloc")]
854impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
855    type Error = InsufficientDataError;
856
857    #[inline]
858    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
859        let segments = self
860            .points
861            .array_windows()
862            .map(|&[a, b]| CubicSegment {
863                coeff: [a, b - a, P::default(), P::default()],
864            })
865            .collect_vec();
866
867        if segments.is_empty() {
868            Err(InsufficientDataError {
869                expected: 2,
870                given: self.points.len(),
871            })
872        } else {
873            Ok(CubicCurve { segments })
874        }
875    }
876}
877
878#[cfg(feature = "alloc")]
879impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
880    type Error = InsufficientDataError;
881
882    #[inline]
883    fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
884        let segments = self
885            .points
886            .iter()
887            .circular_tuple_windows()
888            .map(|(&a, &b)| CubicSegment {
889                coeff: [a, b - a, P::default(), P::default()],
890            })
891            .collect_vec();
892
893        if segments.is_empty() {
894            Err(InsufficientDataError {
895                expected: 2,
896                given: self.points.len(),
897            })
898        } else {
899            Ok(CubicCurve { segments })
900        }
901    }
902}
903
904/// An error indicating that a spline construction didn't have enough control points to generate a curve.
905#[derive(Clone, Debug, Error)]
906#[error("Not enough data to build curve: needed at least {expected} control points but was only given {given}")]
907pub struct InsufficientDataError {
908    expected: usize,
909    given: usize,
910}
911
912/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
913#[cfg(feature = "alloc")]
914pub trait CubicGenerator<P: VectorSpace> {
915    /// An error type indicating why construction might fail.
916    type Error;
917
918    /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
919    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;
920}
921
922/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters.
923///
924/// This makes sense only when the control data can be interpreted cyclically.
925#[cfg(feature = "alloc")]
926pub trait CyclicCubicGenerator<P: VectorSpace> {
927    /// An error type indicating why construction might fail.
928    type Error;
929
930    /// Build a cyclic [`CubicCurve`] by computing the interpolation coefficients for each curve segment,
931    /// treating the control data as cyclic so that the result is a closed curve.
932    fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error>;
933}
934
935/// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation.
936/// It is a [`Curve`] with domain `[0, 1]`.
937///
938/// Segments can be chained together to form a longer [compound curve].
939///
940/// [compound curve]: CubicCurve
941/// [`Curve`]: crate::curve::Curve
942#[derive(Copy, Clone, Debug, Default, PartialEq)]
943#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
944#[cfg_attr(
945    feature = "bevy_reflect",
946    derive(Reflect),
947    reflect(Debug, Default, Clone)
948)]
949pub struct CubicSegment<P: VectorSpace> {
950    /// Polynomial coefficients for the segment.
951    pub coeff: [P; 4],
952}
953
954impl<P: VectorSpace<Scalar = f32>> CubicSegment<P> {
955    /// Instantaneous position of a point at parametric value `t`.
956    #[inline]
957    pub fn position(&self, t: f32) -> P {
958        let [a, b, c, d] = self.coeff;
959        // Evaluate `a + bt + ct^2 + dt^3`, avoiding exponentiation
960        a + (b + (c + d * t) * t) * t
961    }
962
963    /// Instantaneous velocity of a point at parametric value `t`.
964    #[inline]
965    pub fn velocity(&self, t: f32) -> P {
966        let [_, b, c, d] = self.coeff;
967        // Evaluate the derivative, which is `b + 2ct + 3dt^2`, avoiding exponentiation
968        b + (c * 2.0 + d * 3.0 * t) * t
969    }
970
971    /// Instantaneous acceleration of a point at parametric value `t`.
972    #[inline]
973    pub fn acceleration(&self, t: f32) -> P {
974        let [_, _, c, d] = self.coeff;
975        // Evaluate the second derivative, which is `2c + 6dt`
976        c * 2.0 + d * 6.0 * t
977    }
978
979    /// Creates a cubic segment from four points, representing a Bezier curve.
980    pub fn new_bezier(points: [P; 4]) -> Self {
981        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
982        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
983        // See section 4.2 and equation 11.
984        let char_matrix = [
985            [1., 0., 0., 0.],
986            [-3., 3., 0., 0.],
987            [3., -6., 3., 0.],
988            [-1., 3., -3., 1.],
989        ];
990        Self::coefficients(points, char_matrix)
991    }
992
993    /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.
994    #[inline]
995    fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
996        let [c0, c1, c2, c3] = char_matrix;
997        // These are the polynomial coefficients, computed by multiplying the characteristic
998        // matrix by the point matrix.
999        let coeff = [
1000            p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
1001            p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
1002            p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
1003            p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
1004        ];
1005        Self { coeff }
1006    }
1007
1008    /// A flexible iterator used to sample curves with arbitrary functions.
1009    ///
1010    /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1011    /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1012    /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1013    ///
1014    /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1015    /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1016    #[inline]
1017    pub fn iter_samples<'a, 'b: 'a>(
1018        &'b self,
1019        subdivisions: usize,
1020        mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1021    ) -> impl Iterator<Item = P> + 'a {
1022        self.iter_uniformly(subdivisions)
1023            .map(move |t| sample_function(self, t))
1024    }
1025
1026    /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1027    #[inline]
1028    pub fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1029        let step = 1.0 / subdivisions as f32;
1030        (0..=subdivisions).map(move |i| i as f32 * step)
1031    }
1032
1033    /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1034    pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1035        self.iter_samples(subdivisions, Self::position)
1036    }
1037
1038    /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1039    pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1040        self.iter_samples(subdivisions, Self::velocity)
1041    }
1042
1043    /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1044    pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1045        self.iter_samples(subdivisions, Self::acceleration)
1046    }
1047}
1048
1049/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.
1050///
1051/// The x-axis of the curve is time, and the y-axis is the output value. This struct provides
1052/// methods for extremely fast solves for y given x.
1053impl CubicSegment<Vec2> {
1054    /// Construct a cubic Bezier curve for animation easing, with control points `p1` and `p2`. A
1055    /// cubic Bezier easing curve has control point `p0` at (0, 0) and `p3` at (1, 1), leaving only
1056    /// `p1` and `p2` as the remaining degrees of freedom. The first and last control points are
1057    /// fixed to ensure the animation begins at 0, and ends at 1.
1058    ///
1059    /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For
1060    /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
1061    #[cfg(feature = "alloc")]
1062    pub fn new_bezier_easing(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
1063        let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
1064        Self::new_bezier([p0, p1.into(), p2.into(), p3])
1065    }
1066
1067    /// Maximum allowable error for iterative Bezier solve
1068    const MAX_ERROR: f32 = 1e-5;
1069
1070    /// Maximum number of iterations during Bezier solve
1071    const MAX_ITERS: u8 = 8;
1072
1073    /// Given a `time` within `0..=1`, returns an eased value that follows the cubic curve instead
1074    /// of a straight line. This eased result may be outside the range `0..=1`, however it will
1075    /// always start at 0 and end at 1: `ease(0) = 0` and `ease(1) = 1`.
1076    ///
1077    /// ```
1078    /// # use bevy_math::prelude::*;
1079    /// # #[cfg(feature = "alloc")]
1080    /// # {
1081    /// let cubic_bezier = CubicSegment::new_bezier_easing((0.25, 0.1), (0.25, 1.0));
1082    /// assert_eq!(cubic_bezier.ease(0.0), 0.0);
1083    /// assert_eq!(cubic_bezier.ease(1.0), 1.0);
1084    /// # }
1085    /// ```
1086    ///
1087    /// # How cubic easing works
1088    ///
1089    /// Easing is generally accomplished with the help of "shaping functions". These are curves that
1090    /// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the
1091    /// animation, from 0 to 1. The y-axis is how far along the animation is, also from 0 to 1. You
1092    /// can imagine that if the shaping function is a straight line, there is a 1:1 mapping between
1093    /// the `time` and how far along your animation is. If the `time` = 0.5, the animation is
1094    /// halfway through. This is known as linear interpolation, and results in objects animating
1095    /// with a constant velocity, and no smooth acceleration or deceleration at the start or end.
1096    ///
1097    /// ```text
1098    /// y
1099    /// │         ●
1100    /// │       ⬈
1101    /// │     ⬈
1102    /// │   ⬈
1103    /// │ ⬈
1104    /// ●─────────── x (time)
1105    /// ```
1106    ///
1107    /// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path
1108    /// determined by the two remaining control points (handles). These handles allow us to define a
1109    /// smooth curve. As `time` (x-axis) progresses, we now follow the curve, and use the `y` value
1110    /// to determine how far along the animation is.
1111    ///
1112    /// ```text
1113    /// y
1114    ///          ⬈➔●
1115    /// │      ⬈
1116    /// │     ↑
1117    /// │     ↑
1118    /// │    ⬈
1119    /// ●➔⬈───────── x (time)
1120    /// ```
1121    ///
1122    /// To accomplish this, we need to be able to find the position `y` on a curve, given the `x`
1123    /// value. Cubic curves are implicit parametric functions like B(t) = (x,y). To find `y`, we
1124    /// first solve for `t` that corresponds to the given `x` (`time`). We use the Newton-Raphson
1125    /// root-finding method to quickly find a value of `t` that is very near the desired value of
1126    /// `x`. Once we have this we can easily plug that `t` into our curve's `position` function, to
1127    /// find the `y` component, which is how far along our animation should be. In other words:
1128    ///
1129    /// > Given `time` in `0..=1`
1130    ///
1131    /// > Use Newton's method to find a value of `t` that results in B(t) = (x,y) where `x == time`
1132    ///
1133    /// > Once a solution is found, use the resulting `y` value as the final result
1134    #[inline]
1135    pub fn ease(&self, time: f32) -> f32 {
1136        let x = time.clamp(0.0, 1.0);
1137        self.find_y_given_x(x)
1138    }
1139
1140    /// Find the `y` value of the curve at the given `x` value using the Newton-Raphson method.
1141    #[inline]
1142    fn find_y_given_x(&self, x: f32) -> f32 {
1143        let mut t_guess = x;
1144        let mut pos_guess = Vec2::ZERO;
1145        for _ in 0..Self::MAX_ITERS {
1146            pos_guess = self.position(t_guess);
1147            let error = pos_guess.x - x;
1148            if ops::abs(error) <= Self::MAX_ERROR {
1149                break;
1150            }
1151            // Using Newton's method, use the tangent line to estimate a better guess value.
1152            let slope = self.velocity(t_guess).x; // dx/dt
1153            t_guess -= error / slope;
1154        }
1155        pos_guess.y
1156    }
1157}
1158
1159/// A collection of [`CubicSegment`]s chained into a single parametric curve. It is a [`Curve`]
1160/// with domain `[0, N]`, where `N` is its number of segments.
1161///
1162/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
1163/// [`CubicBezier`].
1164///
1165/// [`Curve`]: crate::curve::Curve
1166#[derive(Clone, Debug, PartialEq)]
1167#[cfg(feature = "alloc")]
1168#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1169#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
1170pub struct CubicCurve<P: VectorSpace> {
1171    /// The segments comprising the curve. This must always be nonempty.
1172    segments: Vec<CubicSegment<P>>,
1173}
1174
1175#[cfg(feature = "alloc")]
1176impl<P: VectorSpace<Scalar = f32>> CubicCurve<P> {
1177    /// Create a new curve from a collection of segments. If the collection of segments is empty,
1178    /// a curve cannot be built and `None` will be returned instead.
1179    pub fn from_segments(segments: impl IntoIterator<Item = CubicSegment<P>>) -> Option<Self> {
1180        let segments: Vec<_> = segments.into_iter().collect();
1181        if segments.is_empty() {
1182            None
1183        } else {
1184            Some(Self { segments })
1185        }
1186    }
1187
1188    /// Compute the position of a point on the cubic curve at the parametric value `t`.
1189    ///
1190    /// Note that `t` varies from `0..=(n_points - 3)`.
1191    #[inline]
1192    pub fn position(&self, t: f32) -> P {
1193        let (segment, t) = self.segment(t);
1194        segment.position(t)
1195    }
1196
1197    /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
1198    /// a point on the cubic curve at `t`.
1199    ///
1200    /// Note that `t` varies from `0..=(n_points - 3)`.
1201    #[inline]
1202    pub fn velocity(&self, t: f32) -> P {
1203        let (segment, t) = self.segment(t);
1204        segment.velocity(t)
1205    }
1206
1207    /// Compute the second derivative with respect to t at `t`. This is the instantaneous
1208    /// acceleration of a point on the cubic curve at `t`.
1209    ///
1210    /// Note that `t` varies from `0..=(n_points - 3)`.
1211    #[inline]
1212    pub fn acceleration(&self, t: f32) -> P {
1213        let (segment, t) = self.segment(t);
1214        segment.acceleration(t)
1215    }
1216
1217    /// A flexible iterator used to sample curves with arbitrary functions.
1218    ///
1219    /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1220    /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1221    /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1222    ///
1223    /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1224    /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1225    #[inline]
1226    pub fn iter_samples<'a, 'b: 'a>(
1227        &'b self,
1228        subdivisions: usize,
1229        mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1230    ) -> impl Iterator<Item = P> + 'a {
1231        self.iter_uniformly(subdivisions)
1232            .map(move |t| sample_function(self, t))
1233    }
1234
1235    /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1236    #[inline]
1237    fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1238        let segments = self.segments.len() as f32;
1239        let step = segments / subdivisions as f32;
1240        (0..=subdivisions).map(move |i| i as f32 * step)
1241    }
1242
1243    /// The list of segments contained in this `CubicCurve`.
1244    ///
1245    /// This spline's global `t` value is equal to how many segments it has.
1246    ///
1247    /// All method accepting `t` on `CubicCurve` depends on the global `t`.
1248    /// When sampling over the entire curve, you should either use one of the
1249    /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
1250    #[inline]
1251    pub fn segments(&self) -> &[CubicSegment<P>] {
1252        &self.segments
1253    }
1254
1255    /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1256    pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1257        self.iter_samples(subdivisions, Self::position)
1258    }
1259
1260    /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1261    pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1262        self.iter_samples(subdivisions, Self::velocity)
1263    }
1264
1265    /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1266    pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1267        self.iter_samples(subdivisions, Self::acceleration)
1268    }
1269
1270    #[inline]
1271    /// Adds a segment to the curve
1272    pub fn push_segment(&mut self, segment: CubicSegment<P>) {
1273        self.segments.push(segment);
1274    }
1275
1276    /// Returns the [`CubicSegment`] and local `t` value given a spline's global `t` value.
1277    #[inline]
1278    fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {
1279        if self.segments.len() == 1 {
1280            (&self.segments[0], t)
1281        } else {
1282            let i = (ops::floor(t) as usize).clamp(0, self.segments.len() - 1);
1283            (&self.segments[i], t - i as f32)
1284        }
1285    }
1286}
1287
1288#[cfg(feature = "alloc")]
1289impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
1290    fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
1291        self.segments.extend(iter);
1292    }
1293}
1294
1295#[cfg(feature = "alloc")]
1296impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
1297    type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
1298
1299    type Item = CubicSegment<P>;
1300
1301    fn into_iter(self) -> Self::IntoIter {
1302        self.segments.into_iter()
1303    }
1304}
1305
1306/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
1307#[cfg(feature = "alloc")]
1308pub trait RationalGenerator<P: VectorSpace> {
1309    /// An error type indicating why construction might fail.
1310    type Error;
1311
1312    /// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.
1313    fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error>;
1314}
1315
1316/// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation.
1317/// It is a [`Curve`] with domain `[0, 1]`.
1318///
1319/// Note that the `knot_span` is used only by [compound curves] constructed by chaining these
1320/// together.
1321///
1322/// [compound curves]: RationalCurve
1323/// [`Curve`]: crate::curve::Curve
1324#[derive(Copy, Clone, Debug, Default, PartialEq)]
1325#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1326#[cfg_attr(
1327    feature = "bevy_reflect",
1328    derive(Reflect),
1329    reflect(Debug, Default, Clone)
1330)]
1331pub struct RationalSegment<P: VectorSpace> {
1332    /// The coefficients matrix of the cubic curve.
1333    pub coeff: [P; 4],
1334    /// The homogeneous weight coefficients.
1335    pub weight_coeff: [f32; 4],
1336    /// The width of the domain of this segment.
1337    pub knot_span: f32,
1338}
1339
1340impl<P: VectorSpace<Scalar = f32>> RationalSegment<P> {
1341    /// Instantaneous position of a point at parametric value `t` in `[0, 1]`.
1342    #[inline]
1343    pub fn position(&self, t: f32) -> P {
1344        let [a, b, c, d] = self.coeff;
1345        let [x, y, z, w] = self.weight_coeff;
1346        // Compute a cubic polynomial for the control points
1347        let numerator = a + (b + (c + d * t) * t) * t;
1348        // Compute a cubic polynomial for the weights
1349        let denominator = x + (y + (z + w * t) * t) * t;
1350        numerator / denominator
1351    }
1352
1353    /// Instantaneous velocity of a point at parametric value `t` in `[0, 1]`.
1354    #[inline]
1355    pub fn velocity(&self, t: f32) -> P {
1356        // A derivation for the following equations can be found in "Matrix representation for NURBS
1357        // curves and surfaces" by Choi et al. See equation 19.
1358
1359        let [a, b, c, d] = self.coeff;
1360        let [x, y, z, w] = self.weight_coeff;
1361        // Compute a cubic polynomial for the control points
1362        let numerator = a + (b + (c + d * t) * t) * t;
1363        // Compute a cubic polynomial for the weights
1364        let denominator = x + (y + (z + w * t) * t) * t;
1365
1366        // Compute the derivative of the control point polynomial
1367        let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
1368        // Compute the derivative of the weight polynomial
1369        let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
1370
1371        // Velocity is the first derivative (wrt to the parameter `t`)
1372        // Position = N/D therefore
1373        // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
1374        numerator_derivative / denominator
1375            - numerator * (denominator_derivative / denominator.squared())
1376    }
1377
1378    /// Instantaneous acceleration of a point at parametric value `t` in `[0, 1]`.
1379    #[inline]
1380    pub fn acceleration(&self, t: f32) -> P {
1381        // A derivation for the following equations can be found in "Matrix representation for NURBS
1382        // curves and surfaces" by Choi et al. See equation 20. Note: In come copies of this paper, equation 20
1383        // is printed with the following two errors:
1384        // + The first term has incorrect sign.
1385        // + The second term uses R when it should use the first derivative.
1386
1387        let [a, b, c, d] = self.coeff;
1388        let [x, y, z, w] = self.weight_coeff;
1389        // Compute a cubic polynomial for the control points
1390        let numerator = a + (b + (c + d * t) * t) * t;
1391        // Compute a cubic polynomial for the weights
1392        let denominator = x + (y + (z + w * t) * t) * t;
1393
1394        // Compute the derivative of the control point polynomial
1395        let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
1396        // Compute the derivative of the weight polynomial
1397        let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
1398
1399        // Compute the second derivative of the control point polynomial
1400        let numerator_second_derivative = c * 2.0 + d * 6.0 * t;
1401        // Compute the second derivative of the weight polynomial
1402        let denominator_second_derivative = z * 2.0 + w * 6.0 * t;
1403
1404        // Velocity is the first derivative (wrt to the parameter `t`)
1405        // Position = N/D therefore
1406        // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
1407        // Acceleration = (N/D)'' = ((N' * D - N * D')/D^2)' = N''/D + N' * (-2D'/D^2) + N * (-D''/D^2 + 2D'^2/D^3)
1408        numerator_second_derivative / denominator
1409            + numerator_derivative * (-2.0 * denominator_derivative / denominator.squared())
1410            + numerator
1411                * (-denominator_second_derivative / denominator.squared()
1412                    + 2.0 * denominator_derivative.squared() / denominator.cubed())
1413    }
1414
1415    /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.
1416    #[cfg_attr(
1417        not(feature = "alloc"),
1418        expect(
1419            dead_code,
1420            reason = "Method only used when `alloc` feature is enabled."
1421        )
1422    )]
1423    #[inline]
1424    fn coefficients(
1425        control_points: [P; 4],
1426        weights: [f32; 4],
1427        knot_span: f32,
1428        char_matrix: [[f32; 4]; 4],
1429    ) -> Self {
1430        // An explanation of this use can be found in "Matrix representation for NURBS curves and surfaces"
1431        // by Choi et al. See section "Evaluation of NURB Curves and Surfaces", and equation 16.
1432
1433        let [c0, c1, c2, c3] = char_matrix;
1434        let p = control_points;
1435        let w = weights;
1436        // These are the control point polynomial coefficients, computed by multiplying the characteristic
1437        // matrix by the point matrix.
1438        let coeff = [
1439            p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
1440            p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
1441            p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
1442            p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
1443        ];
1444        // These are the weight polynomial coefficients, computed by multiplying the characteristic
1445        // matrix by the weight matrix.
1446        let weight_coeff = [
1447            w[0] * c0[0] + w[1] * c0[1] + w[2] * c0[2] + w[3] * c0[3],
1448            w[0] * c1[0] + w[1] * c1[1] + w[2] * c1[2] + w[3] * c1[3],
1449            w[0] * c2[0] + w[1] * c2[1] + w[2] * c2[2] + w[3] * c2[3],
1450            w[0] * c3[0] + w[1] * c3[1] + w[2] * c3[2] + w[3] * c3[3],
1451        ];
1452        Self {
1453            coeff,
1454            weight_coeff,
1455            knot_span,
1456        }
1457    }
1458}
1459
1460/// A collection of [`RationalSegment`]s chained into a single parametric curve. It is a [`Curve`]
1461/// with domain `[0, N]`, where `N` is the number of segments.
1462///
1463/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
1464/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
1465///
1466/// [`Curve`]: crate::curve::Curve
1467#[derive(Clone, Debug, PartialEq)]
1468#[cfg(feature = "alloc")]
1469#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1470#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
1471pub struct RationalCurve<P: VectorSpace> {
1472    /// The segments comprising the curve. This must always be nonempty.
1473    segments: Vec<RationalSegment<P>>,
1474}
1475
1476#[cfg(feature = "alloc")]
1477impl<P: VectorSpace<Scalar = f32>> RationalCurve<P> {
1478    /// Create a new curve from a collection of segments. If the collection of segments is empty,
1479    /// a curve cannot be built and `None` will be returned instead.
1480    pub fn from_segments(segments: impl IntoIterator<Item = RationalSegment<P>>) -> Option<Self> {
1481        let segments: Vec<_> = segments.into_iter().collect();
1482        if segments.is_empty() {
1483            None
1484        } else {
1485            Some(Self { segments })
1486        }
1487    }
1488
1489    /// Compute the position of a point on the curve at the parametric value `t`.
1490    ///
1491    /// Note that `t` varies from `0` to `self.length()`.
1492    #[inline]
1493    pub fn position(&self, t: f32) -> P {
1494        let (segment, t) = self.segment(t);
1495        segment.position(t)
1496    }
1497
1498    /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
1499    /// a point on the curve at `t`.
1500    ///
1501    /// Note that `t` varies from `0` to `self.length()`.
1502    #[inline]
1503    pub fn velocity(&self, t: f32) -> P {
1504        let (segment, t) = self.segment(t);
1505        segment.velocity(t)
1506    }
1507
1508    /// Compute the second derivative with respect to t at `t`. This is the instantaneous
1509    /// acceleration of a point on the curve at `t`.
1510    ///
1511    /// Note that `t` varies from `0` to `self.length()`.
1512    #[inline]
1513    pub fn acceleration(&self, t: f32) -> P {
1514        let (segment, t) = self.segment(t);
1515        segment.acceleration(t)
1516    }
1517
1518    /// A flexible iterator used to sample curves with arbitrary functions.
1519    ///
1520    /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1521    /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1522    /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1523    ///
1524    /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1525    /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1526    #[inline]
1527    pub fn iter_samples<'a, 'b: 'a>(
1528        &'b self,
1529        subdivisions: usize,
1530        mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1531    ) -> impl Iterator<Item = P> + 'a {
1532        self.iter_uniformly(subdivisions)
1533            .map(move |t| sample_function(self, t))
1534    }
1535
1536    /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1537    #[inline]
1538    fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1539        let length = self.length();
1540        let step = length / subdivisions as f32;
1541        (0..=subdivisions).map(move |i| i as f32 * step)
1542    }
1543
1544    /// The list of segments contained in this `RationalCurve`.
1545    ///
1546    /// This spline's global `t` value is equal to how many segments it has.
1547    ///
1548    /// All method accepting `t` on `RationalCurve` depends on the global `t`.
1549    /// When sampling over the entire curve, you should either use one of the
1550    /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
1551    #[inline]
1552    pub fn segments(&self) -> &[RationalSegment<P>] {
1553        &self.segments
1554    }
1555
1556    /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1557    pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1558        self.iter_samples(subdivisions, Self::position)
1559    }
1560
1561    /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1562    pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1563        self.iter_samples(subdivisions, Self::velocity)
1564    }
1565
1566    /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1567    pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1568        self.iter_samples(subdivisions, Self::acceleration)
1569    }
1570
1571    /// Adds a segment to the curve.
1572    #[inline]
1573    pub fn push_segment(&mut self, segment: RationalSegment<P>) {
1574        self.segments.push(segment);
1575    }
1576
1577    /// Returns the [`RationalSegment`] and local `t` value given a spline's global `t` value.
1578    /// Input `t` will be clamped to the domain of the curve. Returned value will be in `[0, 1]`.
1579    #[inline]
1580    fn segment(&self, mut t: f32) -> (&RationalSegment<P>, f32) {
1581        if t <= 0.0 {
1582            (&self.segments[0], 0.0)
1583        } else if self.segments.len() == 1 {
1584            (&self.segments[0], t / self.segments[0].knot_span)
1585        } else {
1586            // Try to fit t into each segment domain
1587            for segment in self.segments.iter() {
1588                if t < segment.knot_span {
1589                    // The division here makes t a normalized parameter in [0, 1] that can be properly
1590                    // evaluated against a rational curve segment. See equations 6 & 16 from "Matrix representation
1591                    // of NURBS curves and surfaces" by Choi et al. or equation 3 from "General Matrix
1592                    // Representations for B-Splines" by Qin.
1593                    return (segment, t / segment.knot_span);
1594                }
1595                t -= segment.knot_span;
1596            }
1597            (self.segments.last().unwrap(), 1.0)
1598        }
1599    }
1600
1601    /// Returns the length of the domain of the parametric curve.
1602    #[inline]
1603    pub fn length(&self) -> f32 {
1604        self.segments.iter().map(|segment| segment.knot_span).sum()
1605    }
1606}
1607
1608#[cfg(feature = "alloc")]
1609impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
1610    fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
1611        self.segments.extend(iter);
1612    }
1613}
1614
1615#[cfg(feature = "alloc")]
1616impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
1617    type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
1618
1619    type Item = RationalSegment<P>;
1620
1621    fn into_iter(self) -> Self::IntoIter {
1622        self.segments.into_iter()
1623    }
1624}
1625
1626impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
1627    fn from(value: CubicSegment<P>) -> Self {
1628        Self {
1629            coeff: value.coeff,
1630            weight_coeff: [1.0, 0.0, 0.0, 0.0],
1631            knot_span: 1.0, // Cubic curves are uniform, so every segment has domain [0, 1).
1632        }
1633    }
1634}
1635
1636#[cfg(feature = "alloc")]
1637impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
1638    fn from(value: CubicCurve<P>) -> Self {
1639        Self {
1640            segments: value.segments.into_iter().map(Into::into).collect(),
1641        }
1642    }
1643}
1644
1645#[cfg(feature = "alloc")]
1646#[cfg(test)]
1647mod tests {
1648    use crate::{
1649        cubic_splines::{
1650            CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
1651            RationalGenerator,
1652        },
1653        ops::{self, FloatPow},
1654    };
1655    use alloc::vec::Vec;
1656    use glam::{vec2, Vec2};
1657
1658    /// How close two floats can be and still be considered equal
1659    const FLOAT_EQ: f32 = 1e-5;
1660
1661    /// Sweep along the full length of a 3D cubic Bezier, and manually check the position.
1662    #[test]
1663    fn cubic() {
1664        const N_SAMPLES: usize = 1000;
1665        let points = [[
1666            vec2(-1.0, -20.0),
1667            vec2(3.0, 2.0),
1668            vec2(5.0, 3.0),
1669            vec2(9.0, 8.0),
1670        ]];
1671        let bezier = CubicBezier::new(points).to_curve().unwrap();
1672        for i in 0..=N_SAMPLES {
1673            let t = i as f32 / N_SAMPLES as f32; // Check along entire length
1674            assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
1675        }
1676    }
1677
1678    /// Manual, hardcoded function for computing the position along a cubic bezier.
1679    fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {
1680        let p = points;
1681        p[0] * (1.0 - t).cubed()
1682            + 3.0 * p[1] * t * (1.0 - t).squared()
1683            + 3.0 * p[2] * t.squared() * (1.0 - t)
1684            + p[3] * t.cubed()
1685    }
1686
1687    /// Basic cubic Bezier easing test to verify the shape of the curve.
1688    #[test]
1689    fn easing_simple() {
1690        // A curve similar to ease-in-out, but symmetric
1691        let bezier = CubicSegment::new_bezier_easing([1.0, 0.0], [0.0, 1.0]);
1692        assert_eq!(bezier.ease(0.0), 0.0);
1693        assert!(bezier.ease(0.2) < 0.2); // tests curve
1694        assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry
1695        assert!(bezier.ease(0.8) > 0.8); // tests curve
1696        assert_eq!(bezier.ease(1.0), 1.0);
1697    }
1698
1699    /// A curve that forms an upside-down "U", that should extend below 0.0. Useful for animations
1700    /// that go beyond the start and end positions, e.g. bouncing.
1701    #[test]
1702    fn easing_overshoot() {
1703        // A curve that forms an upside-down "U", that should extend above 1.0
1704        let bezier = CubicSegment::new_bezier_easing([0.0, 2.0], [1.0, 2.0]);
1705        assert_eq!(bezier.ease(0.0), 0.0);
1706        assert!(bezier.ease(0.5) > 1.5);
1707        assert_eq!(bezier.ease(1.0), 1.0);
1708    }
1709
1710    /// A curve that forms a "U", that should extend below 0.0. Useful for animations that go beyond
1711    /// the start and end positions, e.g. bouncing.
1712    #[test]
1713    fn easing_undershoot() {
1714        let bezier = CubicSegment::new_bezier_easing([0.0, -2.0], [1.0, -2.0]);
1715        assert_eq!(bezier.ease(0.0), 0.0);
1716        assert!(bezier.ease(0.5) < -0.5);
1717        assert_eq!(bezier.ease(1.0), 1.0);
1718    }
1719
1720    /// Test that a simple cardinal spline passes through all of its control points with
1721    /// the correct tangents.
1722    #[test]
1723    fn cardinal_control_pts() {
1724        use super::CubicCardinalSpline;
1725
1726        let tension = 0.2;
1727        let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];
1728        let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3])
1729            .to_curve()
1730            .unwrap();
1731
1732        // Positions at segment endpoints
1733        assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));
1734        assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ));
1735        assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ));
1736        assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ));
1737
1738        // Tangents at segment endpoints
1739        assert!(curve
1740            .velocity(0.)
1741            .abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ));
1742        assert!(curve
1743            .velocity(1.)
1744            .abs_diff_eq((p2 - p0) * tension, FLOAT_EQ));
1745        assert!(curve
1746            .velocity(2.)
1747            .abs_diff_eq((p3 - p1) * tension, FLOAT_EQ));
1748        assert!(curve
1749            .velocity(3.)
1750            .abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ));
1751    }
1752
1753    /// Test that [`RationalCurve`] properly generalizes [`CubicCurve`]. A Cubic upgraded to a rational
1754    /// should produce pretty much the same output.
1755    #[test]
1756    fn cubic_to_rational() {
1757        const EPSILON: f32 = 0.00001;
1758
1759        let points = [
1760            vec2(0.0, 0.0),
1761            vec2(1.0, 1.0),
1762            vec2(1.0, 1.0),
1763            vec2(2.0, -1.0),
1764            vec2(3.0, 1.0),
1765            vec2(0.0, 0.0),
1766        ];
1767
1768        let b_spline = CubicBSpline::new(points).to_curve().unwrap();
1769        let rational_b_spline = RationalCurve::from(b_spline.clone());
1770
1771        /// Tests if two vectors of points are approximately the same
1772        fn compare_vectors(cubic_curve: Vec<Vec2>, rational_curve: Vec<Vec2>, name: &str) {
1773            assert_eq!(
1774                cubic_curve.len(),
1775                rational_curve.len(),
1776                "{name} vector lengths mismatch"
1777            );
1778            for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() {
1779                assert!(
1780                    a.distance(*b) < EPSILON,
1781                    "Mismatch at {name} value {i}. CubicCurve: {a} Converted RationalCurve: {b}",
1782                );
1783            }
1784        }
1785
1786        // Both curves should yield the same values
1787        let cubic_positions: Vec<_> = b_spline.iter_positions(10).collect();
1788        let rational_positions: Vec<_> = rational_b_spline.iter_positions(10).collect();
1789        compare_vectors(cubic_positions, rational_positions, "position");
1790
1791        let cubic_velocities: Vec<_> = b_spline.iter_velocities(10).collect();
1792        let rational_velocities: Vec<_> = rational_b_spline.iter_velocities(10).collect();
1793        compare_vectors(cubic_velocities, rational_velocities, "velocity");
1794
1795        let cubic_accelerations: Vec<_> = b_spline.iter_accelerations(10).collect();
1796        let rational_accelerations: Vec<_> = rational_b_spline.iter_accelerations(10).collect();
1797        compare_vectors(cubic_accelerations, rational_accelerations, "acceleration");
1798    }
1799
1800    /// Test that a nurbs curve can approximate a portion of a circle.
1801    #[test]
1802    fn nurbs_circular_arc() {
1803        use core::f32::consts::FRAC_PI_2;
1804        const EPSILON: f32 = 0.0000001;
1805
1806        // The following NURBS parameters were determined by constraining the first two
1807        // points to the line y=1, the second two points to the line x=1, and the distance
1808        // between each pair of points to be equal. One can solve the weights by assuming the
1809        // first and last weights to be one, the intermediate weights to be equal, and
1810        // subjecting ones self to a lot of tedious matrix algebra.
1811
1812        let alpha = FRAC_PI_2;
1813        let leg = 2.0 * ops::sin(alpha / 2.0) / (1.0 + 2.0 * ops::cos(alpha / 2.0));
1814        let weight = (1.0 + 2.0 * ops::cos(alpha / 2.0)) / 3.0;
1815        let points = [
1816            vec2(1.0, 0.0),
1817            vec2(1.0, leg),
1818            vec2(leg, 1.0),
1819            vec2(0.0, 1.0),
1820        ];
1821        let weights = [1.0, weight, weight, 1.0];
1822        let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
1823        let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();
1824        let curve = spline.to_curve().unwrap();
1825        for (i, point) in curve.iter_positions(10).enumerate() {
1826            assert!(
1827                ops::abs(point.length() - 1.0) < EPSILON,
1828                "Point {i} is not on the unit circle: {point:?} has length {}",
1829                point.length()
1830            );
1831        }
1832    }
1833}