#[cfg(feature = "curve")]
mod curve_impls;
use crate::{
ops::{self, FloatPow},
Vec2, VectorSpace,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use thiserror::Error;
#[cfg(feature = "alloc")]
use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicBezier<P: VectorSpace> {
pub control_points: Vec<[P; 4]>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBezier<P> {
pub fn new(control_points: impl IntoIterator<Item = [P; 4]>) -> Self {
Self {
control_points: control_points.into_iter().collect(),
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBezier<P> {
type Error = CubicBezierError;
#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.iter()
.map(|p| CubicSegment::new_bezier(*p))
.collect_vec();
if segments.is_empty() {
Err(CubicBezierError)
} else {
Ok(CubicCurve { segments })
}
}
}
#[derive(Clone, Debug, Error)]
#[error("Unable to generate cubic curve: at least one set of control points is required")]
pub struct CubicBezierError;
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicHermite<P: VectorSpace> {
pub control_points: Vec<(P, P)>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicHermite<P> {
pub fn new(
control_points: impl IntoIterator<Item = P>,
tangents: impl IntoIterator<Item = P>,
) -> Self {
Self {
control_points: control_points.into_iter().zip(tangents).collect(),
}
}
#[inline]
fn char_matrix(&self) -> [[f32; 4]; 4] {
[
[1., 0., 0., 0.],
[0., 1., 0., 0.],
[-3., -2., 3., -1.],
[2., 1., -2., 1.],
]
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(2)
.map(|p| {
let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);
CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
})
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.iter()
.circular_tuple_windows()
.map(|(&j0, &j1)| {
let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1);
CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
})
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicCardinalSpline<P: VectorSpace> {
pub tension: f32,
pub control_points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicCardinalSpline<P> {
pub fn new(tension: f32, control_points: impl IntoIterator<Item = P>) -> Self {
Self {
tension,
control_points: control_points.into_iter().collect(),
}
}
pub fn new_catmull_rom(control_points: impl IntoIterator<Item = P>) -> Self {
Self {
tension: 0.5,
control_points: control_points.into_iter().collect(),
}
}
#[inline]
fn char_matrix(&self) -> [[f32; 4]; 4] {
let s = self.tension;
[
[0., 1., 0., 0.],
[-s, 0., s, 0.],
[2. * s, s - 3., 3. - 2. * s, -s],
[-s, 2. - s, s - 2., s],
]
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let length = self.control_points.len();
if length < 2 {
return Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
});
}
let mirrored_first = self.control_points[0] * 2. - self.control_points[1];
let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2];
let extended_control_points = once(&mirrored_first)
.chain(self.control_points.iter())
.chain(once(&mirrored_last));
let segments = extended_control_points
.tuple_windows()
.map(|(&p0, &p1, &p2, &p3)| {
CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
})
.collect_vec();
Ok(CubicCurve { segments })
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let len = self.control_points.len();
if len < 2 {
return Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
});
}
let first_segment = {
let p0 = self.control_points[len - 1];
let p1 = self.control_points[0];
let p2 = self.control_points[1 % len];
let p3 = self.control_points[2 % len];
CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
};
let later_segments = self
.control_points
.iter()
.circular_tuple_windows()
.map(|(&p0, &p1, &p2, &p3)| {
CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
})
.take(len - 1);
let mut segments = Vec::with_capacity(len);
segments.push(first_segment);
segments.extend(later_segments);
Ok(CubicCurve { segments })
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicBSpline<P: VectorSpace> {
pub control_points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBSpline<P> {
pub fn new(control_points: impl IntoIterator<Item = P>) -> Self {
Self {
control_points: control_points.into_iter().collect(),
}
}
#[inline]
fn char_matrix(&self) -> [[f32; 4]; 4] {
let mut char_matrix = [
[1.0, 4.0, 1.0, 0.0],
[-3.0, 0.0, 3.0, 0.0],
[3.0, -6.0, 3.0, 0.0],
[-1.0, 3.0, -3.0, 1.0],
];
char_matrix
.iter_mut()
.for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0));
char_matrix
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(4)
.map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], self.char_matrix()))
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 4,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.iter()
.circular_tuple_windows()
.map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix()))
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[derive(Clone, Debug, Error)]
pub enum CubicNurbsError {
#[error("Wrong number of knots: expected {expected}, provided {provided}")]
KnotsNumberMismatch {
expected: usize,
provided: usize,
},
#[error("Invalid knots: contains descending knot pair")]
DescendingKnots,
#[error("Invalid knots: all knots are equal")]
ConstantKnots,
#[error("Incorrect number of weights: expected {expected}, provided {provided}")]
WeightsNumberMismatch {
expected: usize,
provided: usize,
},
#[error("Not enough control points, at least 4 are required, {provided} were provided")]
NotEnoughControlPoints {
provided: usize,
},
}
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicNurbs<P: VectorSpace> {
pub control_points: Vec<P>,
pub weights: Vec<f32>,
pub knots: Vec<f32>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicNurbs<P> {
pub fn new(
control_points: impl IntoIterator<Item = P>,
weights: Option<impl IntoIterator<Item = f32>>,
knots: Option<impl IntoIterator<Item = f32>>,
) -> Result<Self, CubicNurbsError> {
let mut control_points: Vec<P> = control_points.into_iter().collect();
let control_points_len = control_points.len();
if control_points_len < 4 {
return Err(CubicNurbsError::NotEnoughControlPoints {
provided: control_points_len,
});
}
let weights: Vec<f32> = weights
.map(|ws| ws.into_iter().collect())
.unwrap_or_else(|| vec![1.0; control_points_len]);
let mut knots: Vec<f32> = knots.map(|ks| ks.into_iter().collect()).unwrap_or_else(|| {
Self::open_uniform_knots(control_points_len)
.expect("The amount of control points was checked")
});
let expected_knots_len = Self::knots_len(control_points_len);
if knots.len() != expected_knots_len {
return Err(CubicNurbsError::KnotsNumberMismatch {
expected: expected_knots_len,
provided: knots.len(),
});
}
if knots.windows(2).any(|win| win[0] > win[1]) {
return Err(CubicNurbsError::DescendingKnots);
}
if knots.windows(2).all(|win| win[0] == win[1]) {
return Err(CubicNurbsError::ConstantKnots);
}
if weights.len() != control_points_len {
return Err(CubicNurbsError::WeightsNumberMismatch {
expected: control_points_len,
provided: weights.len(),
});
}
let curve_length = (control_points.len() - 3) as f32;
let min = *knots.first().unwrap();
let max = *knots.last().unwrap();
let knot_delta = max - min;
knots = knots
.into_iter()
.map(|k| k - min)
.map(|k| k * curve_length / knot_delta)
.collect();
control_points
.iter_mut()
.zip(weights.iter())
.for_each(|(p, w)| *p = *p * *w);
Ok(Self {
control_points,
weights,
knots,
})
}
pub fn uniform_knots(control_points: usize) -> Option<Vec<f32>> {
if control_points < 4 {
return None;
}
Some(
(0..Self::knots_len(control_points))
.map(|v| v as f32)
.collect(),
)
}
pub fn open_uniform_knots(control_points: usize) -> Option<Vec<f32>> {
if control_points < 4 {
return None;
}
let last_knots_value = control_points - 3;
Some(
core::iter::repeat_n(0.0, 4)
.chain((1..last_knots_value).map(|v| v as f32))
.chain(core::iter::repeat_n(last_knots_value as f32, 4))
.collect(),
)
}
#[inline]
const fn knots_len(control_points_len: usize) -> usize {
control_points_len + 4
}
fn generate_matrix(knots: &[f32; 8]) -> [[f32; 4]; 4] {
let t = knots;
let m00 = (t[4] - t[3]).squared() / ((t[4] - t[2]) * (t[4] - t[1]));
let m02 = (t[3] - t[2]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2]));
let m22 = 3.0 * (t[4] - t[3]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
let m33 = (t[4] - t[3]).squared() / ((t[6] - t[3]) * (t[5] - t[3]));
let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).squared() / ((t[5] - t[3]) * (t[5] - t[2]));
[
[m00, 1.0 - m00 - m02, m02, 0.0],
[-3.0 * m00, 3.0 * m00 - m12, m12, 0.0],
[3.0 * m00, -3.0 * m00 - m22, m22, 0.0],
[-m00, m00 - m32 - m33, m32, m33],
]
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> RationalGenerator<P> for CubicNurbs<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(4)
.zip(self.weights.windows(4))
.zip(self.knots.windows(8))
.filter(|(_, knots)| knots[4] - knots[3] > 0.0)
.map(|((points, weights), knots)| {
let span = knots[4] - knots[3];
let coefficient_knots = knots.try_into().expect("Knot windows are of length 6");
let matrix = Self::generate_matrix(coefficient_knots);
RationalSegment::coefficients(
points.try_into().expect("Point windows are of length 4"),
weights.try_into().expect("Weight windows are of length 4"),
span,
matrix,
)
})
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 4,
given: self.control_points.len(),
})
} else {
Ok(RationalCurve { segments })
}
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct LinearSpline<P: VectorSpace> {
pub points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> LinearSpline<P> {
pub fn new(points: impl IntoIterator<Item = P>) -> Self {
Self {
points: points.into_iter().collect(),
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.points
.windows(2)
.map(|points| {
let a = points[0];
let b = points[1];
CubicSegment {
coeff: [a, b - a, P::default(), P::default()],
}
})
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.points
.iter()
.circular_tuple_windows()
.map(|(&a, &b)| CubicSegment {
coeff: [a, b - a, P::default(), P::default()],
})
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
#[derive(Clone, Debug, Error)]
#[error("Not enough data to build curve: needed at least {expected} control points but was only given {given}")]
pub struct InsufficientDataError {
expected: usize,
given: usize,
}
#[cfg(feature = "alloc")]
pub trait CubicGenerator<P: VectorSpace> {
type Error;
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;
}
#[cfg(feature = "alloc")]
pub trait CyclicCubicGenerator<P: VectorSpace> {
type Error;
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error>;
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Clone)
)]
pub struct CubicSegment<P: VectorSpace> {
pub coeff: [P; 4],
}
impl<P: VectorSpace<Scalar = f32>> CubicSegment<P> {
#[inline]
pub fn position(&self, t: f32) -> P {
let [a, b, c, d] = self.coeff;
a + (b + (c + d * t) * t) * t
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let [_, b, c, d] = self.coeff;
b + (c * 2.0 + d * 3.0 * t) * t
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let [_, _, c, d] = self.coeff;
c * 2.0 + d * 6.0 * t
}
pub fn new_bezier(points: [P; 4]) -> Self {
let char_matrix = [
[1., 0., 0., 0.],
[-3., 3., 0., 0.],
[3., -6., 3., 0.],
[-1., 3., -3., 1.],
];
Self::coefficients(points, char_matrix)
}
#[inline]
fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
let [c0, c1, c2, c3] = char_matrix;
let coeff = [
p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
];
Self { coeff }
}
#[inline]
pub fn iter_samples<'a, 'b: 'a>(
&'b self,
subdivisions: usize,
mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
) -> impl Iterator<Item = P> + 'a {
self.iter_uniformly(subdivisions)
.map(move |t| sample_function(self, t))
}
#[inline]
pub fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
let step = 1.0 / subdivisions as f32;
(0..=subdivisions).map(move |i| i as f32 * step)
}
pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::position)
}
pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::velocity)
}
pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::acceleration)
}
}
impl CubicSegment<Vec2> {
#[cfg(feature = "alloc")]
pub fn new_bezier_easing(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
Self::new_bezier([p0, p1.into(), p2.into(), p3])
}
const MAX_ERROR: f32 = 1e-5;
const MAX_ITERS: u8 = 8;
#[inline]
pub fn ease(&self, time: f32) -> f32 {
let x = time.clamp(0.0, 1.0);
self.find_y_given_x(x)
}
#[inline]
fn find_y_given_x(&self, x: f32) -> f32 {
let mut t_guess = x;
let mut pos_guess = Vec2::ZERO;
for _ in 0..Self::MAX_ITERS {
pos_guess = self.position(t_guess);
let error = pos_guess.x - x;
if ops::abs(error) <= Self::MAX_ERROR {
break;
}
let slope = self.velocity(t_guess).x; t_guess -= error / slope;
}
pos_guess.y
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct CubicCurve<P: VectorSpace> {
segments: Vec<CubicSegment<P>>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> CubicCurve<P> {
pub fn from_segments(segments: impl IntoIterator<Item = CubicSegment<P>>) -> Option<Self> {
let segments: Vec<_> = segments.into_iter().collect();
if segments.is_empty() {
None
} else {
Some(Self { segments })
}
}
#[inline]
pub fn position(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.position(t)
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.velocity(t)
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.acceleration(t)
}
#[inline]
pub fn iter_samples<'a, 'b: 'a>(
&'b self,
subdivisions: usize,
mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
) -> impl Iterator<Item = P> + 'a {
self.iter_uniformly(subdivisions)
.map(move |t| sample_function(self, t))
}
#[inline]
fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
let segments = self.segments.len() as f32;
let step = segments / subdivisions as f32;
(0..=subdivisions).map(move |i| i as f32 * step)
}
#[inline]
pub fn segments(&self) -> &[CubicSegment<P>] {
&self.segments
}
pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::position)
}
pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::velocity)
}
pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::acceleration)
}
#[inline]
pub fn push_segment(&mut self, segment: CubicSegment<P>) {
self.segments.push(segment);
}
#[inline]
fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {
if self.segments.len() == 1 {
(&self.segments[0], t)
} else {
let i = (ops::floor(t) as usize).clamp(0, self.segments.len() - 1);
(&self.segments[i], t - i as f32)
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
type Item = CubicSegment<P>;
fn into_iter(self) -> Self::IntoIter {
self.segments.into_iter()
}
}
#[cfg(feature = "alloc")]
pub trait RationalGenerator<P: VectorSpace> {
type Error;
fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error>;
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Clone)
)]
pub struct RationalSegment<P: VectorSpace> {
pub coeff: [P; 4],
pub weight_coeff: [f32; 4],
pub knot_span: f32,
}
impl<P: VectorSpace<Scalar = f32>> RationalSegment<P> {
#[inline]
pub fn position(&self, t: f32) -> P {
let [a, b, c, d] = self.coeff;
let [x, y, z, w] = self.weight_coeff;
let numerator = a + (b + (c + d * t) * t) * t;
let denominator = x + (y + (z + w * t) * t) * t;
numerator / denominator
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let [a, b, c, d] = self.coeff;
let [x, y, z, w] = self.weight_coeff;
let numerator = a + (b + (c + d * t) * t) * t;
let denominator = x + (y + (z + w * t) * t) * t;
let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
numerator_derivative / denominator
- numerator * (denominator_derivative / denominator.squared())
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let [a, b, c, d] = self.coeff;
let [x, y, z, w] = self.weight_coeff;
let numerator = a + (b + (c + d * t) * t) * t;
let denominator = x + (y + (z + w * t) * t) * t;
let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
let numerator_second_derivative = c * 2.0 + d * 6.0 * t;
let denominator_second_derivative = z * 2.0 + w * 6.0 * t;
numerator_second_derivative / denominator
+ numerator_derivative * (-2.0 * denominator_derivative / denominator.squared())
+ numerator
* (-denominator_second_derivative / denominator.squared()
+ 2.0 * denominator_derivative.squared() / denominator.cubed())
}
#[cfg_attr(
not(feature = "alloc"),
expect(
dead_code,
reason = "Method only used when `alloc` feature is enabled."
)
)]
#[inline]
fn coefficients(
control_points: [P; 4],
weights: [f32; 4],
knot_span: f32,
char_matrix: [[f32; 4]; 4],
) -> Self {
let [c0, c1, c2, c3] = char_matrix;
let p = control_points;
let w = weights;
let coeff = [
p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
];
let weight_coeff = [
w[0] * c0[0] + w[1] * c0[1] + w[2] * c0[2] + w[3] * c0[3],
w[0] * c1[0] + w[1] * c1[1] + w[2] * c1[2] + w[3] * c1[3],
w[0] * c2[0] + w[1] * c2[1] + w[2] * c2[2] + w[3] * c2[3],
w[0] * c3[0] + w[1] * c3[1] + w[2] * c3[2] + w[3] * c3[3],
];
Self {
coeff,
weight_coeff,
knot_span,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct RationalCurve<P: VectorSpace> {
segments: Vec<RationalSegment<P>>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace<Scalar = f32>> RationalCurve<P> {
pub fn from_segments(segments: impl IntoIterator<Item = RationalSegment<P>>) -> Option<Self> {
let segments: Vec<_> = segments.into_iter().collect();
if segments.is_empty() {
None
} else {
Some(Self { segments })
}
}
#[inline]
pub fn position(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.position(t)
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.velocity(t)
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.acceleration(t)
}
#[inline]
pub fn iter_samples<'a, 'b: 'a>(
&'b self,
subdivisions: usize,
mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
) -> impl Iterator<Item = P> + 'a {
self.iter_uniformly(subdivisions)
.map(move |t| sample_function(self, t))
}
#[inline]
fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
let length = self.length();
let step = length / subdivisions as f32;
(0..=subdivisions).map(move |i| i as f32 * step)
}
#[inline]
pub fn segments(&self) -> &[RationalSegment<P>] {
&self.segments
}
pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::position)
}
pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::velocity)
}
pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::acceleration)
}
#[inline]
pub fn push_segment(&mut self, segment: RationalSegment<P>) {
self.segments.push(segment);
}
#[inline]
fn segment(&self, mut t: f32) -> (&RationalSegment<P>, f32) {
if t <= 0.0 {
(&self.segments[0], 0.0)
} else if self.segments.len() == 1 {
(&self.segments[0], t / self.segments[0].knot_span)
} else {
for segment in self.segments.iter() {
if t < segment.knot_span {
return (segment, t / segment.knot_span);
}
t -= segment.knot_span;
}
(self.segments.last().unwrap(), 1.0)
}
}
#[inline]
pub fn length(&self) -> f32 {
self.segments.iter().map(|segment| segment.knot_span).sum()
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
type Item = RationalSegment<P>;
fn into_iter(self) -> Self::IntoIter {
self.segments.into_iter()
}
}
impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
fn from(value: CubicSegment<P>) -> Self {
Self {
coeff: value.coeff,
weight_coeff: [1.0, 0.0, 0.0, 0.0],
knot_span: 1.0, }
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
fn from(value: CubicCurve<P>) -> Self {
Self {
segments: value.segments.into_iter().map(Into::into).collect(),
}
}
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use crate::{
cubic_splines::{
CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
RationalGenerator,
},
ops::{self, FloatPow},
};
use alloc::vec::Vec;
use glam::{vec2, Vec2};
const FLOAT_EQ: f32 = 1e-5;
#[test]
fn cubic() {
const N_SAMPLES: usize = 1000;
let points = [[
vec2(-1.0, -20.0),
vec2(3.0, 2.0),
vec2(5.0, 3.0),
vec2(9.0, 8.0),
]];
let bezier = CubicBezier::new(points).to_curve().unwrap();
for i in 0..=N_SAMPLES {
let t = i as f32 / N_SAMPLES as f32; assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
}
}
fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {
let p = points;
p[0] * (1.0 - t).cubed()
+ 3.0 * p[1] * t * (1.0 - t).squared()
+ 3.0 * p[2] * t.squared() * (1.0 - t)
+ p[3] * t.cubed()
}
#[test]
fn easing_simple() {
let bezier = CubicSegment::new_bezier_easing([1.0, 0.0], [0.0, 1.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.2) < 0.2); assert_eq!(bezier.ease(0.5), 0.5); assert!(bezier.ease(0.8) > 0.8); assert_eq!(bezier.ease(1.0), 1.0);
}
#[test]
fn easing_overshoot() {
let bezier = CubicSegment::new_bezier_easing([0.0, 2.0], [1.0, 2.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.5) > 1.5);
assert_eq!(bezier.ease(1.0), 1.0);
}
#[test]
fn easing_undershoot() {
let bezier = CubicSegment::new_bezier_easing([0.0, -2.0], [1.0, -2.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.5) < -0.5);
assert_eq!(bezier.ease(1.0), 1.0);
}
#[test]
fn cardinal_control_pts() {
use super::CubicCardinalSpline;
let tension = 0.2;
let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];
let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3])
.to_curve()
.unwrap();
assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));
assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ));
assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ));
assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ));
assert!(curve
.velocity(0.)
.abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ));
assert!(curve
.velocity(1.)
.abs_diff_eq((p2 - p0) * tension, FLOAT_EQ));
assert!(curve
.velocity(2.)
.abs_diff_eq((p3 - p1) * tension, FLOAT_EQ));
assert!(curve
.velocity(3.)
.abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ));
}
#[test]
fn cubic_to_rational() {
const EPSILON: f32 = 0.00001;
let points = [
vec2(0.0, 0.0),
vec2(1.0, 1.0),
vec2(1.0, 1.0),
vec2(2.0, -1.0),
vec2(3.0, 1.0),
vec2(0.0, 0.0),
];
let b_spline = CubicBSpline::new(points).to_curve().unwrap();
let rational_b_spline = RationalCurve::from(b_spline.clone());
fn compare_vectors(cubic_curve: Vec<Vec2>, rational_curve: Vec<Vec2>, name: &str) {
assert_eq!(
cubic_curve.len(),
rational_curve.len(),
"{name} vector lengths mismatch"
);
for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() {
assert!(
a.distance(*b) < EPSILON,
"Mismatch at {name} value {i}. CubicCurve: {a} Converted RationalCurve: {b}",
);
}
}
let cubic_positions: Vec<_> = b_spline.iter_positions(10).collect();
let rational_positions: Vec<_> = rational_b_spline.iter_positions(10).collect();
compare_vectors(cubic_positions, rational_positions, "position");
let cubic_velocities: Vec<_> = b_spline.iter_velocities(10).collect();
let rational_velocities: Vec<_> = rational_b_spline.iter_velocities(10).collect();
compare_vectors(cubic_velocities, rational_velocities, "velocity");
let cubic_accelerations: Vec<_> = b_spline.iter_accelerations(10).collect();
let rational_accelerations: Vec<_> = rational_b_spline.iter_accelerations(10).collect();
compare_vectors(cubic_accelerations, rational_accelerations, "acceleration");
}
#[test]
fn nurbs_circular_arc() {
use core::f32::consts::FRAC_PI_2;
const EPSILON: f32 = 0.0000001;
let alpha = FRAC_PI_2;
let leg = 2.0 * ops::sin(alpha / 2.0) / (1.0 + 2.0 * ops::cos(alpha / 2.0));
let weight = (1.0 + 2.0 * ops::cos(alpha / 2.0)) / 3.0;
let points = [
vec2(1.0, 0.0),
vec2(1.0, leg),
vec2(leg, 1.0),
vec2(0.0, 1.0),
];
let weights = [1.0, weight, weight, 1.0];
let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();
let curve = spline.to_curve().unwrap();
for (i, point) in curve.iter_positions(10).enumerate() {
assert!(
ops::abs(point.length() - 1.0) < EPSILON,
"Point {i} is not on the unit circle: {point:?} has length {}",
point.length()
);
}
}
}