Skip to main content

proof_engine/math/
curves.rs

1//! Parametric curves — Bezier, B-Spline, Catmull-Rom, and Hermite splines.
2//!
3//! All curves implement the `Curve` trait: `sample(t)` returns a point on
4//! the curve for t ∈ [0, 1].  Arc-length reparametrization is provided
5//! for uniform speed traversal.
6
7use glam::{Vec2, Vec3};
8
9// ── Curve trait ───────────────────────────────────────────────────────────────
10
11/// Common interface for parametric curves.
12pub trait Curve: Send + Sync {
13    /// Sample position at parameter t ∈ [0, 1].
14    fn sample(&self, t: f32) -> Vec3;
15
16    /// Sample tangent (unnormalized derivative) at t.
17    fn tangent(&self, t: f32) -> Vec3 {
18        let eps = 1e-4_f32;
19        let t0  = (t - eps).max(0.0);
20        let t1  = (t + eps).min(1.0);
21        (self.sample(t1) - self.sample(t0)) / (t1 - t0)
22    }
23
24    /// Sample unit tangent at t.
25    fn unit_tangent(&self, t: f32) -> Vec3 {
26        self.tangent(t).normalize_or_zero()
27    }
28
29    /// Sample the normal (perpendicular to tangent) at t, in the XY plane.
30    fn normal_2d(&self, t: f32) -> Vec3 {
31        let tan = self.unit_tangent(t);
32        Vec3::new(-tan.y, tan.x, 0.0)
33    }
34
35    /// Approximate arc length using `n` segments.
36    fn arc_length(&self, n: u32) -> f32 {
37        let n = n.max(2);
38        let mut len  = 0.0_f32;
39        let mut prev = self.sample(0.0);
40        for i in 1..=n {
41            let t    = i as f32 / n as f32;
42            let next = self.sample(t);
43            len += (next - prev).length();
44            prev = next;
45        }
46        len
47    }
48
49    /// Build an arc-length table with `n` segments for uniform-speed queries.
50    fn build_arc_table(&self, n: u32) -> ArcTable {
51        let n = n.max(2) as usize;
52        let mut t_values    = Vec::with_capacity(n + 1);
53        let mut arc_lengths = Vec::with_capacity(n + 1);
54        let mut len         = 0.0_f32;
55        let mut prev        = self.sample(0.0);
56
57        t_values.push(0.0_f32);
58        arc_lengths.push(0.0_f32);
59
60        for i in 1..=n {
61            let t    = i as f32 / n as f32;
62            let next = self.sample(t);
63            len += (next - prev).length();
64            t_values.push(t);
65            arc_lengths.push(len);
66            prev = next;
67        }
68
69        // Normalize
70        let total = len;
71        for v in &mut arc_lengths { *v /= total.max(f32::EPSILON); }
72
73        ArcTable { t_values, arc_lengths, total_length: total }
74    }
75
76    /// Sample n evenly-spaced points along the curve (by parameter).
77    fn sample_uniform(&self, n: usize) -> Vec<Vec3> {
78        (0..n).map(|i| self.sample(i as f32 / (n - 1).max(1) as f32)).collect()
79    }
80
81    /// Sample n points at equal arc-length intervals.
82    fn sample_arc_uniform(&self, n: usize, table_resolution: u32) -> Vec<Vec3> {
83        let table = self.build_arc_table(table_resolution);
84        (0..n).map(|i| {
85            let s = i as f32 / (n - 1).max(1) as f32;
86            let t = table.arc_to_t(s);
87            self.sample(t)
88        }).collect()
89    }
90
91    /// Bounding box as (min, max).
92    fn bounding_box(&self, resolution: u32) -> (Vec3, Vec3) {
93        let pts = self.sample_uniform(resolution as usize);
94        let min = pts.iter().copied().fold(Vec3::splat(f32::MAX), |a, p| a.min(p));
95        let max = pts.iter().copied().fold(Vec3::splat(f32::MIN), |a, p| a.max(p));
96        (min, max)
97    }
98
99    /// Find the closest point on the curve to `query`, returning (t, point).
100    fn closest_point(&self, query: Vec3, resolution: u32) -> (f32, Vec3) {
101        let n = resolution.max(2) as usize;
102        let mut best_t   = 0.0_f32;
103        let mut best_d2  = f32::MAX;
104        let mut best_pt  = self.sample(0.0);
105
106        for i in 0..=n {
107            let t  = i as f32 / n as f32;
108            let pt = self.sample(t);
109            let d2 = (pt - query).length_squared();
110            if d2 < best_d2 {
111                best_d2 = d2;
112                best_t  = t;
113                best_pt = pt;
114            }
115        }
116        (best_t, best_pt)
117    }
118}
119
120// ── Arc-length table ──────────────────────────────────────────────────────────
121
122/// Precomputed arc-length lookup table for uniform speed traversal.
123pub struct ArcTable {
124    t_values:     Vec<f32>,
125    arc_lengths:  Vec<f32>,  // normalized [0, 1]
126    pub total_length: f32,
127}
128
129impl ArcTable {
130    /// Given a normalized arc-length fraction s ∈ [0, 1], return curve parameter t.
131    pub fn arc_to_t(&self, s: f32) -> f32 {
132        let s = s.clamp(0.0, 1.0);
133        // Binary search in arc_lengths
134        let pos = self.arc_lengths.partition_point(|&v| v < s);
135        if pos == 0 { return self.t_values[0]; }
136        if pos >= self.t_values.len() { return *self.t_values.last().unwrap(); }
137        let lo = pos - 1;
138        let hi = pos;
139        let al = self.arc_lengths[lo];
140        let ah = self.arc_lengths[hi];
141        let span = ah - al;
142        if span < f32::EPSILON { return self.t_values[lo]; }
143        let frac = (s - al) / span;
144        self.t_values[lo] + frac * (self.t_values[hi] - self.t_values[lo])
145    }
146}
147
148// ── Linear segment ────────────────────────────────────────────────────────────
149
150/// Simple line segment from A to B.
151pub struct LineSegment {
152    pub a: Vec3,
153    pub b: Vec3,
154}
155
156impl LineSegment {
157    pub fn new(a: Vec3, b: Vec3) -> Self { Self { a, b } }
158}
159
160impl Curve for LineSegment {
161    fn sample(&self, t: f32) -> Vec3 {
162        self.a.lerp(self.b, t)
163    }
164    fn tangent(&self, _t: f32) -> Vec3 {
165        self.b - self.a
166    }
167}
168
169// ── Quadratic Bezier ──────────────────────────────────────────────────────────
170
171/// Quadratic Bezier curve through 3 control points.
172pub struct QuadraticBezier {
173    pub p0: Vec3,
174    pub p1: Vec3,  // control point
175    pub p2: Vec3,
176}
177
178impl QuadraticBezier {
179    pub fn new(p0: Vec3, p1: Vec3, p2: Vec3) -> Self { Self { p0, p1, p2 } }
180
181    /// Construct a curve that passes through `through` at t=0.5,
182    /// using De Casteljau to find the implied control point.
183    pub fn through_point(p0: Vec3, through: Vec3, p2: Vec3) -> Self {
184        // p1 = 2*through - 0.5*p0 - 0.5*p2
185        let p1 = through * 2.0 - p0 * 0.5 - p2 * 0.5;
186        Self { p0, p1, p2 }
187    }
188
189    /// Split at t into two quadratic curves.
190    pub fn split(&self, t: f32) -> (Self, Self) {
191        let q0 = self.p0.lerp(self.p1, t);
192        let q1 = self.p1.lerp(self.p2, t);
193        let r0 = q0.lerp(q1, t);
194        (
195            Self::new(self.p0, q0, r0),
196            Self::new(r0, q1, self.p2),
197        )
198    }
199}
200
201impl Curve for QuadraticBezier {
202    fn sample(&self, t: f32) -> Vec3 {
203        let u  = 1.0 - t;
204        self.p0 * (u * u) + self.p1 * (2.0 * u * t) + self.p2 * (t * t)
205    }
206    fn tangent(&self, t: f32) -> Vec3 {
207        let u = 1.0 - t;
208        (self.p1 - self.p0) * (2.0 * u) + (self.p2 - self.p1) * (2.0 * t)
209    }
210}
211
212// ── Cubic Bezier ──────────────────────────────────────────────────────────────
213
214/// Cubic Bezier curve (4 control points).
215pub struct CubicBezier {
216    pub p0: Vec3,
217    pub p1: Vec3,
218    pub p2: Vec3,
219    pub p3: Vec3,
220}
221
222impl CubicBezier {
223    pub fn new(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> Self {
224        Self { p0, p1, p2, p3 }
225    }
226
227    /// Ease-in-out curve (commonly used for animation).
228    pub fn ease_in_out(from: Vec3, to: Vec3) -> Self {
229        let dir = (to - from) * 0.33;
230        Self::new(from, from + Vec3::new(dir.x, 0.0, 0.0), to - Vec3::new(dir.x, 0.0, 0.0), to)
231    }
232
233    /// Split at t into two cubic curves (De Casteljau).
234    pub fn split(&self, t: f32) -> (Self, Self) {
235        let q0 = self.p0.lerp(self.p1, t);
236        let q1 = self.p1.lerp(self.p2, t);
237        let q2 = self.p2.lerp(self.p3, t);
238        let r0 = q0.lerp(q1, t);
239        let r1 = q1.lerp(q2, t);
240        let s0 = r0.lerp(r1, t);
241        (
242            Self::new(self.p0, q0, r0, s0),
243            Self::new(s0, r1, q2, self.p3),
244        )
245    }
246
247    /// Second derivative (curvature vector) at t.
248    pub fn second_derivative(&self, t: f32) -> Vec3 {
249        let d0 = self.p1 - self.p0;
250        let d1 = self.p2 - self.p1;
251        let d2 = self.p3 - self.p2;
252        let e0 = d1 - d0;
253        let e1 = d2 - d1;
254        (e0.lerp(e1, t)) * 6.0
255    }
256
257    /// Curvature scalar at t.
258    pub fn curvature(&self, t: f32) -> f32 {
259        let d1 = self.tangent(t);
260        let d2 = self.second_derivative(t);
261        let cross = d1.cross(d2);
262        cross.length() / d1.length().powi(3)
263    }
264}
265
266impl Curve for CubicBezier {
267    fn sample(&self, t: f32) -> Vec3 {
268        let u  = 1.0 - t;
269        let u2 = u * u;
270        let u3 = u2 * u;
271        let t2 = t * t;
272        let t3 = t2 * t;
273        self.p0 * u3 + self.p1 * (3.0 * u2 * t) + self.p2 * (3.0 * u * t2) + self.p3 * t3
274    }
275    fn tangent(&self, t: f32) -> Vec3 {
276        let u  = 1.0 - t;
277        let u2 = u * u;
278        let t2 = t * t;
279        (self.p1 - self.p0) * (3.0 * u2)
280        + (self.p2 - self.p1) * (6.0 * u * t)
281        + (self.p3 - self.p2) * (3.0 * t2)
282    }
283}
284
285// ── N-th order Bezier ─────────────────────────────────────────────────────────
286
287/// Arbitrary-degree Bezier via De Casteljau's algorithm.
288pub struct BezierN {
289    pub control_points: Vec<Vec3>,
290}
291
292impl BezierN {
293    pub fn new(pts: Vec<Vec3>) -> Self {
294        assert!(pts.len() >= 2, "Need at least 2 control points");
295        Self { control_points: pts }
296    }
297}
298
299impl Curve for BezierN {
300    fn sample(&self, t: f32) -> Vec3 {
301        // De Casteljau
302        let mut pts = self.control_points.clone();
303        while pts.len() > 1 {
304            pts = pts.windows(2).map(|w| w[0].lerp(w[1], t)).collect();
305        }
306        pts[0]
307    }
308}
309
310// ── Catmull-Rom spline ────────────────────────────────────────────────────────
311
312/// Catmull-Rom spline through a sequence of waypoints.
313///
314/// The spline passes through all waypoints (unlike Bezier which only passes
315/// through endpoints). `alpha` controls the parameterization:
316/// - 0.0 = uniform (standard)
317/// - 0.5 = centripetal (avoids self-intersection)
318/// - 1.0 = chordal
319#[derive(Clone, Debug)]
320pub struct CatmullRom {
321    pub points: Vec<Vec3>,
322    pub alpha:  f32,
323    pub closed: bool,
324}
325
326impl CatmullRom {
327    pub fn new(points: Vec<Vec3>) -> Self {
328        Self { points, alpha: 0.5, closed: false }
329    }
330
331    pub fn closed(points: Vec<Vec3>) -> Self {
332        Self { points, alpha: 0.5, closed: true }
333    }
334
335    pub fn with_alpha(mut self, alpha: f32) -> Self { self.alpha = alpha; self }
336
337    fn segment_count(&self) -> usize {
338        if self.closed {
339            self.points.len()
340        } else {
341            self.points.len().saturating_sub(1)
342        }
343    }
344
345    fn get_point(&self, i: i32) -> Vec3 {
346        let n = self.points.len() as i32;
347        if self.closed {
348            self.points[((i % n + n) % n) as usize]
349        } else {
350            self.points[i.clamp(0, n - 1) as usize]
351        }
352    }
353
354    fn sample_segment(&self, seg: usize, t: f32) -> Vec3 {
355        let i  = seg as i32;
356        let p0 = self.get_point(i - 1);
357        let p1 = self.get_point(i);
358        let p2 = self.get_point(i + 1);
359        let p3 = self.get_point(i + 2);
360
361        // Centripetal parameterization
362        let t01 = ((p1 - p0).length() + f32::EPSILON).powf(self.alpha);
363        let t12 = ((p2 - p1).length() + f32::EPSILON).powf(self.alpha);
364        let t23 = ((p3 - p2).length() + f32::EPSILON).powf(self.alpha);
365
366        let t0 = 0.0_f32;
367        let t1 = t0 + t01;
368        let t2 = t1 + t12;
369        let t3 = t2 + t23;
370
371        let tt = t1 + t * t12; // map t ∈ [0,1] to [t1, t2]
372
373        let a1 = p0 * ((t1 - tt) / (t1 - t0)) + p1 * ((tt - t0) / (t1 - t0));
374        let a2 = p1 * ((t2 - tt) / (t2 - t1)) + p2 * ((tt - t1) / (t2 - t1));
375        let a3 = p2 * ((t3 - tt) / (t3 - t2)) + p3 * ((tt - t2) / (t3 - t2));
376
377        let b1 = a1 * ((t2 - tt) / (t2 - t0)) + a2 * ((tt - t0) / (t2 - t0));
378        let b2 = a2 * ((t3 - tt) / (t3 - t1)) + a3 * ((tt - t1) / (t3 - t1));
379
380        b1 * ((t2 - tt) / (t2 - t1)) + b2 * ((tt - t1) / (t2 - t1))
381    }
382}
383
384impl Curve for CatmullRom {
385    fn sample(&self, t: f32) -> Vec3 {
386        let n_segs = self.segment_count();
387        if n_segs == 0 { return self.points.first().copied().unwrap_or(Vec3::ZERO); }
388        let t      = t.clamp(0.0, 1.0);
389        let scaled = t * n_segs as f32;
390        let seg    = (scaled as usize).min(n_segs - 1);
391        let local  = scaled - seg as f32;
392        self.sample_segment(seg, local)
393    }
394}
395
396// ── Hermite spline ────────────────────────────────────────────────────────────
397
398/// Cubic Hermite spline: interpolates positions and velocities at each knot.
399#[derive(Clone, Debug)]
400pub struct HermiteSpline {
401    /// (position, tangent) pairs.
402    pub knots: Vec<(Vec3, Vec3)>,
403    pub times: Vec<f32>,  // parameter values (must be sorted)
404}
405
406impl HermiteSpline {
407    pub fn new() -> Self {
408        Self { knots: Vec::new(), times: Vec::new() }
409    }
410
411    pub fn add_knot(mut self, t: f32, pos: Vec3, tangent: Vec3) -> Self {
412        // Insert sorted by time
413        let idx = self.times.partition_point(|&v| v < t);
414        self.times.insert(idx, t);
415        self.knots.insert(idx, (pos, tangent));
416        self
417    }
418
419    /// Evaluate at local parameter u ∈ [0,1] within a segment.
420    fn eval_segment(p0: Vec3, m0: Vec3, p1: Vec3, m1: Vec3, u: f32) -> Vec3 {
421        let u2 = u * u;
422        let u3 = u2 * u;
423        let h00 =  2.0*u3 - 3.0*u2 + 1.0;
424        let h10 =      u3 - 2.0*u2 + u;
425        let h01 = -2.0*u3 + 3.0*u2;
426        let h11 =      u3 -     u2;
427        p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
428    }
429}
430
431impl Default for HermiteSpline {
432    fn default() -> Self { Self::new() }
433}
434
435impl Curve for HermiteSpline {
436    fn sample(&self, t: f32) -> Vec3 {
437        if self.knots.is_empty() { return Vec3::ZERO; }
438        if self.knots.len() == 1 { return self.knots[0].0; }
439
440        let t0 = *self.times.first().unwrap();
441        let t1 = *self.times.last().unwrap();
442        let t  = t0 + t.clamp(0.0, 1.0) * (t1 - t0);
443
444        // Find segment
445        let idx = self.times.partition_point(|&v| v < t).min(self.times.len() - 1);
446        let idx = idx.max(1);
447        let i0  = idx - 1;
448        let i1  = idx;
449
450        let ta  = self.times[i0];
451        let tb  = self.times[i1];
452        let dt  = tb - ta;
453        let u   = if dt < f32::EPSILON { 0.0 } else { (t - ta) / dt };
454
455        let (p0, m0) = self.knots[i0];
456        let (p1, m1) = self.knots[i1];
457        // Scale tangents by segment duration
458        Self::eval_segment(p0, m0 * dt, p1, m1 * dt, u)
459    }
460}
461
462// ── Uniform B-Spline ──────────────────────────────────────────────────────────
463
464/// Cubic uniform B-Spline (C2 continuity). Does NOT pass through control points.
465pub struct BSpline {
466    pub control_points: Vec<Vec3>,
467    pub closed:         bool,
468}
469
470impl BSpline {
471    pub fn new(control_points: Vec<Vec3>) -> Self {
472        Self { control_points, closed: false }
473    }
474
475    pub fn closed(control_points: Vec<Vec3>) -> Self {
476        Self { control_points, closed: true }
477    }
478
479    fn get_point(&self, i: i32) -> Vec3 {
480        let n = self.control_points.len() as i32;
481        if self.closed {
482            self.control_points[((i % n + n) % n) as usize]
483        } else {
484            self.control_points[i.clamp(0, n - 1) as usize]
485        }
486    }
487
488    fn segment_count(&self) -> usize {
489        if self.closed {
490            self.control_points.len()
491        } else {
492            self.control_points.len().saturating_sub(3)
493        }
494    }
495
496    fn sample_segment(&self, seg: usize, u: f32) -> Vec3 {
497        let i  = seg as i32;
498        let p0 = self.get_point(i);
499        let p1 = self.get_point(i + 1);
500        let p2 = self.get_point(i + 2);
501        let p3 = self.get_point(i + 3);
502
503        // Uniform cubic B-spline basis
504        let u2 = u * u;
505        let u3 = u2 * u;
506        let b0 = (1.0 - 3.0*u + 3.0*u2 -     u3) / 6.0;
507        let b1 = (4.0          - 6.0*u2 + 3.0*u3) / 6.0;
508        let b2 = (1.0 + 3.0*u + 3.0*u2 - 3.0*u3) / 6.0;
509        let b3 =                               u3  / 6.0;
510
511        p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3
512    }
513}
514
515impl Curve for BSpline {
516    fn sample(&self, t: f32) -> Vec3 {
517        let n_segs = self.segment_count();
518        if n_segs == 0 { return self.control_points.first().copied().unwrap_or(Vec3::ZERO); }
519        let t      = t.clamp(0.0, 1.0);
520        let scaled = t * n_segs as f32;
521        let seg    = (scaled as usize).min(n_segs - 1);
522        let local  = scaled - seg as f32;
523        self.sample_segment(seg, local)
524    }
525}
526
527// ── Composite / Polyline ──────────────────────────────────────────────────────
528
529/// Multiple curve segments joined end-to-end.
530pub struct CompositeCurve {
531    segments:    Vec<Box<dyn Curve>>,
532    /// Normalized parameter breakpoints (length == segments.len() + 1).
533    breakpoints: Vec<f32>,
534}
535
536impl CompositeCurve {
537    pub fn new() -> Self {
538        Self { segments: Vec::new(), breakpoints: vec![0.0] }
539    }
540
541    /// Add a segment with an explicit weight (relative length contribution).
542    pub fn add_weighted(mut self, seg: Box<dyn Curve>, weight: f32) -> Self {
543        let last = *self.breakpoints.last().unwrap();
544        self.breakpoints.push(last + weight.max(0.0));
545        self.segments.push(seg);
546        self
547    }
548
549    /// Add a segment with weight derived from arc length.
550    pub fn add(self, seg: Box<dyn Curve>) -> Self {
551        let len = seg.arc_length(64);
552        self.add_weighted(seg, len)
553    }
554
555    fn normalize_breakpoints(&mut self) {
556        let total = *self.breakpoints.last().copied().as_ref().unwrap_or(&1.0);
557        if total > f32::EPSILON {
558            for b in &mut self.breakpoints { *b /= total; }
559        }
560    }
561}
562
563impl Default for CompositeCurve {
564    fn default() -> Self { Self::new() }
565}
566
567impl Curve for CompositeCurve {
568    fn sample(&self, t: f32) -> Vec3 {
569        if self.segments.is_empty() { return Vec3::ZERO; }
570        let t = t.clamp(0.0, 1.0);
571        for i in 0..self.segments.len() {
572            let t0 = self.breakpoints[i];
573            let t1 = self.breakpoints[i + 1];
574            if t <= t1 || i == self.segments.len() - 1 {
575                let span = t1 - t0;
576                let local = if span < f32::EPSILON { 1.0 } else { (t - t0) / span };
577                return self.segments[i].sample(local.clamp(0.0, 1.0));
578            }
579        }
580        self.segments.last().unwrap().sample(1.0)
581    }
582}
583
584// ── 2D Bezier helpers (for UI paths) ─────────────────────────────────────────
585
586/// 2D cubic Bezier for UI/path use.
587pub struct CubicBezier2D {
588    pub p0: Vec2,
589    pub p1: Vec2,
590    pub p2: Vec2,
591    pub p3: Vec2,
592}
593
594impl CubicBezier2D {
595    pub fn new(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) -> Self {
596        Self { p0, p1, p2, p3 }
597    }
598
599    pub fn sample(&self, t: f32) -> Vec2 {
600        let u  = 1.0 - t;
601        let u2 = u * u;
602        let u3 = u2 * u;
603        let t2 = t * t;
604        let t3 = t2 * t;
605        self.p0 * u3 + self.p1 * (3.0 * u2 * t) + self.p2 * (3.0 * u * t2) + self.p3 * t3
606    }
607
608    pub fn tangent(&self, t: f32) -> Vec2 {
609        let u  = 1.0 - t;
610        let u2 = u * u;
611        let t2 = t * t;
612        (self.p1 - self.p0) * (3.0 * u2)
613        + (self.p2 - self.p1) * (6.0 * u * t)
614        + (self.p3 - self.p2) * (3.0 * t2)
615    }
616
617    /// CSS cubic-bezier ease convenience function (control points on [0,1]).
618    /// Standard CSS ease: cubic-bezier(0.25, 0.1, 0.25, 1.0).
619    pub fn css_ease() -> Self {
620        Self::new(
621            Vec2::ZERO,
622            Vec2::new(0.25, 0.1),
623            Vec2::new(0.25, 1.0),
624            Vec2::ONE,
625        )
626    }
627
628    pub fn css_ease_in() -> Self {
629        Self::new(Vec2::ZERO, Vec2::new(0.42, 0.0), Vec2::ONE, Vec2::ONE)
630    }
631
632    pub fn css_ease_out() -> Self {
633        Self::new(Vec2::ZERO, Vec2::ZERO, Vec2::new(0.58, 1.0), Vec2::ONE)
634    }
635
636    pub fn css_ease_in_out() -> Self {
637        Self::new(Vec2::ZERO, Vec2::new(0.42, 0.0), Vec2::new(0.58, 1.0), Vec2::ONE)
638    }
639
640    /// Solve for Y given X using Newton's method (for CSS-style timing functions).
641    pub fn solve_for_x(&self, x: f32, tol: f32) -> f32 {
642        let mut t = x;
643        for _ in 0..8 {
644            let pt = self.sample(t);
645            let err = pt.x - x;
646            if err.abs() < tol { return pt.y; }
647            let dt = self.tangent(t).x;
648            if dt.abs() < 1e-8 { break; }
649            t -= err / dt;
650            t  = t.clamp(0.0, 1.0);
651        }
652        self.sample(t).y
653    }
654}
655
656// ── Frenet frame ──────────────────────────────────────────────────────────────
657
658/// Frenet-Serret frame at a point on a curve.
659#[derive(Clone, Copy, Debug)]
660pub struct FrenetFrame {
661    pub position: Vec3,
662    pub tangent:  Vec3,  // T
663    pub normal:   Vec3,  // N (principal normal)
664    pub binormal: Vec3,  // B = T × N
665    pub curvature: f32,
666    pub torsion:   f32,
667}
668
669impl FrenetFrame {
670    /// Compute the Frenet frame at parameter t on a curve.
671    pub fn compute(curve: &dyn Curve, t: f32) -> Self {
672        let eps = 1e-4_f32;
673        let pos = curve.sample(t);
674        let tan = curve.unit_tangent(t);
675
676        // Second derivative for normal
677        let t0 = (t - eps).max(0.0);
678        let t1 = (t + eps).min(1.0);
679        let d0 = curve.unit_tangent(t0);
680        let d1 = curve.unit_tangent(t1);
681        let dT = (d1 - d0) / (t1 - t0);
682
683        let curvature = dT.length();
684        let normal    = if curvature > 1e-8 { dT.normalize() } else { tan.any_orthogonal_vector() };
685        let binormal  = tan.cross(normal).normalize_or_zero();
686
687        // Torsion: dB/dt · N
688        let t00 = (t - 2.0 * eps).max(0.0);
689        let t11 = (t + 2.0 * eps).min(1.0);
690        let bin0 = {
691            let t0 = curve.unit_tangent(t00);
692            let d  = (curve.unit_tangent(t00 + eps) - t0) / eps;
693            let n  = if d.length() > 1e-8 { d.normalize() } else { Vec3::Y };
694            t0.cross(n)
695        };
696        let bin1 = {
697            let t1 = curve.unit_tangent(t11);
698            let d  = (t1 - curve.unit_tangent(t11 - eps)) / eps;
699            let n  = if d.length() > 1e-8 { d.normalize() } else { Vec3::Y };
700            t1.cross(n)
701        };
702        let dB = (bin1 - bin0) / (t11 - t00);
703        let torsion = dB.dot(normal);
704
705        Self { position: pos, tangent: tan, normal, binormal, curvature, torsion }
706    }
707}
708
709// ── Curve Sampler (iterator) ──────────────────────────────────────────────────
710
711/// Iterator that walks a curve at fixed arc-length steps.
712pub struct CurveWalker<'a> {
713    curve:   &'a dyn Curve,
714    table:   ArcTable,
715    current: f32,  // current arc-length fraction
716    step:    f32,  // arc-length step per tick
717}
718
719impl<'a> CurveWalker<'a> {
720    pub fn new(curve: &'a dyn Curve, steps_per_unit_length: f32, resolution: u32) -> Self {
721        let table = curve.build_arc_table(resolution);
722        let step  = steps_per_unit_length / table.total_length.max(f32::EPSILON);
723        Self { curve, table, current: 0.0, step }
724    }
725}
726
727impl<'a> Iterator for CurveWalker<'a> {
728    type Item = (f32, Vec3);  // (arc_fraction, position)
729
730    fn next(&mut self) -> Option<Self::Item> {
731        if self.current > 1.0 { return None; }
732        let s   = self.current;
733        let t   = self.table.arc_to_t(s);
734        let pos = self.curve.sample(t);
735        self.current += self.step;
736        Some((s, pos))
737    }
738}
739
740// ── Tests ─────────────────────────────────────────────────────────────────────
741
742#[cfg(test)]
743mod tests {
744    use super::*;
745
746    fn v3(x: f32, y: f32, z: f32) -> Vec3 { Vec3::new(x, y, z) }
747
748    #[test]
749    fn line_segment_endpoints() {
750        let seg = LineSegment::new(v3(0.0, 0.0, 0.0), v3(1.0, 0.0, 0.0));
751        let p0  = seg.sample(0.0);
752        let p1  = seg.sample(1.0);
753        assert!((p0 - v3(0.0, 0.0, 0.0)).length() < 1e-5);
754        assert!((p1 - v3(1.0, 0.0, 0.0)).length() < 1e-5);
755    }
756
757    #[test]
758    fn quadratic_bezier_midpoint() {
759        // Control points: (0,0), (1,2), (2,0) — midpoint should be (1,1)
760        let q   = QuadraticBezier::new(v3(0.0,0.0,0.0), v3(1.0,2.0,0.0), v3(2.0,0.0,0.0));
761        let mid = q.sample(0.5);
762        assert!((mid - v3(1.0, 1.0, 0.0)).length() < 1e-5);
763    }
764
765    #[test]
766    fn cubic_bezier_endpoints() {
767        let b = CubicBezier::new(
768            v3(0.0,0.0,0.0), v3(1.0,2.0,0.0),
769            v3(2.0,-1.0,0.0), v3(3.0,0.0,0.0),
770        );
771        assert!((b.sample(0.0) - v3(0.0,0.0,0.0)).length() < 1e-5);
772        assert!((b.sample(1.0) - v3(3.0,0.0,0.0)).length() < 1e-5);
773    }
774
775    #[test]
776    fn bezier_n_matches_cubic() {
777        let pts = vec![
778            v3(0.0,0.0,0.0), v3(1.0,2.0,0.0),
779            v3(2.0,-1.0,0.0), v3(3.0,0.0,0.0),
780        ];
781        let cubic   = CubicBezier::new(pts[0], pts[1], pts[2], pts[3]);
782        let generic = BezierN::new(pts);
783        for i in 0..=10 {
784            let t = i as f32 / 10.0;
785            let d = (cubic.sample(t) - generic.sample(t)).length();
786            assert!(d < 1e-4, "Mismatch at t={}: {}", t, d);
787        }
788    }
789
790    #[test]
791    fn catmull_rom_passes_through_waypoints() {
792        let pts = vec![
793            v3(0.0,0.0,0.0), v3(1.0,1.0,0.0),
794            v3(2.0,0.0,0.0), v3(3.0,1.0,0.0),
795        ];
796        let cr = CatmullRom::new(pts.clone());
797        // For uniform CR, internal points are not exactly hit (centripetal)
798        // But start and end should be close to first/last segments
799        let start = cr.sample(0.0);
800        let end   = cr.sample(1.0);
801        // With 4 pts, segments go [0,1], [1,2], [2,3] → t=0 → pt[0], t=1 → pt[3]
802        assert!((start - pts[0]).length() < 0.1);
803        assert!((end   - pts[3]).length() < 0.1);
804    }
805
806    #[test]
807    fn arc_table_monotone() {
808        let b = CubicBezier::new(
809            v3(0.0,0.0,0.0), v3(0.5,1.0,0.0),
810            v3(1.5,-1.0,0.0), v3(2.0,0.0,0.0),
811        );
812        let table = b.build_arc_table(128);
813        // Arc lengths should be monotonically non-decreasing
814        for i in 1..table.arc_lengths.len() {
815            assert!(table.arc_lengths[i] >= table.arc_lengths[i-1]);
816        }
817    }
818
819    #[test]
820    fn arc_to_t_returns_valid_range() {
821        let b = LineSegment::new(v3(0.0,0.0,0.0), v3(1.0,0.0,0.0));
822        let table = b.build_arc_table(64);
823        for i in 0..=10 {
824            let s = i as f32 / 10.0;
825            let t = table.arc_to_t(s);
826            assert!(t >= 0.0 && t <= 1.0, "t={} out of range for s={}", t, s);
827        }
828    }
829
830    #[test]
831    fn bspline_continuity() {
832        let pts = vec![
833            v3(0.0,0.0,0.0), v3(1.0,1.0,0.0), v3(2.0,0.0,0.0),
834            v3(3.0,1.0,0.0), v3(4.0,0.0,0.0),
835        ];
836        let bs = BSpline::new(pts);
837        // Just verify no panics and values are bounded
838        for i in 0..=20 {
839            let t = i as f32 / 20.0;
840            let p = bs.sample(t);
841            assert!(p.x >= -1.0 && p.x <= 5.0);
842        }
843    }
844
845    #[test]
846    fn hermite_spline_at_knot() {
847        let spl = HermiteSpline::new()
848            .add_knot(0.0, v3(0.0, 0.0, 0.0), v3(1.0, 0.0, 0.0))
849            .add_knot(1.0, v3(2.0, 0.0, 0.0), v3(1.0, 0.0, 0.0));
850        let p0 = spl.sample(0.0);
851        let p1 = spl.sample(1.0);
852        assert!((p0 - v3(0.0, 0.0, 0.0)).length() < 1e-5);
853        assert!((p1 - v3(2.0, 0.0, 0.0)).length() < 1e-5);
854    }
855
856    #[test]
857    fn css_bezier_ease_start_end() {
858        let b = CubicBezier2D::css_ease();
859        let s = b.sample(0.0);
860        let e = b.sample(1.0);
861        assert!((s - Vec2::ZERO).length() < 1e-5);
862        assert!((e - Vec2::ONE ).length() < 1e-5);
863    }
864}