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}