#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CatmullRomConfig {
pub alpha: f32,
pub closed: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CatmullRomSpline {
pub control_points: Vec<[f32; 3]>,
pub alpha: f32,
pub closed: bool,
}
#[allow(dead_code)]
pub fn default_catmull_rom_config() -> CatmullRomConfig {
CatmullRomConfig {
alpha: 0.5,
closed: false,
}
}
#[allow(dead_code)]
pub fn new_catmull_rom_spline(
control_points: Vec<[f32; 3]>,
cfg: &CatmullRomConfig,
) -> CatmullRomSpline {
CatmullRomSpline {
control_points,
alpha: cfg.alpha,
closed: cfg.closed,
}
}
#[allow(dead_code)]
pub fn catmull_rom_segment_count(spline: &CatmullRomSpline) -> usize {
let n = spline.control_points.len();
if n < 2 {
return 0;
}
if spline.closed {
n
} else {
n - 1
}
}
#[allow(dead_code)]
pub fn catmull_rom_point_count(spline: &CatmullRomSpline) -> usize {
spline.control_points.len()
}
fn get_point(spline: &CatmullRomSpline, i: isize) -> [f32; 3] {
let n = spline.control_points.len() as isize;
if n == 0 {
return [0.0; 3];
}
let idx = if spline.closed {
i.rem_euclid(n) as usize
} else {
i.clamp(0, n - 1) as usize
};
spline.control_points[idx]
}
fn knot_distance(a: [f32; 3], b: [f32; 3], alpha: f32) -> f32 {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let dz = b[2] - a[2];
let d2 = dx * dx + dy * dy + dz * dz;
if alpha == 0.5 {
d2.sqrt().sqrt() } else if alpha == 0.0 {
1.0
} else {
d2.powf(alpha * 0.5)
}
}
#[allow(dead_code)]
pub fn catmull_rom_evaluate(spline: &CatmullRomSpline, t: f32) -> [f32; 3] {
let segs = catmull_rom_segment_count(spline);
if segs == 0 {
return spline.control_points.first().copied().unwrap_or([0.0; 3]);
}
let t = t.clamp(0.0, segs as f32);
let seg = (t.floor() as usize).min(segs - 1);
let local_t = t - seg as f32;
let i = seg as isize;
let p0 = get_point(spline, i - 1);
let p1 = get_point(spline, i);
let p2 = get_point(spline, i + 1);
let p3 = get_point(spline, i + 2);
catmull_rom_interp(p0, p1, p2, p3, local_t, spline.alpha)
}
fn catmull_rom_interp(
p0: [f32; 3],
p1: [f32; 3],
p2: [f32; 3],
p3: [f32; 3],
t: f32,
alpha: f32,
) -> [f32; 3] {
if alpha == 0.0 {
let t2 = t * t;
let t3 = t2 * t;
let mut out = [0.0f32; 3];
for k in 0..3 {
out[k] = 0.5
* ((2.0 * p1[k])
+ (-p0[k] + p2[k]) * t
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3);
}
return out;
}
let t0 = 0.0f32;
let t1 = t0 + knot_distance(p0, p1, alpha);
let t2 = t1 + knot_distance(p1, p2, alpha);
let t3 = t2 + knot_distance(p2, p3, alpha);
let tg = t1 + t * (t2 - t1);
let lerp3 = |a: [f32; 3], b: [f32; 3], ta: f32, tb: f32, tc: f32| -> [f32; 3] {
if (tb - ta).abs() < 1e-10 {
return a;
}
let frac = (tc - ta) / (tb - ta);
[
a[0] + frac * (b[0] - a[0]),
a[1] + frac * (b[1] - a[1]),
a[2] + frac * (b[2] - a[2]),
]
};
let a1 = lerp3(p0, p1, t0, t1, tg);
let a2 = lerp3(p1, p2, t1, t2, tg);
let a3 = lerp3(p2, p3, t2, t3, tg);
let b1 = lerp3(a1, a2, t0, t2, tg);
let b2 = lerp3(a2, a3, t1, t3, tg);
lerp3(b1, b2, t1, t2, tg)
}
#[allow(dead_code)]
pub fn catmull_rom_sample(spline: &CatmullRomSpline, n_samples: usize) -> Vec<[f32; 3]> {
let segs = catmull_rom_segment_count(spline);
if n_samples == 0 || segs == 0 {
return vec![];
}
let total = segs as f32;
(0..n_samples)
.map(|i| {
let t = if n_samples == 1 {
0.0
} else {
total * (i as f32) / (n_samples - 1) as f32
};
catmull_rom_evaluate(spline, t)
})
.collect()
}
#[allow(dead_code)]
pub fn catmull_rom_tangent(spline: &CatmullRomSpline, t: f32) -> [f32; 3] {
let eps = 1e-4f32;
let segs = catmull_rom_segment_count(spline) as f32;
let t0 = (t - eps).clamp(0.0, segs);
let t1 = (t + eps).clamp(0.0, segs);
let p0 = catmull_rom_evaluate(spline, t0);
let p1 = catmull_rom_evaluate(spline, t1);
let dt = (t1 - t0).max(1e-10);
[
(p1[0] - p0[0]) / dt,
(p1[1] - p0[1]) / dt,
(p1[2] - p0[2]) / dt,
]
}
#[allow(dead_code)]
pub fn catmull_rom_arc_length(spline: &CatmullRomSpline, n_steps: usize) -> f32 {
let segs = catmull_rom_segment_count(spline);
if n_steps == 0 || segs == 0 {
return 0.0;
}
let total = segs as f32;
let mut length = 0.0f32;
let mut prev = catmull_rom_evaluate(spline, 0.0);
for step in 1..=n_steps {
let t = total * step as f32 / n_steps as f32;
let cur = catmull_rom_evaluate(spline, t);
let dx = cur[0] - prev[0];
let dy = cur[1] - prev[1];
let dz = cur[2] - prev[2];
length += (dx * dx + dy * dy + dz * dz).sqrt();
prev = cur;
}
length
}
#[allow(dead_code)]
pub fn catmull_rom_closest_t(
spline: &CatmullRomSpline,
point: [f32; 3],
n_steps: usize,
) -> f32 {
let segs = catmull_rom_segment_count(spline);
if n_steps == 0 || segs == 0 {
return 0.0;
}
let total = segs as f32;
let mut best_t = 0.0f32;
let mut best_dist2 = f32::MAX;
for step in 0..=n_steps {
let t = total * step as f32 / n_steps as f32;
let p = catmull_rom_evaluate(spline, t);
let dx = p[0] - point[0];
let dy = p[1] - point[1];
let dz = p[2] - point[2];
let d2 = dx * dx + dy * dy + dz * dz;
if d2 < best_dist2 {
best_dist2 = d2;
best_t = t;
}
}
best_t
}
#[allow(dead_code)]
pub fn catmull_rom_set_alpha(spline: &mut CatmullRomSpline, alpha: f32) {
spline.alpha = alpha.clamp(0.0, 1.0);
}
#[cfg(test)]
mod tests {
use super::*;
fn make_line_spline() -> CatmullRomSpline {
let cfg = default_catmull_rom_config();
new_catmull_rom_spline(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
&cfg,
)
}
#[test]
fn test_segment_count() {
let s = make_line_spline();
assert_eq!(catmull_rom_segment_count(&s), 3);
}
#[test]
fn test_point_count() {
let s = make_line_spline();
assert_eq!(catmull_rom_point_count(&s), 4);
}
#[test]
fn test_evaluate_endpoints() {
let s = make_line_spline();
let p0 = catmull_rom_evaluate(&s, 0.0);
let p_end = catmull_rom_evaluate(&s, 3.0);
assert!((p0[0] - 0.0).abs() < 1e-4, "start x={}", p0[0]);
assert!((p_end[0] - 3.0).abs() < 1e-4, "end x={}", p_end[0]);
}
#[test]
fn test_sample_count() {
let s = make_line_spline();
let samples = catmull_rom_sample(&s, 10);
assert_eq!(samples.len(), 10);
}
#[test]
fn test_arc_length_line() {
let s = make_line_spline();
let len = catmull_rom_arc_length(&s, 300);
assert!((len - 3.0).abs() < 0.05, "arc length={}", len);
}
#[test]
fn test_set_alpha() {
let cfg = default_catmull_rom_config();
let mut s = new_catmull_rom_spline(
vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]],
&cfg,
);
catmull_rom_set_alpha(&mut s, 1.0);
assert!((s.alpha - 1.0).abs() < 1e-6);
}
#[test]
fn test_closest_t_midpoint() {
let s = make_line_spline();
let t = catmull_rom_closest_t(&s, [1.5, 0.0, 0.0], 300);
assert!((t - 1.5).abs() < 0.15, "closest t={}", t);
}
#[test]
fn test_tangent_direction() {
let s = make_line_spline();
let tan = catmull_rom_tangent(&s, 1.5);
assert!(tan[0] > 0.9, "tangent x={}", tan[0]);
assert!(tan[1].abs() < 0.1, "tangent y={}", tan[1]);
}
#[test]
fn test_empty_spline() {
let cfg = default_catmull_rom_config();
let s = new_catmull_rom_spline(vec![], &cfg);
assert_eq!(catmull_rom_segment_count(&s), 0);
assert_eq!(catmull_rom_sample(&s, 5).len(), 0);
assert_eq!(catmull_rom_arc_length(&s, 10), 0.0);
}
#[test]
fn test_uniform_alpha() {
let cfg = CatmullRomConfig { alpha: 0.0, closed: false };
let s = new_catmull_rom_spline(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
&cfg,
);
let p = catmull_rom_evaluate(&s, 1.5);
assert!((p[0] - 1.5).abs() < 0.05, "mid x={}", p[0]);
}
}