Skip to main content

prime_splines/
lib.rs

1//! `prime-splines` — Curve interpolation: Bezier, Hermite, Catmull-Rom, B-spline, slerp.
2//!
3//! All public functions are pure (LOAD + COMPUTE only). No `&mut`, no side effects,
4//! no hidden state. Same inputs always produce the same output.
5//!
6//! # Temporal Assembly Model
7//! - **LOAD** — read function parameters
8//! - **COMPUTE** — pure math
9//!
10//! STORE and JUMP do not exist here.
11//!
12//! # Included
13//! - `bezier_quadratic` / `bezier_quadratic_3d` — quadratic Bezier (3 control points)
14//! - `bezier_cubic` / `bezier_cubic_3d` — cubic Bezier (4 control points)
15//! - `hermite` / `hermite_3d` — cubic Hermite (position + tangent at endpoints)
16//! - `catmull_rom` / `catmull_rom_3d` — Catmull-Rom (smooth through control points)
17//! - `b_spline_cubic` / `b_spline_cubic_3d` — uniform cubic B-spline segment
18//! - `slerp` — spherical linear interpolation for unit quaternions
19//! - `bezier_cubic_arc_length` / `bezier_cubic_arc_length_3d` — approximate arc length
20//! - `bezier_cubic_t_at_length` / `bezier_cubic_t_at_length_3d` — inverse arc-length parameterisation
21
22// ── Quadratic Bezier ──────────────────────────────────────────────────────────
23
24/// Quadratic Bezier interpolation (3 control points).
25///
26/// # Math
27///
28/// ```text
29/// u = 1 - t
30/// B(t) = u²·p0 + 2u·t·p1 + t²·p2
31/// ```
32///
33/// # Arguments
34/// * `t`  — parameter in [0, 1] (0 = p0, 1 = p2)
35/// * `p0` — start point
36/// * `p1` — control point
37/// * `p2` — end point
38///
39/// # Returns
40/// Interpolated value.
41///
42/// # Edge cases
43/// * `t = 0` → `p0`
44/// * `t = 1` → `p2`
45///
46/// # Example
47/// ```rust
48/// use prime_splines::bezier_quadratic;
49/// let mid = bezier_quadratic(0.5, 0.0, 1.0, 0.0);
50/// assert!((mid - 0.5).abs() < 1e-5);
51/// ```
52pub fn bezier_quadratic(t: f32, p0: f32, p1: f32, p2: f32) -> f32 {
53    let u = 1.0 - t;
54    u * u * p0 + 2.0 * u * t * p1 + t * t * p2
55}
56
57/// Quadratic Bezier interpolation on `(x, y, z)` tuples.
58///
59/// Applies [`bezier_quadratic`] independently to each component.
60///
61/// # Example
62/// ```rust
63/// use prime_splines::bezier_quadratic_3d;
64/// let (x, _, _) = bezier_quadratic_3d(0.5, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0), (2.0, 0.0, 0.0));
65/// assert!((x - 1.0).abs() < 1e-5);
66/// ```
67pub fn bezier_quadratic_3d(
68    t: f32,
69    p0: (f32, f32, f32),
70    p1: (f32, f32, f32),
71    p2: (f32, f32, f32),
72) -> (f32, f32, f32) {
73    (
74        bezier_quadratic(t, p0.0, p1.0, p2.0),
75        bezier_quadratic(t, p0.1, p1.1, p2.1),
76        bezier_quadratic(t, p0.2, p1.2, p2.2),
77    )
78}
79
80// ── Cubic Bezier ──────────────────────────────────────────────────────────────
81
82/// Cubic Bezier interpolation (4 control points).
83///
84/// # Math
85///
86/// ```text
87/// u = 1 - t
88/// B(t) = u³·p0 + 3u²t·p1 + 3ut²·p2 + t³·p3
89/// ```
90///
91/// # Arguments
92/// * `t`  — parameter in [0, 1]
93/// * `p0` — start point
94/// * `p1` — first control point
95/// * `p2` — second control point
96/// * `p3` — end point
97///
98/// # Returns
99/// Interpolated value.
100///
101/// # Edge cases
102/// * `t = 0` → `p0`
103/// * `t = 1` → `p3`
104///
105/// # Example
106/// ```rust
107/// use prime_splines::bezier_cubic;
108/// assert!((bezier_cubic(0.0, 1.0, 2.0, 3.0, 4.0) - 1.0).abs() < 1e-5);
109/// assert!((bezier_cubic(1.0, 1.0, 2.0, 3.0, 4.0) - 4.0).abs() < 1e-5);
110/// ```
111pub fn bezier_cubic(t: f32, p0: f32, p1: f32, p2: f32, p3: f32) -> f32 {
112    let u = 1.0 - t;
113    u * u * u * p0
114        + 3.0 * u * u * t * p1
115        + 3.0 * u * t * t * p2
116        + t * t * t * p3
117}
118
119/// Cubic Bezier interpolation on `(x, y, z)` tuples.
120///
121/// Applies [`bezier_cubic`] independently to each component.
122///
123/// # Example
124/// ```rust
125/// use prime_splines::bezier_cubic_3d;
126/// let (x, _, _) = bezier_cubic_3d(0.0, (1.0, 0.0, 0.0), (2.0, 1.0, 0.0), (3.0, 1.0, 0.0), (4.0, 0.0, 0.0));
127/// assert!((x - 1.0).abs() < 1e-5);
128/// ```
129pub fn bezier_cubic_3d(
130    t: f32,
131    p0: (f32, f32, f32),
132    p1: (f32, f32, f32),
133    p2: (f32, f32, f32),
134    p3: (f32, f32, f32),
135) -> (f32, f32, f32) {
136    (
137        bezier_cubic(t, p0.0, p1.0, p2.0, p3.0),
138        bezier_cubic(t, p0.1, p1.1, p2.1, p3.1),
139        bezier_cubic(t, p0.2, p1.2, p2.2, p3.2),
140    )
141}
142
143// ── Hermite cubic ─────────────────────────────────────────────────────────────
144
145/// Cubic Hermite interpolation: endpoints and tangents.
146///
147/// # Math
148///
149/// ```text
150/// h00(t) =  2t³ - 3t² + 1
151/// h10(t) =   t³ - 2t² + t
152/// h01(t) = -2t³ + 3t²
153/// h11(t) =   t³ -  t²
154///
155/// h(t) = h00·p0 + h10·m0 + h01·p1 + h11·m1
156/// ```
157///
158/// # Arguments
159/// * `t`  — parameter in [0, 1]
160/// * `p0` — value at t=0
161/// * `m0` — tangent (derivative) at t=0
162/// * `p1` — value at t=1
163/// * `m1` — tangent (derivative) at t=1
164///
165/// # Returns
166/// Interpolated value.
167///
168/// # Edge cases
169/// * `t = 0` → `p0`
170/// * `t = 1` → `p1`
171///
172/// # Example
173/// ```rust
174/// use prime_splines::hermite;
175/// // Straight line — tangents chosen so it passes linearly
176/// let v = hermite(0.5, 0.0, 1.0, 1.0, 1.0);
177/// assert!((v - 0.5).abs() < 1e-5);
178/// ```
179pub fn hermite(t: f32, p0: f32, m0: f32, p1: f32, m1: f32) -> f32 {
180    let t2 = t * t;
181    let t3 = t2 * t;
182    let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
183    let h10 = t3 - 2.0 * t2 + t;
184    let h01 = -2.0 * t3 + 3.0 * t2;
185    let h11 = t3 - t2;
186    h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
187}
188
189/// Cubic Hermite interpolation on `(x, y, z)` tuples.
190///
191/// # Example
192/// ```rust
193/// use prime_splines::hermite_3d;
194/// let (x, _, _) = hermite_3d(0.0, (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0));
195/// assert!(x.abs() < 1e-5);
196/// ```
197pub fn hermite_3d(
198    t: f32,
199    p0: (f32, f32, f32),
200    m0: (f32, f32, f32),
201    p1: (f32, f32, f32),
202    m1: (f32, f32, f32),
203) -> (f32, f32, f32) {
204    (
205        hermite(t, p0.0, m0.0, p1.0, m1.0),
206        hermite(t, p0.1, m0.1, p1.1, m1.1),
207        hermite(t, p0.2, m0.2, p1.2, m1.2),
208    )
209}
210
211// ── Catmull-Rom ───────────────────────────────────────────────────────────────
212
213/// Uniform Catmull-Rom spline segment.
214///
215/// Interpolates between `p1` and `p2`, using `p0` and `p3` as phantom
216/// neighbours to compute tangents. The curve passes through `p1` at `t=0`
217/// and `p2` at `t=1`.
218///
219/// # Math
220///
221/// ```text
222/// tangent at p1: m0 = (p2 - p0) / 2
223/// tangent at p2: m1 = (p3 - p1) / 2
224/// result = hermite(t, p1, m0, p2, m1)
225///
226/// Equivalently (matrix form):
227/// result = 0.5 * ( 2p1
228///                + (-p0 + p2) * t
229///                + (2p0 - 5p1 + 4p2 - p3) * t²
230///                + (-p0 + 3p1 - 3p2 + p3) * t³ )
231/// ```
232///
233/// # Arguments
234/// * `t`      — parameter in [0, 1]
235/// * `p0..p3` — four consecutive control points; curve segment is p1→p2
236///
237/// # Returns
238/// Interpolated value on the p1→p2 segment.
239///
240/// # Edge cases
241/// * `t = 0` → `p1`
242/// * `t = 1` → `p2`
243///
244/// # Example
245/// ```rust
246/// use prime_splines::catmull_rom;
247/// let v = catmull_rom(0.5, 0.0, 1.0, 2.0, 3.0);
248/// assert!((v - 1.5).abs() < 1e-5);
249/// ```
250pub fn catmull_rom(t: f32, p0: f32, p1: f32, p2: f32, p3: f32) -> f32 {
251    let t2 = t * t;
252    let t3 = t2 * t;
253    0.5 * (2.0 * p1
254        + (-p0 + p2) * t
255        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
256        + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
257}
258
259/// Catmull-Rom spline segment on `(x, y, z)` tuples.
260///
261/// # Example
262/// ```rust
263/// use prime_splines::catmull_rom_3d;
264/// let p = catmull_rom_3d(0.0, (0.0,0.0,0.0), (1.0,0.0,0.0), (2.0,0.0,0.0), (3.0,0.0,0.0));
265/// assert!((p.0 - 1.0).abs() < 1e-5);
266/// ```
267pub fn catmull_rom_3d(
268    t: f32,
269    p0: (f32, f32, f32),
270    p1: (f32, f32, f32),
271    p2: (f32, f32, f32),
272    p3: (f32, f32, f32),
273) -> (f32, f32, f32) {
274    (
275        catmull_rom(t, p0.0, p1.0, p2.0, p3.0),
276        catmull_rom(t, p0.1, p1.1, p2.1, p3.1),
277        catmull_rom(t, p0.2, p1.2, p2.2, p3.2),
278    )
279}
280
281// ── Uniform cubic B-spline ────────────────────────────────────────────────────
282
283/// Uniform cubic B-spline segment.
284///
285/// Evaluates one segment of a uniform cubic B-spline. The curve does NOT
286/// pass through the control points (unlike Catmull-Rom). It is contained
287/// within the convex hull of the four control points.
288///
289/// # Math
290///
291/// ```text
292/// Basis matrix (1/6 factor applied):
293///
294/// [-1  3 -3  1]   [p0]
295/// [ 3 -6  3  0] * [p1] * (1/6)
296/// [-3  0  3  0]   [p2]
297/// [ 1  4  1  0]   [p3]
298///
299/// B(t) = (1/6) * ((-t³+3t²-3t+1)·p0
300///                +(3t³-6t²+4)·p1
301///                +(-3t³+3t²+3t+1)·p2
302///                + t³·p3)
303/// ```
304///
305/// # Arguments
306/// * `t`      — parameter in [0, 1]
307/// * `p0..p3` — four consecutive control points
308///
309/// # Returns
310/// Interpolated B-spline value.
311///
312/// # Edge cases
313/// * `t = 0` → `(p0 + 4·p1 + p2) / 6` (knot value at start of segment)
314/// * `t = 1` → `(p1 + 4·p2 + p3) / 6` (knot value at end of segment)
315///
316/// # Example
317/// ```rust
318/// use prime_splines::b_spline_cubic;
319/// // Uniform points on a line → B-spline stays on that line
320/// let v = b_spline_cubic(0.5, 0.0, 1.0, 2.0, 3.0);
321/// assert!((v - 1.5).abs() < 1e-5);
322/// ```
323pub fn b_spline_cubic(t: f32, p0: f32, p1: f32, p2: f32, p3: f32) -> f32 {
324    let t2 = t * t;
325    let t3 = t2 * t;
326    ((-t3 + 3.0 * t2 - 3.0 * t + 1.0) * p0
327        + (3.0 * t3 - 6.0 * t2 + 4.0) * p1
328        + (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) * p2
329        + t3 * p3)
330        / 6.0
331}
332
333/// Uniform cubic B-spline segment on `(x, y, z)` tuples.
334///
335/// # Example
336/// ```rust
337/// use prime_splines::b_spline_cubic_3d;
338/// let p = b_spline_cubic_3d(0.5, (0.0,0.0,0.0), (1.0,0.0,0.0), (2.0,0.0,0.0), (3.0,0.0,0.0));
339/// assert!((p.0 - 1.5).abs() < 1e-5);
340/// ```
341pub fn b_spline_cubic_3d(
342    t: f32,
343    p0: (f32, f32, f32),
344    p1: (f32, f32, f32),
345    p2: (f32, f32, f32),
346    p3: (f32, f32, f32),
347) -> (f32, f32, f32) {
348    (
349        b_spline_cubic(t, p0.0, p1.0, p2.0, p3.0),
350        b_spline_cubic(t, p0.1, p1.1, p2.1, p3.1),
351        b_spline_cubic(t, p0.2, p1.2, p2.2, p3.2),
352    )
353}
354
355// ── Slerp ─────────────────────────────────────────────────────────────────────
356
357/// Spherical linear interpolation between two unit quaternions.
358///
359/// Takes the shorter arc (negates `q1` if `dot < 0`). Falls back to linear
360/// interpolation when the quaternions are nearly identical (avoids division
361/// by near-zero `sin(θ)`).
362///
363/// # Math
364///
365/// ```text
366/// dot = q0·q1
367/// if dot < 0: q1 = -q1, dot = -dot   ← shorter arc
368/// if dot > 0.9995: lerp and normalise ← near-identical quaternions
369///
370/// θ = acos(dot)
371/// result = sin((1-t)·θ)/sin(θ) · q0 + sin(t·θ)/sin(θ) · q1
372/// ```
373///
374/// # Arguments
375/// * `t`  — parameter in [0, 1]
376/// * `q0` — start quaternion `(x, y, z, w)`, assumed unit length
377/// * `q1` — end quaternion `(x, y, z, w)`, assumed unit length
378///
379/// # Returns
380/// Unit quaternion interpolated along the shorter arc.
381///
382/// # Edge cases
383/// * `t = 0` → `q0`
384/// * `t = 1` → `q1` (or `-q1` if the shorter arc required negation)
385/// * Nearly identical quaternions → normalised linear interpolation
386///
387/// # Example
388/// ```rust
389/// use prime_splines::slerp;
390/// let q = slerp(0.0, (0.0, 0.0, 0.0, 1.0), (0.0, 0.0, 1.0, 0.0));
391/// let (x, y, z, w) = q;
392/// assert!((x*x + y*y + z*z + w*w - 1.0).abs() < 1e-5);
393/// ```
394pub fn slerp(
395    t: f32,
396    q0: (f32, f32, f32, f32),
397    q1: (f32, f32, f32, f32),
398) -> (f32, f32, f32, f32) {
399    let dot_raw = q0.0 * q1.0 + q0.1 * q1.1 + q0.2 * q1.2 + q0.3 * q1.3;
400
401    // Shorter arc: negate q1 if dot is negative
402    let (q1, dot) = if dot_raw < 0.0 {
403        ((-q1.0, -q1.1, -q1.2, -q1.3), -dot_raw)
404    } else {
405        (q1, dot_raw)
406    };
407
408    // Near-identical quaternions → normalised linear interpolation
409    if dot > 0.9995 {
410        let rx = q0.0 + t * (q1.0 - q0.0);
411        let ry = q0.1 + t * (q1.1 - q0.1);
412        let rz = q0.2 + t * (q1.2 - q0.2);
413        let rw = q0.3 + t * (q1.3 - q0.3);
414        let len = (rx * rx + ry * ry + rz * rz + rw * rw).sqrt();
415        return (rx / len, ry / len, rz / len, rw / len);
416    }
417
418    let theta = dot.acos();
419    let sin_theta = theta.sin();
420    let w0 = ((1.0 - t) * theta).sin() / sin_theta;
421    let w1 = (t * theta).sin() / sin_theta;
422
423    (
424        w0 * q0.0 + w1 * q1.0,
425        w0 * q0.1 + w1 * q1.1,
426        w0 * q0.2 + w1 * q1.2,
427        w0 * q0.3 + w1 * q1.3,
428    )
429}
430
431// ── Arc-length parameterisation ───────────────────────────────────────────────
432
433/// Approximate arc length of a 1-D cubic Bezier by subdividing into `steps` linear segments.
434///
435/// # Math
436///
437/// ```text
438/// L ≈ Σ |B(t_i) - B(t_{i-1})|   for i = 1..steps, t_i = i / steps
439/// ```
440///
441/// # Example
442/// ```rust
443/// # use prime_splines::bezier_cubic_arc_length;
444/// let len = bezier_cubic_arc_length(0.0, 1.0, 2.0, 3.0, 100);
445/// assert!((len - 3.0).abs() < 0.01); // straight line p0=0, p1=1, p2=2, p3=3
446/// ```
447pub fn bezier_cubic_arc_length(p0: f32, p1: f32, p2: f32, p3: f32, steps: usize) -> f32 {
448    (1..=steps)
449        .fold((0.0_f32, p0), |(acc, prev), i| {
450            let t = i as f32 / steps as f32;
451            let curr = bezier_cubic(t, p0, p1, p2, p3);
452            (acc + (curr - prev).abs(), curr)
453        })
454        .0
455}
456
457/// Approximate arc length of a 3-D cubic Bezier by subdividing into `steps` linear segments.
458///
459/// # Math
460///
461/// ```text
462/// L ≈ Σ |B(t_i) - B(t_{i-1})|   (Euclidean distance in 3-D)
463/// ```
464///
465/// # Example
466/// ```rust
467/// # use prime_splines::bezier_cubic_arc_length_3d;
468/// // Straight line from origin to (3, 0, 0)
469/// let len = bezier_cubic_arc_length_3d(
470///     (0.0, 0.0, 0.0), (1.0, 0.0, 0.0),
471///     (2.0, 0.0, 0.0), (3.0, 0.0, 0.0), 100,
472/// );
473/// assert!((len - 3.0).abs() < 0.01);
474/// ```
475pub fn bezier_cubic_arc_length_3d(
476    p0: (f32, f32, f32),
477    p1: (f32, f32, f32),
478    p2: (f32, f32, f32),
479    p3: (f32, f32, f32),
480    steps: usize,
481) -> f32 {
482    (1..=steps)
483        .fold((0.0_f32, p0), |(acc, prev), i| {
484            let t = i as f32 / steps as f32;
485            let curr = bezier_cubic_3d(t, p0, p1, p2, p3);
486            let dx = curr.0 - prev.0;
487            let dy = curr.1 - prev.1;
488            let dz = curr.2 - prev.2;
489            (acc + (dx * dx + dy * dy + dz * dz).sqrt(), curr)
490        })
491        .0
492}
493
494/// Find the parameter `t` corresponding to a target arc length along a 1-D cubic Bezier.
495///
496/// Uses binary search over `t` in `[0, 1]` to find the `t` where the arc length
497/// from `t=0` equals `target_length`. Returns `t` within tolerance after `iterations`
498/// bisection steps.
499///
500/// # Arguments
501/// * `p0..p3`        — control points
502/// * `target_length` — desired arc length from `t=0`
503/// * `steps`         — subdivision count for each arc-length measurement
504/// * `iterations`    — number of binary-search bisection steps
505///
506/// # Example
507/// ```rust
508/// # use prime_splines::bezier_cubic_t_at_length;
509/// // Straight line 0→3: half the length should be at t≈0.5
510/// let t = bezier_cubic_t_at_length(0.0, 1.0, 2.0, 3.0, 1.5, 100, 20);
511/// assert!((t - 0.5).abs() < 0.01);
512/// ```
513pub fn bezier_cubic_t_at_length(
514    p0: f32,
515    p1: f32,
516    p2: f32,
517    p3: f32,
518    target_length: f32,
519    steps: usize,
520    iterations: usize,
521) -> f32 {
522    // Binary search helper: compute arc length from 0 to t_max
523    let arc_length_to = |t_max: f32| -> f32 {
524        if t_max <= 0.0 {
525            return 0.0;
526        }
527        (1..=steps)
528            .fold((0.0_f32, p0), |(acc, prev), i| {
529                let t = t_max * i as f32 / steps as f32;
530                let curr = bezier_cubic(t, p0, p1, p2, p3);
531                (acc + (curr - prev).abs(), curr)
532            })
533            .0
534    };
535
536    let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
537    for _ in 0..iterations {
538        let mid = (lo + hi) * 0.5;
539        if arc_length_to(mid) < target_length {
540            lo = mid;
541        } else {
542            hi = mid;
543        }
544    }
545    (lo + hi) * 0.5
546}
547
548/// Find the parameter `t` corresponding to a target arc length along a 3-D cubic Bezier.
549///
550/// Uses binary search over `t` in `[0, 1]`.
551///
552/// # Example
553/// ```rust
554/// # use prime_splines::bezier_cubic_t_at_length_3d;
555/// let t = bezier_cubic_t_at_length_3d(
556///     (0.0, 0.0, 0.0), (1.0, 0.0, 0.0),
557///     (2.0, 0.0, 0.0), (3.0, 0.0, 0.0),
558///     1.5, 100, 20,
559/// );
560/// assert!((t - 0.5).abs() < 0.01);
561/// ```
562pub fn bezier_cubic_t_at_length_3d(
563    p0: (f32, f32, f32),
564    p1: (f32, f32, f32),
565    p2: (f32, f32, f32),
566    p3: (f32, f32, f32),
567    target_length: f32,
568    steps: usize,
569    iterations: usize,
570) -> f32 {
571    let arc_length_to = |t_max: f32| -> f32 {
572        if t_max <= 0.0 {
573            return 0.0;
574        }
575        (1..=steps)
576            .fold((0.0_f32, p0), |(acc, prev), i| {
577                let t = t_max * i as f32 / steps as f32;
578                let curr = bezier_cubic_3d(t, p0, p1, p2, p3);
579                let dx = curr.0 - prev.0;
580                let dy = curr.1 - prev.1;
581                let dz = curr.2 - prev.2;
582                (acc + (dx * dx + dy * dy + dz * dz).sqrt(), curr)
583            })
584            .0
585    };
586
587    let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
588    for _ in 0..iterations {
589        let mid = (lo + hi) * 0.5;
590        if arc_length_to(mid) < target_length {
591            lo = mid;
592        } else {
593            hi = mid;
594        }
595    }
596    (lo + hi) * 0.5
597}
598
599// ── Tests ─────────────────────────────────────────────────────────────────────
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    const EPSILON: f32 = 1e-4;
606
607    // ── bezier_quadratic ──────────────────────────────────────────────────────
608
609    #[test]
610    fn bezier_quadratic_at_endpoints() {
611        assert!((bezier_quadratic(0.0, 1.0, 2.0, 3.0) - 1.0).abs() < EPSILON);
612        assert!((bezier_quadratic(1.0, 1.0, 2.0, 3.0) - 3.0).abs() < EPSILON);
613    }
614
615    #[test]
616    fn bezier_quadratic_midpoint() {
617        // Symmetric case: p0=0, p1=1, p2=0 → peak at t=0.5
618        let peak = bezier_quadratic(0.5, 0.0, 1.0, 0.0);
619        assert!((peak - 0.5).abs() < EPSILON);
620    }
621
622    #[test]
623    fn bezier_quadratic_deterministic() {
624        let a = bezier_quadratic(0.3, 0.0, 1.0, 2.0);
625        let b = bezier_quadratic(0.3, 0.0, 1.0, 2.0);
626        assert_eq!(a, b);
627    }
628
629    #[test]
630    fn bezier_quadratic_3d_endpoints() {
631        let p0 = (0.0, 0.0, 0.0);
632        let p1 = (1.0, 1.0, 1.0);
633        let p2 = (2.0, 0.0, 0.0);
634        let start = bezier_quadratic_3d(0.0, p0, p1, p2);
635        let end = bezier_quadratic_3d(1.0, p0, p1, p2);
636        assert!((start.0 - 0.0).abs() < EPSILON);
637        assert!((end.0 - 2.0).abs() < EPSILON);
638    }
639
640    // ── bezier_cubic ──────────────────────────────────────────────────────────
641
642    #[test]
643    fn bezier_cubic_at_endpoints() {
644        assert!((bezier_cubic(0.0, 1.0, 2.0, 3.0, 4.0) - 1.0).abs() < EPSILON);
645        assert!((bezier_cubic(1.0, 1.0, 2.0, 3.0, 4.0) - 4.0).abs() < EPSILON);
646    }
647
648    #[test]
649    fn bezier_cubic_collinear_is_linear() {
650        // When all four points are collinear (0, 1, 2, 3), the curve is linear
651        let v = bezier_cubic(0.5, 0.0, 1.0, 2.0, 3.0);
652        assert!((v - 1.5).abs() < EPSILON, "v={v}");
653    }
654
655    #[test]
656    fn bezier_cubic_deterministic() {
657        let a = bezier_cubic(0.4, 0.0, 1.0, 1.0, 0.0);
658        let b = bezier_cubic(0.4, 0.0, 1.0, 1.0, 0.0);
659        assert_eq!(a, b);
660    }
661
662    #[test]
663    fn bezier_cubic_3d_endpoints() {
664        let (p0, p1, p2, p3) = ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0), (3.0, 0.0, 0.0));
665        assert!((bezier_cubic_3d(0.0, p0, p1, p2, p3).0).abs() < EPSILON);
666        assert!((bezier_cubic_3d(1.0, p0, p1, p2, p3).0 - 3.0).abs() < EPSILON);
667    }
668
669    // ── hermite ───────────────────────────────────────────────────────────────
670
671    #[test]
672    fn hermite_at_endpoints() {
673        assert!((hermite(0.0, 1.0, 2.0, 5.0, 2.0) - 1.0).abs() < EPSILON);
674        assert!((hermite(1.0, 1.0, 2.0, 5.0, 2.0) - 5.0).abs() < EPSILON);
675    }
676
677    #[test]
678    fn hermite_linear_case() {
679        // p0=0, m0=1, p1=1, m1=1 → straight line
680        let v = hermite(0.5, 0.0, 1.0, 1.0, 1.0);
681        assert!((v - 0.5).abs() < EPSILON, "v={v}");
682    }
683
684    #[test]
685    fn hermite_deterministic() {
686        let a = hermite(0.3, 0.0, 1.0, 1.0, 0.0);
687        let b = hermite(0.3, 0.0, 1.0, 1.0, 0.0);
688        assert_eq!(a, b);
689    }
690
691    #[test]
692    fn hermite_3d_endpoints() {
693        let p0 = (0.0_f32, 0.0, 0.0);
694        let m0 = (1.0, 0.0, 0.0);
695        let p1 = (1.0, 1.0, 0.0);
696        let m1 = (1.0, 0.0, 0.0);
697        let start = hermite_3d(0.0, p0, m0, p1, m1);
698        assert!((start.0 - p0.0).abs() < EPSILON);
699        assert!((start.1 - p0.1).abs() < EPSILON);
700    }
701
702    // ── catmull_rom ───────────────────────────────────────────────────────────
703
704    #[test]
705    fn catmull_rom_passes_through_inner_points() {
706        // t=0 → p1, t=1 → p2
707        assert!((catmull_rom(0.0, 0.0, 1.0, 2.0, 3.0) - 1.0).abs() < EPSILON);
708        assert!((catmull_rom(1.0, 0.0, 1.0, 2.0, 3.0) - 2.0).abs() < EPSILON);
709    }
710
711    #[test]
712    fn catmull_rom_midpoint_collinear() {
713        // Uniform spacing: p0=0, p1=1, p2=2, p3=3 → linear
714        let v = catmull_rom(0.5, 0.0, 1.0, 2.0, 3.0);
715        assert!((v - 1.5).abs() < EPSILON, "v={v}");
716    }
717
718    #[test]
719    fn catmull_rom_deterministic() {
720        let a = catmull_rom(0.4, 0.0, 1.0, 0.5, 0.0);
721        let b = catmull_rom(0.4, 0.0, 1.0, 0.5, 0.0);
722        assert_eq!(a, b);
723    }
724
725    #[test]
726    fn catmull_rom_3d_passes_through_p1() {
727        let p = catmull_rom_3d(0.0, (0.0, 0.0, 0.0), (1.0, 2.0, 3.0), (3.0, 1.0, 0.0), (5.0, 0.0, 0.0));
728        assert!((p.0 - 1.0).abs() < EPSILON);
729        assert!((p.1 - 2.0).abs() < EPSILON);
730    }
731
732    // ── b_spline_cubic ────────────────────────────────────────────────────────
733
734    #[test]
735    fn b_spline_cubic_collinear_midpoint() {
736        // Uniform collinear points → midpoint on the line
737        let v = b_spline_cubic(0.5, 0.0, 1.0, 2.0, 3.0);
738        assert!((v - 1.5).abs() < EPSILON, "v={v}");
739    }
740
741    #[test]
742    fn b_spline_cubic_start_knot() {
743        // At t=0: (p0 + 4*p1 + p2) / 6
744        let expected = (0.0 + 4.0 * 1.0 + 2.0) / 6.0;
745        let v = b_spline_cubic(0.0, 0.0, 1.0, 2.0, 3.0);
746        assert!((v - expected).abs() < EPSILON, "v={v}, expected={expected}");
747    }
748
749    #[test]
750    fn b_spline_cubic_end_knot() {
751        // At t=1: (p1 + 4*p2 + p3) / 6
752        let expected = (1.0 + 4.0 * 2.0 + 3.0) / 6.0;
753        let v = b_spline_cubic(1.0, 0.0, 1.0, 2.0, 3.0);
754        assert!((v - expected).abs() < EPSILON, "v={v}, expected={expected}");
755    }
756
757    #[test]
758    fn b_spline_cubic_deterministic() {
759        let a = b_spline_cubic(0.3, 0.0, 1.0, 2.0, 3.0);
760        let b = b_spline_cubic(0.3, 0.0, 1.0, 2.0, 3.0);
761        assert_eq!(a, b);
762    }
763
764    #[test]
765    fn b_spline_cubic_3d_midpoint() {
766        let p = b_spline_cubic_3d(0.5, (0.0,0.0,0.0), (1.0,0.0,0.0), (2.0,0.0,0.0), (3.0,0.0,0.0));
767        assert!((p.0 - 1.5).abs() < EPSILON);
768    }
769
770    // ── slerp ─────────────────────────────────────────────────────────────────
771
772    #[test]
773    fn slerp_t0_returns_q0() {
774        let q0 = (0.0, 0.0, 0.0, 1.0_f32);
775        let q1 = (0.0, 0.0, 1.0, 0.0_f32);
776        let r = slerp(0.0, q0, q1);
777        assert!((r.0 - q0.0).abs() < EPSILON);
778        assert!((r.3 - q0.3).abs() < EPSILON);
779    }
780
781    #[test]
782    fn slerp_t1_returns_q1() {
783        let q0 = (0.0, 0.0, 0.0, 1.0_f32);
784        let q1 = (0.0, 0.0, 1.0, 0.0_f32);
785        let r = slerp(1.0, q0, q1);
786        assert!((r.2 - q1.2).abs() < EPSILON);
787        assert!((r.3 - q1.3).abs() < EPSILON);
788    }
789
790    #[test]
791    fn slerp_preserves_unit_length() {
792        let q0 = (0.0, 0.0, 0.0, 1.0_f32);
793        let q1 = (0.0, 1.0, 0.0, 0.0_f32);
794        for i in 0..=10 {
795            let t = i as f32 / 10.0;
796            let r = slerp(t, q0, q1);
797            let len_sq = r.0 * r.0 + r.1 * r.1 + r.2 * r.2 + r.3 * r.3;
798            assert!((len_sq - 1.0).abs() < 1e-3, "t={t} len_sq={len_sq}");
799        }
800    }
801
802    #[test]
803    fn slerp_deterministic() {
804        let q0 = (0.0_f32, 0.0, 0.0, 1.0);
805        let q1 = (0.0_f32, 0.0, 1.0, 0.0);
806        let a = slerp(0.5, q0, q1);
807        let b = slerp(0.5, q0, q1);
808        assert_eq!(a, b);
809    }
810
811    #[test]
812    fn slerp_halfway_is_equidistant() {
813        // Identity → 90° rotation: midpoint should be a 45° rotation
814        let q0 = (0.0_f32, 0.0, 0.0, 1.0); // identity
815        let q1 = (0.0_f32, 0.0, 1.0, 0.0); // 180° around z
816        let mid = slerp(0.5, q0, q1);
817        // Should be 90° around z: (0, 0, sin(45°), cos(45°))
818        let expected_w = std::f32::consts::FRAC_1_SQRT_2;
819        assert!((mid.3.abs() - expected_w).abs() < 1e-3, "w={}", mid.3);
820    }
821
822    // ── degenerate control points (all equal) ─────────────────────────────────
823
824    #[test]
825    fn bezier_quadratic_degenerate_returns_constant() {
826        assert!((bezier_quadratic(0.5, 3.0, 3.0, 3.0) - 3.0).abs() < EPSILON);
827    }
828
829    #[test]
830    fn bezier_cubic_degenerate_returns_constant() {
831        assert!((bezier_cubic(0.5, 2.0, 2.0, 2.0, 2.0) - 2.0).abs() < EPSILON);
832    }
833
834    #[test]
835    fn catmull_rom_degenerate_returns_constant() {
836        assert!((catmull_rom(0.5, 5.0, 5.0, 5.0, 5.0) - 5.0).abs() < EPSILON);
837    }
838
839    #[test]
840    fn hermite_degenerate_zero_tangents_returns_endpoints() {
841        // p0=p1, m0=m1=0 → flat line
842        assert!((hermite(0.0, 1.0, 0.0, 1.0, 0.0) - 1.0).abs() < EPSILON);
843        assert!((hermite(1.0, 1.0, 0.0, 1.0, 0.0) - 1.0).abs() < EPSILON);
844        assert!((hermite(0.5, 1.0, 0.0, 1.0, 0.0) - 1.0).abs() < EPSILON);
845    }
846
847    #[test]
848    fn b_spline_cubic_degenerate_returns_constant() {
849        assert!((b_spline_cubic(0.5, 4.0, 4.0, 4.0, 4.0) - 4.0).abs() < EPSILON);
850    }
851
852    // ── t outside [0, 1] ──────────────────────────────────────────────────────
853
854    #[test]
855    fn bezier_cubic_extrapolates_beyond_t1() {
856        // t > 1 → extrapolation, result should be finite
857        assert!(bezier_cubic(1.5, 0.0, 0.3, 0.7, 1.0).is_finite());
858    }
859
860    #[test]
861    fn catmull_rom_extrapolates_below_t0() {
862        assert!(catmull_rom(-0.5, 0.0, 1.0, 2.0, 3.0).is_finite());
863    }
864
865    // ── bezier_cubic_arc_length ──────────────────────────────────────────────
866
867    #[test]
868    fn arc_length_straight_line() {
869        // Collinear points 0→3: arc length should equal 3.0
870        let len = bezier_cubic_arc_length(0.0, 1.0, 2.0, 3.0, 200);
871        assert!((len - 3.0).abs() < 0.01, "len={len}");
872    }
873
874    #[test]
875    fn arc_length_curve_exceeds_chord() {
876        // Curved bezier: arc length should be greater than chord length |p3 - p0| = 1.0
877        let len = bezier_cubic_arc_length(0.0, 5.0, -5.0, 1.0, 200);
878        assert!(len > 1.0, "curve arc length {len} should exceed chord 1.0");
879    }
880
881    #[test]
882    fn arc_length_degenerate_zero() {
883        // All points the same → arc length = 0
884        let len = bezier_cubic_arc_length(2.0, 2.0, 2.0, 2.0, 100);
885        assert!(len.abs() < EPSILON, "len={len}");
886    }
887
888    #[test]
889    fn arc_length_3d_straight_line() {
890        let len = bezier_cubic_arc_length_3d(
891            (0.0, 0.0, 0.0),
892            (1.0, 0.0, 0.0),
893            (2.0, 0.0, 0.0),
894            (3.0, 0.0, 0.0),
895            200,
896        );
897        assert!((len - 3.0).abs() < 0.01, "len={len}");
898    }
899
900    #[test]
901    fn arc_length_3d_curve_exceeds_chord() {
902        let p0 = (0.0, 0.0, 0.0);
903        let p1 = (0.0, 5.0, 0.0);
904        let p2 = (0.0, -5.0, 0.0);
905        let p3 = (1.0, 0.0, 0.0);
906        let len = bezier_cubic_arc_length_3d(p0, p1, p2, p3, 200);
907        let chord = ((p3.0 - p0.0).powi(2) + (p3.1 - p0.1).powi(2) + (p3.2 - p0.2).powi(2)).sqrt();
908        assert!(len > chord, "arc {len} should exceed chord {chord}");
909    }
910
911    #[test]
912    fn arc_length_3d_diagonal() {
913        // Straight line from (0,0,0) to (1,1,1): length = sqrt(3)
914        let len = bezier_cubic_arc_length_3d(
915            (0.0, 0.0, 0.0),
916            (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0),
917            (2.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0),
918            (1.0, 1.0, 1.0),
919            200,
920        );
921        let expected = 3.0_f32.sqrt();
922        assert!((len - expected).abs() < 0.01, "len={len}, expected={expected}");
923    }
924
925    // ── bezier_cubic_t_at_length ─────────────────────────────────────────────
926
927    #[test]
928    fn t_at_length_midpoint_straight_line() {
929        // Straight line 0→3: half-length (1.5) at t≈0.5
930        let t = bezier_cubic_t_at_length(0.0, 1.0, 2.0, 3.0, 1.5, 100, 30);
931        assert!((t - 0.5).abs() < 0.01, "t={t}");
932    }
933
934    #[test]
935    fn t_at_length_zero_returns_zero() {
936        let t = bezier_cubic_t_at_length(0.0, 1.0, 2.0, 3.0, 0.0, 100, 30);
937        assert!(t < 0.01, "t={t}");
938    }
939
940    #[test]
941    fn t_at_length_full_returns_one() {
942        let total = bezier_cubic_arc_length(0.0, 1.0, 2.0, 3.0, 200);
943        let t = bezier_cubic_t_at_length(0.0, 1.0, 2.0, 3.0, total, 100, 30);
944        assert!((t - 1.0).abs() < 0.01, "t={t}");
945    }
946
947    #[test]
948    fn t_at_length_3d_midpoint() {
949        let t = bezier_cubic_t_at_length_3d(
950            (0.0, 0.0, 0.0),
951            (1.0, 0.0, 0.0),
952            (2.0, 0.0, 0.0),
953            (3.0, 0.0, 0.0),
954            1.5,
955            100,
956            30,
957        );
958        assert!((t - 0.5).abs() < 0.01, "t={t}");
959    }
960
961    #[test]
962    fn t_at_length_3d_deterministic() {
963        let a = bezier_cubic_t_at_length_3d(
964            (0.0, 0.0, 0.0),
965            (0.0, 5.0, 0.0),
966            (5.0, 0.0, 0.0),
967            (5.0, 5.0, 0.0),
968            3.0,
969            100,
970            30,
971        );
972        let b = bezier_cubic_t_at_length_3d(
973            (0.0, 0.0, 0.0),
974            (0.0, 5.0, 0.0),
975            (5.0, 0.0, 0.0),
976            (5.0, 5.0, 0.0),
977            3.0,
978            100,
979            30,
980        );
981        assert_eq!(a, b);
982    }
983}