#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float as _;
#[cfg(not(feature = "std"))]
use alloc::vec::{self, Vec};
pub trait PathEvaluate2D {
fn evaluate(&self, default: [f32; 2], t: f32) -> [f32; 2];
fn tangent(&self, default: [f32; 2], t: f32) -> [f32; 2];
}
#[derive(Debug, Clone)]
pub struct CatmullRomSpline {
points: Vec<[f32; 2]>,
tension: f32,
}
impl CatmullRomSpline {
pub fn new(points: Vec<[f32; 2]>) -> Self {
Self {
points,
tension: 0.5,
}
}
pub fn tension(mut self, t: f32) -> Self {
self.tension = t.max(0.0);
self
}
pub fn point_count(&self) -> usize {
self.points.len()
}
pub fn segment_count(&self) -> usize {
if self.points.len() < 2 {
0
} else {
self.points.len() - 1
}
}
pub fn points(&self) -> &[[f32; 2]] {
&self.points
}
fn segment_control_points(&self, i: usize) -> ([f32; 2], [f32; 2]) {
let n = self.points.len();
assert!(i + 1 < n);
let p0 = if i == 0 {
self.points[0]
} else {
self.points[i - 1]
};
let p1 = self.points[i];
let p2 = self.points[i + 1];
let p3 = if i + 2 < n {
self.points[i + 2]
} else {
self.points[n - 1]
};
let alpha = self.tension;
let cp1 = [
p1[0] + (p2[0] - p0[0]) * alpha / 3.0,
p1[1] + (p2[1] - p0[1]) * alpha / 3.0,
];
let cp2 = [
p2[0] - (p3[0] - p1[0]) * alpha / 3.0,
p2[1] - (p3[1] - p1[1]) * alpha / 3.0,
];
(cp1, cp2)
}
fn eval_cubic(p0: [f32; 2], cp1: [f32; 2], cp2: [f32; 2], p3: [f32; 2], t: f32) -> [f32; 2] {
let inv = 1.0 - t;
let inv2 = inv * inv;
let inv3 = inv2 * inv;
let t2 = t * t;
let t3 = t2 * t;
[
inv3 * p0[0] + 3.0 * inv2 * t * cp1[0] + 3.0 * inv * t2 * cp2[0] + t3 * p3[0],
inv3 * p0[1] + 3.0 * inv2 * t * cp1[1] + 3.0 * inv * t2 * cp2[1] + t3 * p3[1],
]
}
fn eval_cubic_derivative(
p0: [f32; 2],
cp1: [f32; 2],
cp2: [f32; 2],
p3: [f32; 2],
t: f32,
) -> [f32; 2] {
let inv = 1.0 - t;
let inv2 = inv * inv;
let t2 = t * t;
[
3.0 * inv2 * (cp1[0] - p0[0])
+ 6.0 * inv * t * (cp2[0] - cp1[0])
+ 3.0 * t2 * (p3[0] - cp2[0]),
3.0 * inv2 * (cp1[1] - p0[1])
+ 6.0 * inv * t * (cp2[1] - cp1[1])
+ 3.0 * t2 * (p3[1] - cp2[1]),
]
}
fn map_t(&self, t: f32) -> (usize, f32) {
let seg_count = self.segment_count();
if seg_count == 0 {
return (0, 0.0);
}
let t = t.clamp(0.0, 1.0);
let scaled = t * seg_count as f32;
let idx = (scaled.floor() as usize).min(seg_count - 1);
let local = scaled - idx as f32;
(idx, local.clamp(0.0, 1.0))
}
}
impl PathEvaluate2D for CatmullRomSpline {
fn evaluate(&self, default: [f32; 2], t: f32) -> [f32; 2] {
match self.points.len() {
0 => default,
1 => self.points[0],
_ => {
let (idx, local_t) = self.map_t(t);
let (cp1, cp2) = self.segment_control_points(idx);
Self::eval_cubic(self.points[idx], cp1, cp2, self.points[idx + 1], local_t)
}
}
}
fn tangent(&self, default: [f32; 2], t: f32) -> [f32; 2] {
match self.points.len() {
0 | 1 => default,
_ => {
let (idx, local_t) = self.map_t(t);
let (cp1, cp2) = self.segment_control_points(idx);
Self::eval_cubic_derivative(
self.points[idx],
cp1,
cp2,
self.points[idx + 1],
local_t,
)
}
}
}
}
pub fn tangent_angle(tangent: [f32; 2]) -> f32 {
tangent[1].atan2(tangent[0])
}
pub fn tangent_angle_deg(tangent: [f32; 2]) -> f32 {
tangent_angle(tangent).to_degrees()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{format, string::String, vec, vec::Vec};
#[test]
fn catmull_rom_endpoints() {
let spline = CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 50.0], [200.0, 0.0]]);
let start = spline.evaluate([0.0, 0.0], 0.0);
let end = spline.evaluate([0.0, 0.0], 1.0);
assert!(
(start[0]).abs() < 1e-4,
"Expected start x~0, got {}",
start[0]
);
assert!(
(start[1]).abs() < 1e-4,
"Expected start y~0, got {}",
start[1]
);
assert!(
(end[0] - 200.0).abs() < 1e-4,
"Expected end x~200, got {}",
end[0]
);
assert!((end[1]).abs() < 1e-4, "Expected end y~0, got {}", end[1]);
}
#[test]
fn catmull_rom_passes_through_midpoint() {
let spline = CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 100.0], [200.0, 0.0]]);
let mid = spline.evaluate([0.0, 0.0], 0.5);
assert!(
(mid[0] - 100.0).abs() < 1e-3,
"Expected x~100 at t=0.5, got {}",
mid[0]
);
assert!(
(mid[1] - 100.0).abs() < 1e-3,
"Expected y~100 at t=0.5, got {}",
mid[1]
);
}
#[test]
fn catmull_rom_four_points() {
let spline =
CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 50.0], [200.0, 0.0], [300.0, 50.0]]);
let start = spline.evaluate([0.0, 0.0], 0.0);
let end = spline.evaluate([0.0, 0.0], 1.0);
assert!((start[0]).abs() < 1e-4);
assert!((end[0] - 300.0).abs() < 1e-4);
let p1 = spline.evaluate([0.0, 0.0], 1.0 / 3.0);
assert!(
(p1[0] - 100.0).abs() < 1e-3,
"Expected x~100 at t=1/3, got {}",
p1[0]
);
}
#[test]
fn catmull_rom_tension_zero_is_straight() {
let spline =
CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 100.0], [200.0, 0.0]]).tension(0.0);
let quarter = spline.evaluate([0.0, 0.0], 0.25);
assert!(
(quarter[0] - 50.0).abs() < 1e-3,
"Expected x~50, got {}",
quarter[0]
);
assert!(
(quarter[1] - 50.0).abs() < 1e-3,
"Expected y~50, got {}",
quarter[1]
);
}
#[test]
fn catmull_rom_high_tension_overshoots() {
let normal =
CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 100.0], [200.0, 0.0]]).tension(0.5);
let high =
CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 100.0], [200.0, 0.0]]).tension(1.5);
let normal_pt = normal.evaluate([0.0, 0.0], 0.25);
let high_pt = high.evaluate([0.0, 0.0], 0.25);
let diff = (normal_pt[1] - high_pt[1]).abs();
assert!(diff > 0.1, "High tension should differ, got diff={diff}");
}
#[test]
fn catmull_rom_single_point() {
let spline = CatmullRomSpline::new(vec![[42.0, 17.0]]);
let val = spline.evaluate([0.0, 0.0], 0.5);
assert!((val[0] - 42.0).abs() < 1e-6);
assert!((val[1] - 17.0).abs() < 1e-6);
}
#[test]
fn catmull_rom_empty() {
let spline = CatmullRomSpline::new(vec![]);
let val = spline.evaluate([99.0, 99.0], 0.5);
assert!((val[0] - 99.0).abs() < 1e-6); }
#[test]
fn tangent_at_midpoint() {
let spline = CatmullRomSpline::new(vec![[0.0, 0.0], [100.0, 0.0], [200.0, 0.0]]);
let tan = spline.tangent([0.0, 0.0], 0.5);
assert!(tan[0] > 0.0, "Expected positive x tangent, got {}", tan[0]);
assert!(
(tan[1]).abs() < 1.0,
"Expected y tangent ~0, got {}",
tan[1]
);
}
#[test]
fn tangent_angle_horizontal() {
let angle = tangent_angle([1.0, 0.0]);
assert!((angle).abs() < 1e-6, "Expected 0 radians, got {angle}");
}
#[test]
fn tangent_angle_vertical() {
let angle = tangent_angle_deg([0.0, 1.0]);
assert!(
(angle - 90.0).abs() < 1e-4,
"Expected 90 degrees, got {angle}"
);
}
}