Skip to main content

proof_engine/editor/
spline_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10const EPSILON: f32 = 1e-6;
11const ADAPTIVE_SIMPSON_MAX_DEPTH: u32 = 12;
12const ARC_LENGTH_SAMPLE_COUNT: usize = 512;
13const NEWTON_MAX_ITER: u32 = 64;
14const NEWTON_TOL: f32 = 1e-7;
15const BINARY_SEARCH_ITER: u32 = 48;
16const CURVATURE_COMB_SCALE: f32 = 0.1;
17const DEFAULT_RAIL_GAUGE: f32 = 1.435; // standard gauge in meters
18const PARALLEL_TRANSPORT_STEPS: usize = 256;
19
20// ============================================================
21// UTILITY MATH
22// ============================================================
23
24fn lerp(a: f32, b: f32, t: f32) -> f32 {
25    a + (b - a) * t
26}
27
28fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
29    a + (b - a) * t
30}
31
32fn clamp01(x: f32) -> f32 {
33    x.clamp(0.0, 1.0)
34}
35
36fn smooth_damp(current: f32, target: f32, velocity: &mut f32, smooth_time: f32, dt: f32) -> f32 {
37    let omega = 2.0 / smooth_time.max(EPSILON);
38    let x = omega * dt;
39    let exp = 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x);
40    let change = current - target;
41    let temp = (*velocity + omega * change) * dt;
42    *velocity = (*velocity - omega * temp) * exp;
43    target + (change + temp) * exp
44}
45
46fn smooth_step(t: f32) -> f32 { t * t * (3.0 - 2.0 * t) }
47
48fn hash_f32_noise(n: i32) -> f32 {
49    let n = (n << 13) ^ n;
50    let n = n.wrapping_mul(n.wrapping_mul(n.wrapping_mul(15731) + 789221) + 1376312589);
51    1.0 - (n & 0x7fffffff) as f32 / 1073741824.0
52}
53
54fn value_noise_1d(x: f32) -> f32 {
55    let xi = x.floor() as i32;
56    let xf = x - x.floor();
57    let h0 = hash_f32_noise(xi);
58    let h1 = hash_f32_noise(xi + 1);
59    lerp(h0, h1, smooth_step(xf))
60}
61
62fn quintic_ease(t: f32) -> f32 {
63    let t = clamp01(t);
64    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
65}
66
67fn quintic_ease_derivative(t: f32) -> f32 {
68    let t = clamp01(t);
69    30.0 * t * t * (t - 1.0) * (t - 1.0)
70}
71
72fn cubic_ease_in_out(t: f32) -> f32 {
73    let t = clamp01(t);
74    if t < 0.5 {
75        4.0 * t * t * t
76    } else {
77        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
78    }
79}
80
81fn safe_normalize(v: Vec3) -> Vec3 {
82    let len = v.length();
83    if len < EPSILON { Vec3::Z } else { v / len }
84}
85
86fn cross_safe(a: Vec3, b: Vec3) -> Vec3 {
87    let c = a.cross(b);
88    if c.length_squared() < EPSILON * EPSILON {
89        // find a perpendicular vector
90        let perp = if a.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
91        a.cross(perp).normalize_or_zero()
92    } else {
93        c.normalize()
94    }
95}
96
97/// Adaptive Simpson's rule for 1D integration
98fn adaptive_simpson(f: &dyn Fn(f32) -> f32, a: f32, b: f32, tol: f32, depth: u32) -> f32 {
99    let c = (a + b) * 0.5;
100    let fa = f(a);
101    let fb = f(b);
102    let fc = f(c);
103    let s = (b - a) / 6.0 * (fa + 4.0 * fc + fb);
104    adaptive_simpson_inner(f, a, b, fa, fb, fc, s, tol, depth)
105}
106
107fn adaptive_simpson_inner(
108    f: &dyn Fn(f32) -> f32,
109    a: f32, b: f32,
110    fa: f32, fb: f32, fc: f32,
111    s: f32, tol: f32, depth: u32
112) -> f32 {
113    let c = (a + b) * 0.5;
114    let d = (a + c) * 0.5;
115    let e = (c + b) * 0.5;
116    let fd = f(d);
117    let fe = f(e);
118    let left  = (c - a) / 6.0 * (fa + 4.0 * fd + fc);
119    let right = (b - c) / 6.0 * (fc + 4.0 * fe + fb);
120    let delta = left + right - s;
121    if depth == 0 || delta.abs() <= 15.0 * tol {
122        left + right + delta / 15.0
123    } else {
124        adaptive_simpson_inner(f, a, c, fa, fc, fd, left,  tol * 0.5, depth - 1)
125      + adaptive_simpson_inner(f, c, b, fc, fb, fe, right, tol * 0.5, depth - 1)
126    }
127}
128
129fn integrate_arc_length(deriv: &dyn Fn(f32) -> f32, a: f32, b: f32) -> f32 {
130    let speed = |t: f32| deriv(t);
131    adaptive_simpson(&speed, a, b, 1e-5, ADAPTIVE_SIMPSON_MAX_DEPTH)
132}
133
134/// Build arc-length table: maps parameter t -> arc length
135fn build_arc_length_table(
136    sample_count: usize,
137    position_fn: &dyn Fn(f32) -> Vec3,
138) -> Vec<(f32, f32)> {
139    let mut table = Vec::with_capacity(sample_count + 1);
140    let mut cumulative = 0.0_f32;
141    let mut prev = position_fn(0.0);
142    table.push((0.0_f32, 0.0_f32));
143    for i in 1..=sample_count {
144        let t = i as f32 / sample_count as f32;
145        let cur = position_fn(t);
146        cumulative += (cur - prev).length();
147        table.push((t, cumulative));
148        prev = cur;
149    }
150    table
151}
152
153/// Invert arc-length table: given arc length s, return parameter t
154fn arc_length_to_t(table: &[(f32, f32)], s: f32) -> f32 {
155    if table.is_empty() { return 0.0; }
156    let total = table.last().unwrap().1;
157    let s = s.clamp(0.0, total);
158    let idx = table.partition_point(|entry| entry.1 <= s);
159    if idx == 0 { return table[0].0; }
160    if idx >= table.len() { return table.last().unwrap().0; }
161    let (t0, s0) = table[idx - 1];
162    let (t1, s1) = table[idx];
163    let frac = if (s1 - s0).abs() < EPSILON { 0.0 } else { (s - s0) / (s1 - s0) };
164    lerp(t0, t1, frac)
165}
166
167// ============================================================
168// FRENET-SERRET FRAME
169// ============================================================
170
171#[derive(Clone, Debug)]
172pub struct FrenetFrame {
173    pub position: Vec3,
174    pub tangent:  Vec3,  // T
175    pub normal:   Vec3,  // N
176    pub binormal: Vec3,  // B
177    pub curvature: f32,  // κ
178    pub torsion:   f32,  // τ
179}
180
181impl FrenetFrame {
182    pub fn identity() -> Self {
183        FrenetFrame {
184            position: Vec3::ZERO,
185            tangent:  Vec3::X,
186            normal:   Vec3::Y,
187            binormal: Vec3::Z,
188            curvature: 0.0,
189            torsion:   0.0,
190        }
191    }
192
193    pub fn compute(pos: Vec3, d1: Vec3, d2: Vec3, d3: Vec3) -> Self {
194        // d1 = first derivative, d2 = second, d3 = third
195        let speed = d1.length();
196        let tangent = if speed > EPSILON { d1 / speed } else { Vec3::X };
197        let d1_cross_d2 = d1.cross(d2);
198        let kappa_vec_len = d1_cross_d2.length();
199        let curvature = if speed > EPSILON {
200            kappa_vec_len / speed.powi(3)
201        } else {
202            0.0
203        };
204        let binormal = if kappa_vec_len > EPSILON {
205            d1_cross_d2 / kappa_vec_len
206        } else {
207            Vec3::Z
208        };
209        let normal = binormal.cross(tangent);
210
211        // Torsion: τ = (d1 × d2) · d3 / |d1 × d2|²
212        let torsion = if kappa_vec_len > EPSILON {
213            d1_cross_d2.dot(d3) / kappa_vec_len.powi(2)
214        } else {
215            0.0
216        };
217
218        FrenetFrame { position: pos, tangent, normal, binormal, curvature, torsion }
219    }
220
221    pub fn to_matrix(&self) -> Mat4 {
222        Mat4::from_cols(
223            Vec4::new(self.tangent.x,  self.tangent.y,  self.tangent.z,  0.0),
224            Vec4::new(self.normal.x,   self.normal.y,   self.normal.z,   0.0),
225            Vec4::new(self.binormal.x, self.binormal.y, self.binormal.z, 0.0),
226            Vec4::new(self.position.x, self.position.y, self.position.z, 1.0),
227        )
228    }
229}
230
231// ============================================================
232// PARALLEL TRANSPORT FRAME
233// ============================================================
234
235#[derive(Clone, Debug)]
236pub struct ParallelTransportFrame {
237    pub position: Vec3,
238    pub tangent:  Vec3,
239    pub normal:   Vec3,
240    pub binormal: Vec3,
241}
242
243impl ParallelTransportFrame {
244    /// Double-reflection parallel transport
245    pub fn transport(prev: &ParallelTransportFrame, new_pos: Vec3, new_tangent: Vec3) -> Self {
246        let t_prev = prev.tangent;
247        let t_next = safe_normalize(new_tangent);
248        let v1 = new_pos - prev.position;
249        let c1 = v1.dot(v1);
250        let r_l = if c1 > EPSILON { prev.normal  - (2.0 / c1) * v1.dot(prev.normal)  * v1 } else { prev.normal };
251        let t_l = if c1 > EPSILON { t_prev - (2.0 / c1) * v1.dot(t_prev) * v1 } else { t_prev };
252        let v2 = t_next - t_l;
253        let c2 = v2.dot(v2);
254        let normal = if c2 > EPSILON { r_l - (2.0 / c2) * v2.dot(r_l) * v2 } else { r_l };
255        let normal = safe_normalize(normal);
256        let binormal = safe_normalize(t_next.cross(normal));
257        ParallelTransportFrame { position: new_pos, tangent: t_next, normal, binormal }
258    }
259
260    pub fn initial(position: Vec3, tangent: Vec3) -> Self {
261        let t = safe_normalize(tangent);
262        let perp = if t.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
263        let normal = safe_normalize(t.cross(perp).cross(t));
264        let binormal = safe_normalize(t.cross(normal));
265        ParallelTransportFrame { position, tangent: t, normal, binormal }
266    }
267}
268
269// ============================================================
270// CONTROL POINT
271// ============================================================
272
273#[derive(Clone, Debug)]
274pub struct ControlPoint {
275    pub position:  Vec3,
276    pub tangent_in:  Vec3,
277    pub tangent_out: Vec3,
278    pub weight: f32,          // for NURBS
279    pub knot_value: f32,      // for B-Spline/NURBS
280    pub id: u64,
281    pub tension: f32,         // for Hermite/Kochanek-Bartels
282}
283
284impl ControlPoint {
285    pub fn new(position: Vec3) -> Self {
286        ControlPoint {
287            position,
288            tangent_in:  Vec3::ZERO,
289            tangent_out: Vec3::ZERO,
290            weight: 1.0,
291            knot_value: 0.0,
292            id: rand_id(),
293            tension: 0.0,
294        }
295    }
296
297    pub fn with_tangents(position: Vec3, t_in: Vec3, t_out: Vec3) -> Self {
298        let mut cp = Self::new(position);
299        cp.tangent_in  = t_in;
300        cp.tangent_out = t_out;
301        cp
302    }
303}
304
305static CONTROL_POINT_ID_COUNTER: std::sync::atomic::AtomicU64 =
306    std::sync::atomic::AtomicU64::new(1);
307
308fn rand_id() -> u64 {
309    CONTROL_POINT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
310}
311
312// ============================================================
313// SPLINE TYPES ENUM
314// ============================================================
315
316#[derive(Clone, Debug, PartialEq)]
317pub enum SplineType {
318    CatmullRom,
319    CubicBezier,
320    BSpline { degree: usize },
321    Nurbs    { degree: usize },
322    Hermite,
323}
324
325// ============================================================
326// CATMULL-ROM SPLINE (centripetal parameterization)
327// ============================================================
328
329#[derive(Clone, Debug)]
330pub struct CatmullRomSpline {
331    pub control_points: Vec<ControlPoint>,
332    pub closed: bool,
333    pub alpha: f32,  // 0=uniform, 0.5=centripetal, 1=chordal
334    arc_length_table: Vec<(f32, f32)>,
335    total_length: f32,
336}
337
338impl CatmullRomSpline {
339    pub fn new(points: Vec<Vec3>, alpha: f32, closed: bool) -> Self {
340        let control_points = points.into_iter().map(ControlPoint::new).collect();
341        let mut s = CatmullRomSpline {
342            control_points,
343            closed,
344            alpha,
345            arc_length_table: Vec::new(),
346            total_length: 0.0,
347        };
348        s.rebuild_arc_length_table();
349        s
350    }
351
352    fn num_segments(&self) -> usize {
353        let n = self.control_points.len();
354        if n < 2 { return 0; }
355        if self.closed { n } else { n - 1 }
356    }
357
358    fn get_point(&self, i: usize) -> Vec3 {
359        let n = self.control_points.len();
360        self.control_points[i % n].position
361    }
362
363    fn segment_t_values(&self, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> [f32; 4] {
364        let t0 = 0.0_f32;
365        let t1 = t0 + (p1 - p0).length().powf(self.alpha);
366        let t2 = t1 + (p2 - p1).length().powf(self.alpha);
367        let t3 = t2 + (p3 - p2).length().powf(self.alpha);
368        [t0, t1, t2, t3]
369    }
370
371    /// Evaluate position at local segment parameter u in [0,1]
372    pub fn eval_segment(&self, seg: usize, u: f32) -> Vec3 {
373        let n = self.control_points.len();
374        if n < 2 { return Vec3::ZERO; }
375        let (i0, i1, i2, i3) = self.segment_indices(seg);
376        let p0 = self.get_point(i0);
377        let p1 = self.get_point(i1);
378        let p2 = self.get_point(i2);
379        let p3 = self.get_point(i3);
380        let [t0, t1, t2, t3] = self.segment_t_values(p0, p1, p2, p3);
381        let t = lerp(t1, t2, u);
382        self.barry_phase(p0, p1, p2, p3, t0, t1, t2, t3, t)
383    }
384
385    fn barry_phase(&self, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3,
386                   t0: f32, t1: f32, t2: f32, t3: f32, t: f32) -> Vec3 {
387        let safe_div = |n: Vec3, d: f32| if d.abs() < EPSILON { Vec3::ZERO } else { n / d };
388        let a1 = safe_div(p0 * (t1 - t) + p1 * (t - t0), t1 - t0);
389        let a2 = safe_div(p1 * (t2 - t) + p2 * (t - t1), t2 - t1);
390        let a3 = safe_div(p2 * (t3 - t) + p3 * (t - t2), t3 - t2);
391        let b1 = safe_div(a1 * (t2 - t) + a2 * (t - t0), t2 - t0);
392        let b2 = safe_div(a2 * (t3 - t) + a3 * (t - t1), t3 - t1);
393        safe_div(b1 * (t2 - t) + b2 * (t - t1), t2 - t1)
394    }
395
396    fn segment_indices(&self, seg: usize) -> (usize, usize, usize, usize) {
397        let n = self.control_points.len();
398        if self.closed {
399            let i1 = seg % n;
400            let i2 = (seg + 1) % n;
401            let i0 = (seg + n - 1) % n;
402            let i3 = (seg + 2) % n;
403            (i0, i1, i2, i3)
404        } else {
405            let i1 = seg.min(n - 1);
406            let i2 = (seg + 1).min(n - 1);
407            let i0 = if seg == 0 { 0 } else { seg - 1 };
408            let i3 = (seg + 2).min(n - 1);
409            (i0, i1, i2, i3)
410        }
411    }
412
413    /// Global parameter t in [0,1] -> position
414    pub fn evaluate(&self, t: f32) -> Vec3 {
415        let nseg = self.num_segments();
416        if nseg == 0 { return Vec3::ZERO; }
417        let t = if self.closed { t.fract() } else { clamp01(t) };
418        let scaled = t * nseg as f32;
419        let seg = (scaled as usize).min(nseg - 1);
420        let u   = scaled - seg as f32;
421        self.eval_segment(seg, u)
422    }
423
424    pub fn evaluate_derivative(&self, t: f32) -> Vec3 {
425        let dt = 1e-4;
426        let t = clamp01(t);
427        let fwd  = self.evaluate((t + dt).min(1.0));
428        let back = self.evaluate((t - dt).max(0.0));
429        (fwd - back) / (2.0 * dt)
430    }
431
432    pub fn evaluate_second_derivative(&self, t: f32) -> Vec3 {
433        let dt = 1e-4;
434        let t = clamp01(t);
435        let fwd   = self.evaluate((t + dt).min(1.0));
436        let cur   = self.evaluate(t);
437        let back  = self.evaluate((t - dt).max(0.0));
438        (fwd - 2.0 * cur + back) / (dt * dt)
439    }
440
441    pub fn evaluate_third_derivative(&self, t: f32) -> Vec3 {
442        let dt = 1e-4;
443        let t = clamp01(t);
444        let p3 = self.evaluate((t + 2.0 * dt).min(1.0));
445        let p1 = self.evaluate((t + dt).min(1.0));
446        let m1 = self.evaluate((t - dt).max(0.0));
447        let m3 = self.evaluate((t - 2.0 * dt).max(0.0));
448        (-p3 + 2.0 * p1 - 2.0 * m1 + m3) / (2.0 * dt.powi(3))
449    }
450
451    pub fn frenet_frame_at(&self, t: f32) -> FrenetFrame {
452        let pos = self.evaluate(t);
453        let d1  = self.evaluate_derivative(t);
454        let d2  = self.evaluate_second_derivative(t);
455        let d3  = self.evaluate_third_derivative(t);
456        FrenetFrame::compute(pos, d1, d2, d3)
457    }
458
459    pub fn rebuild_arc_length_table(&mut self) {
460        let table = build_arc_length_table(ARC_LENGTH_SAMPLE_COUNT, &|t| self.evaluate(t));
461        self.total_length = table.last().map(|e| e.1).unwrap_or(0.0);
462        self.arc_length_table = table;
463    }
464
465    pub fn total_arc_length(&self) -> f32 { self.total_length }
466
467    pub fn t_at_arc_length(&self, s: f32) -> f32 {
468        arc_length_to_t(&self.arc_length_table, s)
469    }
470
471    pub fn evaluate_at_arc_length(&self, s: f32) -> Vec3 {
472        self.evaluate(self.t_at_arc_length(s))
473    }
474
475    pub fn curvature_at(&self, t: f32) -> f32 {
476        self.frenet_frame_at(t).curvature
477    }
478
479    pub fn torsion_at(&self, t: f32) -> f32 {
480        self.frenet_frame_at(t).torsion
481    }
482
483    /// Nearest point on spline via binary search + Newton refinement
484    pub fn nearest_point(&self, query: Vec3) -> (f32, Vec3) {
485        let mut best_t   = 0.0_f32;
486        let mut best_d2  = f32::MAX;
487        let steps = 128usize;
488        for i in 0..=steps {
489            let t = i as f32 / steps as f32;
490            let p = self.evaluate(t);
491            let d2 = (p - query).length_squared();
492            if d2 < best_d2 {
493                best_d2 = d2;
494                best_t  = t;
495            }
496        }
497        // Newton refinement
498        let t = newton_nearest_on_spline(best_t, query, &|t| self.evaluate(t),
499                                         &|t| self.evaluate_derivative(t));
500        (t, self.evaluate(t))
501    }
502
503    /// Insert a knot at parameter t, splitting a segment
504    pub fn insert_knot(&mut self, t: f32) {
505        let pos = self.evaluate(t);
506        let idx = {
507            let nseg = self.num_segments();
508            let scaled = clamp01(t) * nseg as f32;
509            (scaled as usize).min(nseg.saturating_sub(1))
510        };
511        let new_cp = ControlPoint::new(pos);
512        self.control_points.insert(idx + 1, new_cp);
513        self.rebuild_arc_length_table();
514    }
515
516    pub fn split_at(&self, t: f32) -> (CatmullRomSpline, CatmullRomSpline) {
517        let n = self.control_points.len();
518        let nseg = self.num_segments();
519        let scaled = clamp01(t) * nseg as f32;
520        let seg = (scaled as usize).min(nseg.saturating_sub(1));
521        let split_idx = seg + 1;
522        let pts_a: Vec<Vec3> = self.control_points[..split_idx.min(n)].iter()
523            .map(|cp| cp.position).collect();
524        let pts_b: Vec<Vec3> = self.control_points[split_idx.min(n)..].iter()
525            .map(|cp| cp.position).collect();
526        let mut a = CatmullRomSpline::new(pts_a, self.alpha, false);
527        let mut b = CatmullRomSpline::new(pts_b, self.alpha, false);
528        // add split point to both
529        let split_pos = self.evaluate(t);
530        a.control_points.push(ControlPoint::new(split_pos));
531        if !b.control_points.is_empty() {
532            b.control_points.insert(0, ControlPoint::new(split_pos));
533        } else {
534            b.control_points.push(ControlPoint::new(split_pos));
535        }
536        a.rebuild_arc_length_table();
537        b.rebuild_arc_length_table();
538        (a, b)
539    }
540
541    pub fn join(mut a: CatmullRomSpline, b: CatmullRomSpline) -> CatmullRomSpline {
542        for cp in b.control_points {
543            a.control_points.push(cp);
544        }
545        a.rebuild_arc_length_table();
546        a
547    }
548
549    pub fn toggle_closed(&mut self) {
550        self.closed = !self.closed;
551        self.rebuild_arc_length_table();
552    }
553
554    pub fn bounding_box(&self) -> (Vec3, Vec3) {
555        let mut min = Vec3::splat(f32::MAX);
556        let mut max = Vec3::splat(f32::MIN);
557        let steps = 200;
558        for i in 0..=steps {
559            let t = i as f32 / steps as f32;
560            let p = self.evaluate(t);
561            min = min.min(p);
562            max = max.max(p);
563        }
564        (min, max)
565    }
566}
567
568// ============================================================
569// CUBIC BEZIER SPLINE (De Casteljau)
570// ============================================================
571
572#[derive(Clone, Debug)]
573pub struct CubicBezierSpline {
574    /// Control points in groups of 4: P0, P1, P2, P3 per segment
575    /// Adjacent segments share endpoints: P3 of seg i == P0 of seg i+1
576    pub segments: Vec<[Vec3; 4]>,
577    pub closed: bool,
578    arc_length_table: Vec<(f32, f32)>,
579    total_length: f32,
580}
581
582impl CubicBezierSpline {
583    pub fn new(segments: Vec<[Vec3; 4]>) -> Self {
584        let mut s = CubicBezierSpline {
585            segments,
586            closed: false,
587            arc_length_table: Vec::new(),
588            total_length: 0.0,
589        };
590        s.rebuild_arc_length_table();
591        s
592    }
593
594    pub fn from_points(points: &[Vec3]) -> Self {
595        // Auto-generate smooth cubic bezier from polyline
596        let n = points.len();
597        if n < 2 {
598            return CubicBezierSpline::new(Vec::new());
599        }
600        let mut segs = Vec::new();
601        for i in 0..n.saturating_sub(1) {
602            let p0 = points[i];
603            let p3 = points[i + 1];
604            let prev = if i > 0 { points[i - 1] } else { p0 };
605            let next = if i + 2 < n { points[i + 2] } else { p3 };
606            let p1 = p0 + (p3 - prev) * (1.0 / 6.0);
607            let p2 = p3 - (next - p0) * (1.0 / 6.0);
608            segs.push([p0, p1, p2, p3]);
609        }
610        CubicBezierSpline::new(segs)
611    }
612
613    /// De Casteljau evaluation at t in [0,1] for a single segment
614    pub fn de_casteljau(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
615        let q0 = lerp_vec3(p0, p1, t);
616        let q1 = lerp_vec3(p1, p2, t);
617        let q2 = lerp_vec3(p2, p3, t);
618        let r0 = lerp_vec3(q0, q1, t);
619        let r1 = lerp_vec3(q1, q2, t);
620        lerp_vec3(r0, r1, t)
621    }
622
623    /// De Casteljau split: returns left and right halves
624    pub fn de_casteljau_split(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32)
625        -> ([Vec3; 4], [Vec3; 4])
626    {
627        let q0 = lerp_vec3(p0, p1, t);
628        let q1 = lerp_vec3(p1, p2, t);
629        let q2 = lerp_vec3(p2, p3, t);
630        let r0 = lerp_vec3(q0, q1, t);
631        let r1 = lerp_vec3(q1, q2, t);
632        let s  = lerp_vec3(r0, r1, t);
633        ([p0, q0, r0, s], [s, r1, q2, p3])
634    }
635
636    pub fn num_segments(&self) -> usize { self.segments.len() }
637
638    pub fn evaluate(&self, t: f32) -> Vec3 {
639        let n = self.segments.len();
640        if n == 0 { return Vec3::ZERO; }
641        let t = clamp01(t);
642        let scaled = t * n as f32;
643        let seg = (scaled as usize).min(n - 1);
644        let u   = scaled - seg as f32;
645        let [p0, p1, p2, p3] = self.segments[seg];
646        Self::de_casteljau(p0, p1, p2, p3, u)
647    }
648
649    pub fn evaluate_derivative(&self, t: f32) -> Vec3 {
650        let n = self.segments.len();
651        if n == 0 { return Vec3::ZERO; }
652        let t = clamp01(t);
653        let scaled = t * n as f32;
654        let seg = (scaled as usize).min(n - 1);
655        let u   = scaled - seg as f32;
656        let [p0, p1, p2, p3] = self.segments[seg];
657        // Derivative of cubic bezier: 3*(B(u) where control pts are differences)
658        let d0 = 3.0 * (p1 - p0);
659        let d1 = 3.0 * (p2 - p1);
660        let d2 = 3.0 * (p3 - p2);
661        Self::de_casteljau(d0, d1, d2, Vec3::ZERO, u) // quadratic bezier
662        // Actually: derivative is a quadratic bezier with 3 control pts d0,d1,d2
663        // evaluated at u:
664        // = (1-u)^2 d0 + 2u(1-u) d1 + u^2 d2
665    }
666
667    pub fn evaluate_derivative_correct(&self, t: f32) -> Vec3 {
668        let n = self.segments.len();
669        if n == 0 { return Vec3::ZERO; }
670        let t = clamp01(t);
671        let scaled = t * n as f32;
672        let seg = (scaled as usize).min(n - 1);
673        let u   = scaled - seg as f32;
674        let [p0, p1, p2, p3] = self.segments[seg];
675        let u2 = u * u;
676        let t1 = 1.0 - u;
677        let t12 = t1 * t1;
678        // B'(u) = 3[(p1-p0)(1-u)^2 + 2(p2-p1)u(1-u) + (p3-p2)u^2]
679        3.0 * ((p1 - p0) * t12 + 2.0 * (p2 - p1) * u * t1 + (p3 - p2) * u2)
680    }
681
682    pub fn evaluate_second_derivative_correct(&self, t: f32) -> Vec3 {
683        let n = self.segments.len();
684        if n == 0 { return Vec3::ZERO; }
685        let t = clamp01(t);
686        let scaled = t * n as f32;
687        let seg = (scaled as usize).min(n - 1);
688        let u   = scaled - seg as f32;
689        let [p0, p1, p2, p3] = self.segments[seg];
690        // B''(u) = 6[(p2-2p1+p0)(1-u) + (p3-2p2+p1)u]
691        6.0 * ((p2 - 2.0 * p1 + p0) * (1.0 - u) + (p3 - 2.0 * p2 + p1) * u)
692    }
693
694    pub fn curvature_at(&self, t: f32) -> f32 {
695        let d1 = self.evaluate_derivative_correct(t);
696        let d2 = self.evaluate_second_derivative_correct(t);
697        let cross = d1.cross(d2).length();
698        let speed = d1.length();
699        if speed < EPSILON { 0.0 } else { cross / speed.powi(3) }
700    }
701
702    pub fn rebuild_arc_length_table(&mut self) {
703        let table = build_arc_length_table(ARC_LENGTH_SAMPLE_COUNT, &|t| self.evaluate(t));
704        self.total_length = table.last().map(|e| e.1).unwrap_or(0.0);
705        self.arc_length_table = table;
706    }
707
708    pub fn total_arc_length(&self) -> f32 { self.total_length }
709
710    pub fn t_at_arc_length(&self, s: f32) -> f32 {
711        arc_length_to_t(&self.arc_length_table, s)
712    }
713
714    pub fn evaluate_at_arc_length(&self, s: f32) -> Vec3 {
715        self.evaluate(self.t_at_arc_length(s))
716    }
717
718    pub fn split_segment(&mut self, seg: usize, u: f32) {
719        if seg >= self.segments.len() { return; }
720        let [p0, p1, p2, p3] = self.segments[seg];
721        let (left, right) = Self::de_casteljau_split(p0, p1, p2, p3, u);
722        self.segments.remove(seg);
723        self.segments.insert(seg, right);
724        self.segments.insert(seg, left);
725        self.rebuild_arc_length_table();
726    }
727
728    pub fn nearest_point(&self, query: Vec3) -> (f32, Vec3) {
729        let mut best_t = 0.0_f32;
730        let mut best_d2 = f32::MAX;
731        let steps = 200usize;
732        for i in 0..=steps {
733            let t = i as f32 / steps as f32;
734            let p = self.evaluate(t);
735            let d2 = (p - query).length_squared();
736            if d2 < best_d2 {
737                best_d2 = d2;
738                best_t = t;
739            }
740        }
741        let t = newton_nearest_on_spline(best_t, query,
742            &|t| self.evaluate(t),
743            &|t| self.evaluate_derivative_correct(t));
744        (t, self.evaluate(t))
745    }
746
747    pub fn frenet_frame_at(&self, t: f32) -> FrenetFrame {
748        let pos = self.evaluate(t);
749        let d1  = self.evaluate_derivative_correct(t);
750        let d2  = self.evaluate_second_derivative_correct(t);
751        let dt = 1e-4;
752        let d2a = self.evaluate_second_derivative_correct((t + dt).min(1.0));
753        let d2b = self.evaluate_second_derivative_correct((t - dt).max(0.0));
754        let d3  = (d2a - d2b) / (2.0 * dt);
755        FrenetFrame::compute(pos, d1, d2, d3)
756    }
757
758    pub fn bounding_box(&self) -> (Vec3, Vec3) {
759        let mut min = Vec3::splat(f32::MAX);
760        let mut max = Vec3::splat(f32::MIN);
761        for seg in &self.segments {
762            for &p in seg.iter() {
763                min = min.min(p);
764                max = max.max(p);
765            }
766        }
767        (min, max)
768    }
769}
770
771// ============================================================
772// B-SPLINE (Cox-de Boor recursion)
773// ============================================================
774
775#[derive(Clone, Debug)]
776pub struct BSpline {
777    pub control_points: Vec<Vec3>,
778    pub knots: Vec<f32>,
779    pub degree: usize,
780    pub closed: bool,
781    arc_length_table: Vec<(f32, f32)>,
782    total_length: f32,
783}
784
785impl BSpline {
786    pub fn new(control_points: Vec<Vec3>, degree: usize, closed: bool) -> Self {
787        let mut s = BSpline {
788            knots: Vec::new(),
789            control_points,
790            degree,
791            closed,
792            arc_length_table: Vec::new(),
793            total_length: 0.0,
794        };
795        s.generate_uniform_knots();
796        s.rebuild_arc_length_table();
797        s
798    }
799
800    pub fn generate_uniform_knots(&mut self) {
801        let n = self.control_points.len();
802        let k = self.degree;
803        // Clamped uniform knot vector: m = n + k + 1 knots
804        let m = n + k + 1;
805        let mut knots = Vec::with_capacity(m);
806        for i in 0..m {
807            if i < k + 1 {
808                knots.push(0.0);
809            } else if i > n {
810                knots.push(1.0);
811            } else {
812                knots.push((i - k) as f32 / (n - k) as f32);
813            }
814        }
815        self.knots = knots;
816    }
817
818    /// Cox-de Boor basis function N_{i,k}(t)
819    fn basis(&self, i: usize, k: usize, t: f32) -> f32 {
820        if k == 0 {
821            let a = self.knots.get(i).cloned().unwrap_or(0.0);
822            let b = self.knots.get(i + 1).cloned().unwrap_or(0.0);
823            if t >= a && t < b { 1.0 } else { 0.0 }
824        } else {
825            let ti   = self.knots.get(i).cloned().unwrap_or(0.0);
826            let tik  = self.knots.get(i + k).cloned().unwrap_or(0.0);
827            let ti1  = self.knots.get(i + 1).cloned().unwrap_or(0.0);
828            let tik1 = self.knots.get(i + k + 1).cloned().unwrap_or(0.0);
829            let left = if (tik - ti).abs() < EPSILON { 0.0 }
830                       else { (t - ti) / (tik - ti) * self.basis(i, k - 1, t) };
831            let right = if (tik1 - ti1).abs() < EPSILON { 0.0 }
832                        else { (tik1 - t) / (tik1 - ti1) * self.basis(i + 1, k - 1, t) };
833            left + right
834        }
835    }
836
837    pub fn evaluate(&self, t: f32) -> Vec3 {
838        let n = self.control_points.len();
839        if n == 0 { return Vec3::ZERO; }
840        let t_min = self.knots.first().cloned().unwrap_or(0.0);
841        let t_max = self.knots.last().cloned().unwrap_or(1.0);
842        // Clamp t slightly to avoid endpoint issues
843        let t = t.clamp(t_min, t_max - EPSILON);
844        let mut result = Vec3::ZERO;
845        for i in 0..n {
846            let b = self.basis(i, self.degree, t);
847            result += self.control_points[i] * b;
848        }
849        result
850    }
851
852    pub fn evaluate_derivative(&self, t: f32) -> Vec3 {
853        let dt = 1e-4;
854        let a = self.evaluate((t + dt).min(1.0 - EPSILON));
855        let b = self.evaluate((t - dt).max(EPSILON));
856        (a - b) / (2.0 * dt)
857    }
858
859    pub fn evaluate_second_derivative(&self, t: f32) -> Vec3 {
860        let dt = 1e-4;
861        let a = self.evaluate((t + dt).min(1.0 - EPSILON));
862        let c = self.evaluate(t);
863        let b = self.evaluate((t - dt).max(EPSILON));
864        (a - 2.0 * c + b) / (dt * dt)
865    }
866
867    pub fn rebuild_arc_length_table(&mut self) {
868        let table = build_arc_length_table(ARC_LENGTH_SAMPLE_COUNT, &|t| self.evaluate(t));
869        self.total_length = table.last().map(|e| e.1).unwrap_or(0.0);
870        self.arc_length_table = table;
871    }
872
873    pub fn total_arc_length(&self) -> f32 { self.total_length }
874
875    pub fn t_at_arc_length(&self, s: f32) -> f32 {
876        arc_length_to_t(&self.arc_length_table, s)
877    }
878
879    pub fn evaluate_at_arc_length(&self, s: f32) -> Vec3 {
880        self.evaluate(self.t_at_arc_length(s))
881    }
882
883    pub fn frenet_frame_at(&self, t: f32) -> FrenetFrame {
884        let pos = self.evaluate(t);
885        let d1  = self.evaluate_derivative(t);
886        let d2  = self.evaluate_second_derivative(t);
887        let dt = 1e-4;
888        let d2a = self.evaluate_second_derivative((t + dt).min(1.0 - EPSILON));
889        let d2b = self.evaluate_second_derivative((t - dt).max(EPSILON));
890        let d3  = (d2a - d2b) / (2.0 * dt);
891        FrenetFrame::compute(pos, d1, d2, d3)
892    }
893
894    pub fn curvature_at(&self, t: f32) -> f32 {
895        self.frenet_frame_at(t).curvature
896    }
897
898    /// Knot insertion using Boehm's algorithm
899    pub fn insert_knot(&mut self, t_new: f32) {
900        // find span
901        let n  = self.control_points.len();
902        let k  = self.degree;
903        let mut r = 0usize;
904        for i in 0..self.knots.len().saturating_sub(1) {
905            if self.knots[i] <= t_new && t_new < self.knots[i + 1] {
906                r = i;
907            }
908        }
909        // new control points
910        let mut new_pts = Vec::with_capacity(n + 1);
911        for i in 0..=n {
912            if i <= r.saturating_sub(k) {
913                new_pts.push(self.control_points.get(i).cloned().unwrap_or(Vec3::ZERO));
914            } else if i > r {
915                new_pts.push(self.control_points.get(i.saturating_sub(1)).cloned().unwrap_or(Vec3::ZERO));
916            } else {
917                let ti   = self.knots.get(i).cloned().unwrap_or(0.0);
918                let tik1 = self.knots.get(i + k).cloned().unwrap_or(1.0);
919                let alpha = if (tik1 - ti).abs() < EPSILON { 0.5 }
920                            else { (t_new - ti) / (tik1 - ti) };
921                let prev = self.control_points.get(i.saturating_sub(1)).cloned().unwrap_or(Vec3::ZERO);
922                let curr = self.control_points.get(i).cloned().unwrap_or(Vec3::ZERO);
923                new_pts.push(lerp_vec3(prev, curr, alpha));
924            }
925        }
926        self.control_points = new_pts;
927        self.knots.insert(r + 1, t_new);
928        self.rebuild_arc_length_table();
929    }
930
931    pub fn bounding_box(&self) -> (Vec3, Vec3) {
932        let mut min = Vec3::splat(f32::MAX);
933        let mut max = Vec3::splat(f32::MIN);
934        for &p in &self.control_points {
935            min = min.min(p);
936            max = max.max(p);
937        }
938        (min, max)
939    }
940}
941
942// ============================================================
943// NURBS (Rational B-Spline)
944// ============================================================
945
946#[derive(Clone, Debug)]
947pub struct NurbsSpline {
948    pub control_points: Vec<Vec3>,
949    pub weights: Vec<f32>,
950    pub knots: Vec<f32>,
951    pub degree: usize,
952    pub closed: bool,
953    arc_length_table: Vec<(f32, f32)>,
954    total_length: f32,
955}
956
957impl NurbsSpline {
958    pub fn new(control_points: Vec<Vec3>, weights: Vec<f32>, degree: usize) -> Self {
959        let n = control_points.len();
960        assert_eq!(weights.len(), n, "NURBS: weights and control points must match");
961        let mut s = NurbsSpline {
962            control_points,
963            weights,
964            knots: Vec::new(),
965            degree,
966            closed: false,
967            arc_length_table: Vec::new(),
968            total_length: 0.0,
969        };
970        s.generate_uniform_knots();
971        s.rebuild_arc_length_table();
972        s
973    }
974
975    fn generate_uniform_knots(&mut self) {
976        let n = self.control_points.len();
977        let k = self.degree;
978        let m = n + k + 1;
979        let mut knots = Vec::with_capacity(m);
980        for i in 0..m {
981            if i < k + 1 { knots.push(0.0); }
982            else if i > n { knots.push(1.0); }
983            else { knots.push((i - k) as f32 / (n - k) as f32); }
984        }
985        self.knots = knots;
986    }
987
988    fn basis(&self, i: usize, k: usize, t: f32) -> f32 {
989        if k == 0 {
990            let a = self.knots.get(i).cloned().unwrap_or(0.0);
991            let b = self.knots.get(i + 1).cloned().unwrap_or(0.0);
992            if t >= a && t < b { 1.0 } else { 0.0 }
993        } else {
994            let ti   = self.knots.get(i).cloned().unwrap_or(0.0);
995            let tik  = self.knots.get(i + k).cloned().unwrap_or(0.0);
996            let ti1  = self.knots.get(i + 1).cloned().unwrap_or(0.0);
997            let tik1 = self.knots.get(i + k + 1).cloned().unwrap_or(0.0);
998            let left  = if (tik - ti).abs() < EPSILON { 0.0 }
999                        else { (t - ti) / (tik - ti) * self.basis(i, k - 1, t) };
1000            let right = if (tik1 - ti1).abs() < EPSILON { 0.0 }
1001                        else { (tik1 - t) / (tik1 - ti1) * self.basis(i + 1, k - 1, t) };
1002            left + right
1003        }
1004    }
1005
1006    pub fn evaluate(&self, t: f32) -> Vec3 {
1007        let n = self.control_points.len();
1008        if n == 0 { return Vec3::ZERO; }
1009        let t_max = self.knots.last().cloned().unwrap_or(1.0);
1010        let t = t.clamp(0.0, t_max - EPSILON);
1011        let mut numerator   = Vec3::ZERO;
1012        let mut denominator = 0.0_f32;
1013        for i in 0..n {
1014            let b = self.basis(i, self.degree, t);
1015            let w = self.weights[i];
1016            numerator   += self.control_points[i] * (b * w);
1017            denominator += b * w;
1018        }
1019        if denominator.abs() < EPSILON { Vec3::ZERO } else { numerator / denominator }
1020    }
1021
1022    pub fn evaluate_derivative(&self, t: f32) -> Vec3 {
1023        let dt = 1e-4;
1024        let a = self.evaluate((t + dt).min(1.0 - EPSILON));
1025        let b = self.evaluate((t - dt).max(EPSILON));
1026        (a - b) / (2.0 * dt)
1027    }
1028
1029    pub fn evaluate_second_derivative(&self, t: f32) -> Vec3 {
1030        let dt = 1e-4;
1031        let a = self.evaluate((t + dt).min(1.0 - EPSILON));
1032        let c = self.evaluate(t);
1033        let b = self.evaluate((t - dt).max(EPSILON));
1034        (a - 2.0 * c + b) / (dt * dt)
1035    }
1036
1037    pub fn rebuild_arc_length_table(&mut self) {
1038        let table = build_arc_length_table(ARC_LENGTH_SAMPLE_COUNT, &|t| self.evaluate(t));
1039        self.total_length = table.last().map(|e| e.1).unwrap_or(0.0);
1040        self.arc_length_table = table;
1041    }
1042
1043    pub fn total_arc_length(&self) -> f32 { self.total_length }
1044
1045    pub fn t_at_arc_length(&self, s: f32) -> f32 {
1046        arc_length_to_t(&self.arc_length_table, s)
1047    }
1048
1049    pub fn evaluate_at_arc_length(&self, s: f32) -> Vec3 {
1050        self.evaluate(self.t_at_arc_length(s))
1051    }
1052
1053    pub fn frenet_frame_at(&self, t: f32) -> FrenetFrame {
1054        let pos = self.evaluate(t);
1055        let d1  = self.evaluate_derivative(t);
1056        let d2  = self.evaluate_second_derivative(t);
1057        let dt  = 1e-4;
1058        let d2a = self.evaluate_second_derivative((t + dt).min(1.0 - EPSILON));
1059        let d2b = self.evaluate_second_derivative((t - dt).max(EPSILON));
1060        let d3  = (d2a - d2b) / (2.0 * dt);
1061        FrenetFrame::compute(pos, d1, d2, d3)
1062    }
1063
1064    pub fn curvature_at(&self, t: f32) -> f32 {
1065        self.frenet_frame_at(t).curvature
1066    }
1067
1068    pub fn circle_nurbs(center: Vec3, radius: f32, normal: Vec3) -> NurbsSpline {
1069        // Quarter-arc NURBS circle (9 control points, degree 2)
1070        let up = safe_normalize(normal.cross(Vec3::X));
1071        let right = safe_normalize(normal.cross(up));
1072        let r = radius;
1073        let w = std::f32::consts::FRAC_1_SQRT_2; // cos(45°)
1074        let mut pts = Vec::new();
1075        let mut wts = Vec::new();
1076        // 9-point NURBS circle
1077        let angles = [0.0_f32, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0, 360.0];
1078        for (i, &a) in angles.iter().enumerate() {
1079            let rad = a.to_radians();
1080            let pt = center + right * (rad.cos() * r) + up * (rad.sin() * r);
1081            pts.push(pt);
1082            if i % 2 == 0 { wts.push(1.0); } else { wts.push(w); }
1083        }
1084        let knots = vec![0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0];
1085        NurbsSpline {
1086            control_points: pts,
1087            weights: wts,
1088            knots,
1089            degree: 2,
1090            closed: true,
1091            arc_length_table: Vec::new(),
1092            total_length: 0.0,
1093        }
1094    }
1095}
1096
1097// ============================================================
1098// HERMITE SPLINE
1099// ============================================================
1100
1101#[derive(Clone, Debug)]
1102pub struct HermiteSpline {
1103    /// Each entry: (position, tangent)
1104    pub control_points: Vec<(Vec3, Vec3)>,
1105    pub closed: bool,
1106    arc_length_table: Vec<(f32, f32)>,
1107    total_length: f32,
1108}
1109
1110impl HermiteSpline {
1111    pub fn new(points: Vec<(Vec3, Vec3)>) -> Self {
1112        let mut s = HermiteSpline {
1113            control_points: points,
1114            closed: false,
1115            arc_length_table: Vec::new(),
1116            total_length: 0.0,
1117        };
1118        s.rebuild_arc_length_table();
1119        s
1120    }
1121
1122    pub fn num_segments(&self) -> usize {
1123        let n = self.control_points.len();
1124        if n < 2 { 0 }
1125        else if self.closed { n }
1126        else { n - 1 }
1127    }
1128
1129    pub fn eval_segment(&self, seg: usize, u: f32) -> Vec3 {
1130        let n = self.control_points.len();
1131        let i0 = seg % n;
1132        let i1 = (seg + 1) % n;
1133        let (p0, m0) = self.control_points[i0];
1134        let (p1, m1) = self.control_points[i1];
1135        // Cubic Hermite basis functions
1136        let u2 = u * u;
1137        let u3 = u2 * u;
1138        let h00 =  2.0 * u3 - 3.0 * u2 + 1.0;
1139        let h10 =        u3 - 2.0 * u2 + u;
1140        let h01 = -2.0 * u3 + 3.0 * u2;
1141        let h11 =        u3 -       u2;
1142        p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
1143    }
1144
1145    pub fn eval_segment_derivative(&self, seg: usize, u: f32) -> Vec3 {
1146        let n = self.control_points.len();
1147        let i0 = seg % n;
1148        let i1 = (seg + 1) % n;
1149        let (p0, m0) = self.control_points[i0];
1150        let (p1, m1) = self.control_points[i1];
1151        let u2 = u * u;
1152        let dh00 =  6.0 * u2 - 6.0 * u;
1153        let dh10 =  3.0 * u2 - 4.0 * u + 1.0;
1154        let dh01 = -6.0 * u2 + 6.0 * u;
1155        let dh11 =  3.0 * u2 - 2.0 * u;
1156        p0 * dh00 + m0 * dh10 + p1 * dh01 + m1 * dh11
1157    }
1158
1159    pub fn eval_segment_second_derivative(&self, seg: usize, u: f32) -> Vec3 {
1160        let n = self.control_points.len();
1161        let i0 = seg % n;
1162        let i1 = (seg + 1) % n;
1163        let (p0, m0) = self.control_points[i0];
1164        let (p1, m1) = self.control_points[i1];
1165        let ddh00 =  12.0 * u - 6.0;
1166        let ddh10 =   6.0 * u - 4.0;
1167        let ddh01 = -12.0 * u + 6.0;
1168        let ddh11 =   6.0 * u - 2.0;
1169        p0 * ddh00 + m0 * ddh10 + p1 * ddh01 + m1 * ddh11
1170    }
1171
1172    pub fn evaluate(&self, t: f32) -> Vec3 {
1173        let nseg = self.num_segments();
1174        if nseg == 0 { return Vec3::ZERO; }
1175        let t = clamp01(t);
1176        let scaled = t * nseg as f32;
1177        let seg = (scaled as usize).min(nseg - 1);
1178        let u   = scaled - seg as f32;
1179        self.eval_segment(seg, u)
1180    }
1181
1182    pub fn evaluate_derivative(&self, t: f32) -> Vec3 {
1183        let nseg = self.num_segments();
1184        if nseg == 0 { return Vec3::ZERO; }
1185        let t = clamp01(t);
1186        let scaled = t * nseg as f32;
1187        let seg = (scaled as usize).min(nseg - 1);
1188        let u   = scaled - seg as f32;
1189        self.eval_segment_derivative(seg, u) * nseg as f32
1190    }
1191
1192    pub fn evaluate_second_derivative(&self, t: f32) -> Vec3 {
1193        let nseg = self.num_segments();
1194        if nseg == 0 { return Vec3::ZERO; }
1195        let t = clamp01(t);
1196        let scaled = t * nseg as f32;
1197        let seg = (scaled as usize).min(nseg - 1);
1198        let u   = scaled - seg as f32;
1199        self.eval_segment_second_derivative(seg, u) * (nseg * nseg) as f32
1200    }
1201
1202    pub fn rebuild_arc_length_table(&mut self) {
1203        let table = build_arc_length_table(ARC_LENGTH_SAMPLE_COUNT, &|t| self.evaluate(t));
1204        self.total_length = table.last().map(|e| e.1).unwrap_or(0.0);
1205        self.arc_length_table = table;
1206    }
1207
1208    pub fn total_arc_length(&self) -> f32 { self.total_length }
1209
1210    pub fn t_at_arc_length(&self, s: f32) -> f32 {
1211        arc_length_to_t(&self.arc_length_table, s)
1212    }
1213
1214    pub fn frenet_frame_at(&self, t: f32) -> FrenetFrame {
1215        let pos = self.evaluate(t);
1216        let d1  = self.evaluate_derivative(t);
1217        let d2  = self.evaluate_second_derivative(t);
1218        let dt  = 1e-4;
1219        let d2a = self.evaluate_second_derivative((t + dt).min(1.0));
1220        let d2b = self.evaluate_second_derivative((t - dt).max(0.0));
1221        let d3  = (d2a - d2b) / (2.0 * dt);
1222        FrenetFrame::compute(pos, d1, d2, d3)
1223    }
1224
1225    pub fn auto_tangents(&mut self) {
1226        let n = self.control_points.len();
1227        if n < 2 { return; }
1228        for i in 0..n {
1229            let prev = if i > 0 { self.control_points[i - 1].0 } else { self.control_points[0].0 };
1230            let next = if i + 1 < n { self.control_points[i + 1].0 } else { self.control_points[n - 1].0 };
1231            self.control_points[i].1 = (next - prev) * 0.5;
1232        }
1233        self.rebuild_arc_length_table();
1234    }
1235}
1236
1237// ============================================================
1238// NEWTON'S METHOD NEAREST POINT
1239// ============================================================
1240
1241fn newton_nearest_on_spline(
1242    t0: f32,
1243    query: Vec3,
1244    pos_fn: &dyn Fn(f32) -> Vec3,
1245    der_fn: &dyn Fn(f32) -> Vec3,
1246) -> f32 {
1247    let mut t = t0;
1248    for _ in 0..NEWTON_MAX_ITER {
1249        let p  = pos_fn(t);
1250        let d1 = der_fn(t);
1251        let err = (p - query).dot(d1);
1252        let denom = d1.dot(d1) + (p - query).dot(Vec3::ZERO); // simplified, omit d2 term
1253        if denom.abs() < EPSILON { break; }
1254        let delta = err / denom;
1255        t -= delta;
1256        t = clamp01(t);
1257        if delta.abs() < NEWTON_TOL { break; }
1258    }
1259    t
1260}
1261
1262// ============================================================
1263// SPLINE-PLANE INTERSECTION
1264// ============================================================
1265
1266pub struct SplinePlaneIntersection {
1267    pub t: f32,
1268    pub point: Vec3,
1269}
1270
1271pub fn intersect_spline_plane(
1272    pos_fn: &dyn Fn(f32) -> Vec3,
1273    plane_normal: Vec3,
1274    plane_d: f32,
1275    steps: usize,
1276) -> Vec<SplinePlaneIntersection> {
1277    let mut results = Vec::new();
1278    let sdf = |t: f32| {
1279        let p = pos_fn(t);
1280        plane_normal.dot(p) - plane_d
1281    };
1282    let mut prev_val = sdf(0.0);
1283    for i in 1..=steps {
1284        let t1 = i as f32 / steps as f32;
1285        let val = sdf(t1);
1286        if prev_val * val <= 0.0 {
1287            let t0 = (i - 1) as f32 / steps as f32;
1288            // Bisect
1289            let mut lo = t0;
1290            let mut hi = t1;
1291            for _ in 0..32 {
1292                let mid = (lo + hi) * 0.5;
1293                let v = sdf(mid);
1294                if v * sdf(lo) <= 0.0 { hi = mid; } else { lo = mid; }
1295            }
1296            let t_hit = (lo + hi) * 0.5;
1297            results.push(SplinePlaneIntersection {
1298                t: t_hit,
1299                point: pos_fn(t_hit),
1300            });
1301        }
1302        prev_val = val;
1303    }
1304    results
1305}
1306
1307// ============================================================
1308// SPLINE-SPLINE INTERSECTION (Newton-Raphson on distance)
1309// ============================================================
1310
1311pub struct SplineSplineIntersection {
1312    pub t_a: f32,
1313    pub t_b: f32,
1314    pub point_a: Vec3,
1315    pub point_b: Vec3,
1316    pub distance: f32,
1317}
1318
1319pub fn intersect_spline_spline(
1320    pos_a: &dyn Fn(f32) -> Vec3,
1321    pos_b: &dyn Fn(f32) -> Vec3,
1322    grid_steps: usize,
1323    tol: f32,
1324) -> Vec<SplineSplineIntersection> {
1325    let mut results = Vec::new();
1326    let mut checked: HashSet<(u32, u32)> = HashSet::new();
1327    // Coarse grid search
1328    for ia in 0..=grid_steps {
1329        for ib in 0..=grid_steps {
1330            let ta = ia as f32 / grid_steps as f32;
1331            let tb = ib as f32 / grid_steps as f32;
1332            let d = (pos_a(ta) - pos_b(tb)).length();
1333            if d < tol * 10.0 {
1334                // Newton-Raphson refinement on 2D residual
1335                let mut ta2 = ta;
1336                let mut tb2 = tb;
1337                for _ in 0..32 {
1338                    let pa = pos_a(ta2);
1339                    let pb = pos_b(tb2);
1340                    let diff = pa - pb;
1341                    let da = (pos_a(ta2 + 1e-4) - pos_a(ta2 - 1e-4)) / 2e-4;
1342                    let db = (pos_b(tb2 + 1e-4) - pos_b(tb2 - 1e-4)) / 2e-4;
1343                    // Jacobian J = [da, -db], solve J*[dta, dtb]^T = -diff
1344                    let j00 = da.dot(da);
1345                    let j01 = -da.dot(db);
1346                    let j10 = -db.dot(da);
1347                    let j11 = db.dot(db);
1348                    let det = j00 * j11 - j01 * j10;
1349                    if det.abs() < EPSILON { break; }
1350                    let r0 = diff.dot(da);
1351                    let r1 = -diff.dot(db);
1352                    let dta = (j11 * r0 - j01 * r1) / det;
1353                    let dtb = (j00 * r1 - j10 * r0) / det;
1354                    ta2 = (ta2 - dta).clamp(0.0, 1.0);
1355                    tb2 = (tb2 - dtb).clamp(0.0, 1.0);
1356                    if dta.abs() < tol && dtb.abs() < tol { break; }
1357                }
1358                let dist = (pos_a(ta2) - pos_b(tb2)).length();
1359                if dist < tol {
1360                    let key = ((ta2 * 1000.0) as u32, (tb2 * 1000.0) as u32);
1361                    if checked.insert(key) {
1362                        results.push(SplineSplineIntersection {
1363                            t_a: ta2, t_b: tb2,
1364                            point_a: pos_a(ta2), point_b: pos_b(tb2),
1365                            distance: dist,
1366                        });
1367                    }
1368                }
1369            }
1370        }
1371    }
1372    results
1373}
1374
1375// ============================================================
1376// RAIL SYSTEM
1377// ============================================================
1378
1379#[derive(Clone, Debug)]
1380pub struct RailTrack {
1381    pub id: u64,
1382    pub spline: CatmullRomSpline,
1383    pub gauge: f32,              // distance between rails in meters
1384    pub max_speed: f32,          // km/h
1385    pub super_elevation_max: f32, // radians, typically ~0.15 rad
1386    pub cant_deficiency: f32,    // excess cant in mm
1387    pub name: String,
1388}
1389
1390impl RailTrack {
1391    pub fn new(spline: CatmullRomSpline, gauge: f32) -> Self {
1392        RailTrack {
1393            id: rand_id(),
1394            spline,
1395            gauge,
1396            max_speed: 120.0,
1397            super_elevation_max: 0.15,
1398            cant_deficiency: 75.0,
1399            name: String::from("Track"),
1400        }
1401    }
1402
1403    /// Banking angle from curvature: θ = atan(v²κ/g) where κ = curvature
1404    pub fn banking_angle_at(&self, t: f32, speed_ms: f32) -> f32 {
1405        let kappa = self.spline.curvature_at(t);
1406        let g = 9.81_f32;
1407        let centripetal = speed_ms * speed_ms * kappa;
1408        (centripetal / g).atan()
1409    }
1410
1411    /// Superelevation (cant) in mm: e = v²·g/(R·g) * gauge
1412    /// where R = 1/κ
1413    pub fn superelevation_at(&self, t: f32, speed_ms: f32) -> f32 {
1414        let kappa = self.spline.curvature_at(t);
1415        if kappa < EPSILON { return 0.0; }
1416        let r = 1.0 / kappa;
1417        let g = 9.81_f32;
1418        let cant = (speed_ms * speed_ms / (r * g)) * self.gauge * 1000.0; // in mm
1419        cant.min(self.super_elevation_max * 1000.0)
1420    }
1421
1422    /// Left and right rail positions at parameter t
1423    pub fn rail_positions(&self, t: f32, speed_ms: f32) -> (Vec3, Vec3) {
1424        let frame = self.spline.frenet_frame_at(t);
1425        let bank = self.banking_angle_at(t, speed_ms);
1426        let half_gauge = self.gauge * 0.5;
1427        let bank_rot = Quat::from_axis_angle(frame.tangent, bank);
1428        let lateral = bank_rot * frame.normal;
1429        let left  = frame.position + lateral * half_gauge;
1430        let right = frame.position - lateral * half_gauge;
1431        (left, right)
1432    }
1433
1434    pub fn rail_mesh_data(&self, resolution: usize, speed_ms: f32) -> RailMeshData {
1435        let mut left_pts  = Vec::with_capacity(resolution + 1);
1436        let mut right_pts = Vec::with_capacity(resolution + 1);
1437        for i in 0..=resolution {
1438            let t = i as f32 / resolution as f32;
1439            let (l, r) = self.rail_positions(t, speed_ms);
1440            left_pts.push(l);
1441            right_pts.push(r);
1442        }
1443        RailMeshData { left_rail: left_pts, right_rail: right_pts, sleepers: Vec::new() }
1444    }
1445
1446    pub fn add_sleepers(&self, rail_data: &mut RailMeshData, spacing: f32) {
1447        let total = self.spline.total_arc_length();
1448        let mut s = 0.0_f32;
1449        while s < total {
1450            let t = self.spline.t_at_arc_length(s);
1451            let (l, r) = self.rail_positions(t, 0.0);
1452            rail_data.sleepers.push(Sleeper { left: l, right: r, t });
1453            s += spacing;
1454        }
1455    }
1456}
1457
1458#[derive(Clone, Debug)]
1459pub struct Sleeper {
1460    pub left: Vec3,
1461    pub right: Vec3,
1462    pub t: f32,
1463}
1464
1465#[derive(Clone, Debug)]
1466pub struct RailMeshData {
1467    pub left_rail:  Vec<Vec3>,
1468    pub right_rail: Vec<Vec3>,
1469    pub sleepers:   Vec<Sleeper>,
1470}
1471
1472// ============================================================
1473// CAMERA RAIL
1474// ============================================================
1475
1476#[derive(Clone, Debug)]
1477pub struct SpeedProfile {
1478    pub keyframes: Vec<(f32, f32)>, // (t, speed) pairs
1479}
1480
1481impl SpeedProfile {
1482    pub fn constant(speed: f32) -> Self {
1483        SpeedProfile { keyframes: vec![(0.0, speed), (1.0, speed)] }
1484    }
1485
1486    pub fn ease_in_out(start_speed: f32, cruise_speed: f32, end_speed: f32) -> Self {
1487        SpeedProfile {
1488            keyframes: vec![
1489                (0.0, start_speed),
1490                (0.2, cruise_speed),
1491                (0.8, cruise_speed),
1492                (1.0, end_speed),
1493            ]
1494        }
1495    }
1496
1497    pub fn evaluate(&self, t: f32) -> f32 {
1498        if self.keyframes.is_empty() { return 0.0; }
1499        if self.keyframes.len() == 1 { return self.keyframes[0].1; }
1500        let t = clamp01(t);
1501        let idx = self.keyframes.partition_point(|kf| kf.0 <= t);
1502        if idx == 0 { return self.keyframes[0].1; }
1503        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().1; }
1504        let (t0, v0) = self.keyframes[idx - 1];
1505        let (t1, v1) = self.keyframes[idx];
1506        let frac = if (t1 - t0).abs() < EPSILON { 0.0 } else { (t - t0) / (t1 - t0) };
1507        lerp(v0, v1, quintic_ease(frac))
1508    }
1509
1510    /// Compute the arc-length parameter corresponding to time elapsed
1511    pub fn time_to_t(&self, total_length: f32, time: f32, dt: f32) -> f32 {
1512        let mut t = 0.0_f32;
1513        let mut elapsed = 0.0_f32;
1514        while elapsed < time && t < 1.0 {
1515            let speed = self.evaluate(t);
1516            let ds = speed * dt;
1517            // advance arc-length
1518            elapsed += dt;
1519            t += ds / total_length.max(EPSILON);
1520            t = t.min(1.0);
1521        }
1522        t
1523    }
1524}
1525
1526#[derive(Clone, Debug)]
1527pub struct CameraRail {
1528    pub spline: CatmullRomSpline,
1529    pub speed_profile: SpeedProfile,
1530    pub look_ahead_distance: f32,  // meters ahead to look at
1531    pub roll_correction: bool,
1532    pub fov_profile: SpeedProfile, // repurpose SpeedProfile for FOV over t
1533    pub up_axis: Vec3,
1534}
1535
1536impl CameraRail {
1537    pub fn new(spline: CatmullRomSpline) -> Self {
1538        CameraRail {
1539            spline,
1540            speed_profile: SpeedProfile::ease_in_out(0.0, 10.0, 0.0),
1541            look_ahead_distance: 5.0,
1542            roll_correction: true,
1543            fov_profile: SpeedProfile::constant(60.0),
1544            up_axis: Vec3::Y,
1545        }
1546    }
1547
1548    pub fn camera_transform_at(&self, t: f32) -> Mat4 {
1549        let pos = self.spline.evaluate(t);
1550        let total = self.spline.total_arc_length();
1551        let s_current = t * total;
1552        let s_ahead = (s_current + self.look_ahead_distance).min(total);
1553        let t_ahead = self.spline.t_at_arc_length(s_ahead);
1554        let target = self.spline.evaluate(t_ahead);
1555        let forward = safe_normalize(target - pos);
1556        let right   = safe_normalize(forward.cross(self.up_axis));
1557        let up      = if self.roll_correction {
1558            safe_normalize(right.cross(forward))
1559        } else {
1560            self.up_axis
1561        };
1562        Mat4::look_at_rh(pos, target, up).inverse()
1563    }
1564
1565    pub fn fov_at(&self, t: f32) -> f32 {
1566        self.fov_profile.evaluate(t)
1567    }
1568
1569    /// Compute camera path as sequence of (transform, fov) pairs
1570    pub fn bake_camera_path(&self, steps: usize) -> Vec<(Mat4, f32)> {
1571        (0..=steps).map(|i| {
1572            let t = i as f32 / steps as f32;
1573            (self.camera_transform_at(t), self.fov_at(t))
1574        }).collect()
1575    }
1576}
1577
1578// ============================================================
1579// SPLINE MESH GENERATION (sweep cross-section along spline)
1580// ============================================================
1581
1582#[derive(Clone, Debug)]
1583pub struct CrossSection {
1584    /// 2D points in local space (Y up, X right)
1585    pub points: Vec<Vec2>,
1586    pub closed: bool,
1587}
1588
1589impl CrossSection {
1590    pub fn circle(radius: f32, segments: usize) -> Self {
1591        let pts = (0..segments).map(|i| {
1592            let angle = i as f32 / segments as f32 * std::f32::consts::TAU;
1593            Vec2::new(angle.cos() * radius, angle.sin() * radius)
1594        }).collect();
1595        CrossSection { points: pts, closed: true }
1596    }
1597
1598    pub fn rectangle(width: f32, height: f32) -> Self {
1599        let hw = width * 0.5;
1600        let hh = height * 0.5;
1601        CrossSection {
1602            points: vec![
1603                Vec2::new(-hw, -hh),
1604                Vec2::new( hw, -hh),
1605                Vec2::new( hw,  hh),
1606                Vec2::new(-hw,  hh),
1607            ],
1608            closed: true,
1609        }
1610    }
1611
1612    pub fn i_beam(width: f32, height: f32, flange: f32, web: f32) -> Self {
1613        let hw = width * 0.5;
1614        let hh = height * 0.5;
1615        let hw_web = web * 0.5;
1616        CrossSection {
1617            points: vec![
1618                Vec2::new(-hw, -hh),
1619                Vec2::new( hw, -hh),
1620                Vec2::new( hw, -hh + flange),
1621                Vec2::new( hw_web, -hh + flange),
1622                Vec2::new( hw_web,  hh - flange),
1623                Vec2::new( hw,  hh - flange),
1624                Vec2::new( hw,  hh),
1625                Vec2::new(-hw,  hh),
1626                Vec2::new(-hw,  hh - flange),
1627                Vec2::new(-hw_web,  hh - flange),
1628                Vec2::new(-hw_web, -hh + flange),
1629                Vec2::new(-hw,  -hh + flange),
1630            ],
1631            closed: true,
1632        }
1633    }
1634}
1635
1636#[derive(Clone, Debug)]
1637pub struct SplineMesh {
1638    pub vertices:  Vec<Vec3>,
1639    pub normals:   Vec<Vec3>,
1640    pub uvs:       Vec<Vec2>,
1641    pub indices:   Vec<u32>,
1642    pub tangents:  Vec<Vec3>,
1643}
1644
1645impl SplineMesh {
1646    pub fn new() -> Self {
1647        SplineMesh {
1648            vertices: Vec::new(),
1649            normals:  Vec::new(),
1650            uvs:      Vec::new(),
1651            indices:  Vec::new(),
1652            tangents: Vec::new(),
1653        }
1654    }
1655
1656    pub fn vertex_count(&self) -> usize { self.vertices.len() }
1657    pub fn triangle_count(&self) -> usize { self.indices.len() / 3 }
1658
1659    pub fn generate_from_spline(
1660        spline_pos: &dyn Fn(f32) -> Vec3,
1661        spline_tangent: &dyn Fn(f32) -> Vec3,
1662        section: &CrossSection,
1663        spline_steps: usize,
1664        total_arc_length: f32,
1665    ) -> SplineMesh {
1666        let mut mesh = SplineMesh::new();
1667        let n_section = section.points.len();
1668        if n_section == 0 || spline_steps == 0 { return mesh; }
1669
1670        // Build parallel transport frames along the spline
1671        let mut frames: Vec<ParallelTransportFrame> = Vec::with_capacity(spline_steps + 1);
1672        {
1673            let p0 = spline_pos(0.0);
1674            let t0 = spline_tangent(0.0);
1675            frames.push(ParallelTransportFrame::initial(p0, t0));
1676        }
1677        for i in 1..=spline_steps {
1678            let t = i as f32 / spline_steps as f32;
1679            let p = spline_pos(t);
1680            let tang = safe_normalize(spline_tangent(t));
1681            let prev = frames.last().unwrap().clone();
1682            frames.push(ParallelTransportFrame::transport(&prev, p, tang));
1683        }
1684
1685        // Build vertex rings
1686        let mut arc_s = 0.0_f32;
1687        let mut prev_pos = spline_pos(0.0);
1688        for (ring_idx, frame) in frames.iter().enumerate() {
1689            let t = ring_idx as f32 / spline_steps as f32;
1690            if ring_idx > 0 {
1691                let cur_pos = spline_pos(t);
1692                arc_s += (cur_pos - prev_pos).length();
1693                prev_pos = cur_pos;
1694            }
1695            let u_coord = arc_s / total_arc_length.max(EPSILON);
1696            for (j, &sec_pt) in section.points.iter().enumerate() {
1697                let v_coord = j as f32 / n_section as f32;
1698                let world = frame.position
1699                    + frame.normal   * sec_pt.x
1700                    + frame.binormal * sec_pt.y;
1701                let normal_2d = sec_pt.normalize_or_zero();
1702                let world_normal = safe_normalize(
1703                    frame.normal   * normal_2d.x +
1704                    frame.binormal * normal_2d.y
1705                );
1706                mesh.vertices.push(world);
1707                mesh.normals.push(world_normal);
1708                mesh.uvs.push(Vec2::new(u_coord, v_coord));
1709                mesh.tangents.push(frame.tangent);
1710            }
1711        }
1712
1713        // Build indices
1714        let rings = spline_steps + 1;
1715        for r in 0..rings - 1 {
1716            for j in 0..n_section {
1717                let j_next = (j + 1) % n_section;
1718                let a = (r * n_section + j) as u32;
1719                let b = (r * n_section + j_next) as u32;
1720                let c = ((r + 1) * n_section + j) as u32;
1721                let d = ((r + 1) * n_section + j_next) as u32;
1722                mesh.indices.push(a);
1723                mesh.indices.push(b);
1724                mesh.indices.push(c);
1725                mesh.indices.push(b);
1726                mesh.indices.push(d);
1727                mesh.indices.push(c);
1728            }
1729        }
1730
1731        mesh
1732    }
1733
1734    /// LOD by curvature: denser sampling where curvature is high
1735    pub fn generate_lod(
1736        spline_pos: &dyn Fn(f32) -> Vec3,
1737        spline_tangent: &dyn Fn(f32) -> Vec3,
1738        spline_curvature: &dyn Fn(f32) -> f32,
1739        section: &CrossSection,
1740        min_steps: usize,
1741        max_steps: usize,
1742        total_arc_length: f32,
1743    ) -> SplineMesh {
1744        // Build adaptive t samples based on curvature
1745        let mut t_samples = vec![0.0_f32];
1746        let coarse = min_steps * 4;
1747        for i in 1..coarse {
1748            let t = i as f32 / coarse as f32;
1749            let kappa = spline_curvature(t);
1750            let step_factor = (1.0 + kappa * 10.0).recip();
1751            let prev = *t_samples.last().unwrap();
1752            let step = (1.0 / min_steps as f32) * step_factor.max(1.0 / max_steps as f32);
1753            if t - prev >= step { t_samples.push(t); }
1754        }
1755        t_samples.push(1.0);
1756        let spline_steps = t_samples.len() - 1;
1757
1758        let mut mesh = SplineMesh::new();
1759        let n_section = section.points.len();
1760        if n_section == 0 { return mesh; }
1761
1762        let mut frames: Vec<ParallelTransportFrame> = Vec::new();
1763        {
1764            let p0 = spline_pos(0.0);
1765            let t0 = spline_tangent(0.0);
1766            frames.push(ParallelTransportFrame::initial(p0, t0));
1767        }
1768        for i in 1..t_samples.len() {
1769            let t = t_samples[i];
1770            let p = spline_pos(t);
1771            let tang = safe_normalize(spline_tangent(t));
1772            let prev = frames.last().unwrap().clone();
1773            frames.push(ParallelTransportFrame::transport(&prev, p, tang));
1774        }
1775
1776        let mut arc_s = 0.0_f32;
1777        let mut prev_pos = spline_pos(0.0);
1778        for (ring_idx, frame) in frames.iter().enumerate() {
1779            let t = t_samples[ring_idx];
1780            if ring_idx > 0 {
1781                let cur_pos = spline_pos(t);
1782                arc_s += (cur_pos - prev_pos).length();
1783                prev_pos = cur_pos;
1784            }
1785            let u_coord = arc_s / total_arc_length.max(EPSILON);
1786            for (j, &sec_pt) in section.points.iter().enumerate() {
1787                let v_coord = j as f32 / n_section as f32;
1788                let world = frame.position
1789                    + frame.normal   * sec_pt.x
1790                    + frame.binormal * sec_pt.y;
1791                let normal_2d = sec_pt.normalize_or_zero();
1792                let world_normal = safe_normalize(
1793                    frame.normal   * normal_2d.x +
1794                    frame.binormal * normal_2d.y
1795                );
1796                mesh.vertices.push(world);
1797                mesh.normals.push(world_normal);
1798                mesh.uvs.push(Vec2::new(u_coord, v_coord));
1799                mesh.tangents.push(frame.tangent);
1800            }
1801        }
1802
1803        let rings = frames.len();
1804        for r in 0..rings.saturating_sub(1) {
1805            for j in 0..n_section {
1806                let j_next = (j + 1) % n_section;
1807                let a = (r * n_section + j) as u32;
1808                let b = (r * n_section + j_next) as u32;
1809                let c = ((r + 1) * n_section + j) as u32;
1810                let d = ((r + 1) * n_section + j_next) as u32;
1811                mesh.indices.extend_from_slice(&[a, b, c, b, d, c]);
1812            }
1813        }
1814
1815        mesh
1816    }
1817}
1818
1819// ============================================================
1820// PATH NETWORK (Directed Graph + Dijkstra)
1821// ============================================================
1822
1823#[derive(Clone, Debug)]
1824pub struct SplineNode {
1825    pub id: u64,
1826    pub position: Vec3,
1827    pub connected_splines: Vec<u64>, // spline IDs
1828}
1829
1830#[derive(Clone, Debug)]
1831pub struct SplineEdge {
1832    pub id: u64,
1833    pub from_node: u64,
1834    pub to_node: u64,
1835    pub spline_id: u64,
1836    pub weight: f32, // arc length or custom cost
1837    pub one_way: bool,
1838}
1839
1840#[derive(Clone, Debug)]
1841pub struct PathNetwork {
1842    pub nodes: HashMap<u64, SplineNode>,
1843    pub edges: HashMap<u64, SplineEdge>,
1844    pub splines: HashMap<u64, CatmullRomSpline>,
1845    // adjacency list: node_id -> [(edge_id, neighbor_node_id)]
1846    adjacency: HashMap<u64, Vec<(u64, u64)>>,
1847}
1848
1849impl PathNetwork {
1850    pub fn new() -> Self {
1851        PathNetwork {
1852            nodes: HashMap::new(),
1853            edges: HashMap::new(),
1854            splines: HashMap::new(),
1855            adjacency: HashMap::new(),
1856        }
1857    }
1858
1859    pub fn add_node(&mut self, position: Vec3) -> u64 {
1860        let id = rand_id();
1861        self.nodes.insert(id, SplineNode {
1862            id, position, connected_splines: Vec::new(),
1863        });
1864        self.adjacency.insert(id, Vec::new());
1865        id
1866    }
1867
1868    pub fn add_spline(&mut self, spline: CatmullRomSpline) -> u64 {
1869        let id = rand_id();
1870        self.splines.insert(id, spline);
1871        id
1872    }
1873
1874    pub fn connect_nodes(&mut self, from: u64, to: u64, spline_id: u64, one_way: bool) {
1875        let weight = self.splines.get(&spline_id)
1876            .map(|s| s.total_arc_length())
1877            .unwrap_or(1.0);
1878        let edge_id = rand_id();
1879        let edge = SplineEdge { id: edge_id, from_node: from, to_node: to, spline_id, weight, one_way };
1880        self.edges.insert(edge_id, edge.clone());
1881        self.adjacency.entry(from).or_default().push((edge_id, to));
1882        if !one_way {
1883            let rev_edge_id = rand_id();
1884            let rev_edge = SplineEdge { id: rev_edge_id, from_node: to, to_node: from, spline_id, weight, one_way: false };
1885            self.edges.insert(rev_edge_id, rev_edge);
1886            self.adjacency.entry(to).or_default().push((rev_edge_id, from));
1887        }
1888    }
1889
1890    /// Dijkstra shortest path from start to goal
1891    pub fn dijkstra(&self, start: u64, goal: u64) -> Option<Vec<u64>> {
1892        use std::collections::BinaryHeap;
1893        use std::cmp::Reverse;
1894
1895        // dist: node_id -> (cost, prev_node_id)
1896        let mut dist: HashMap<u64, f32> = HashMap::new();
1897        let mut prev: HashMap<u64, u64> = HashMap::new();
1898        let mut heap: BinaryHeap<Reverse<(u32, u64)>> = BinaryHeap::new();
1899
1900        dist.insert(start, 0.0);
1901        heap.push(Reverse((0, start)));
1902
1903        while let Some(Reverse((cost_bits, node))) = heap.pop() {
1904            let cost = f32::from_bits(cost_bits);
1905            if node == goal {
1906                // Reconstruct path
1907                let mut path = vec![goal];
1908                let mut cur = goal;
1909                while let Some(&p) = prev.get(&cur) {
1910                    path.push(p);
1911                    cur = p;
1912                    if cur == start { break; }
1913                }
1914                path.reverse();
1915                return Some(path);
1916            }
1917            let best = dist.get(&node).cloned().unwrap_or(f32::MAX);
1918            if cost > best + EPSILON { continue; }
1919            if let Some(neighbors) = self.adjacency.get(&node) {
1920                for &(edge_id, neighbor) in neighbors {
1921                    if let Some(edge) = self.edges.get(&edge_id) {
1922                        let new_cost = cost + edge.weight;
1923                        let cur_best = dist.get(&neighbor).cloned().unwrap_or(f32::MAX);
1924                        if new_cost < cur_best {
1925                            dist.insert(neighbor, new_cost);
1926                            prev.insert(neighbor, node);
1927                            heap.push(Reverse((new_cost.to_bits(), neighbor)));
1928                        }
1929                    }
1930                }
1931            }
1932        }
1933        None
1934    }
1935
1936    /// A* path planning with Euclidean heuristic
1937    pub fn astar(&self, start: u64, goal: u64) -> Option<Vec<u64>> {
1938        use std::collections::BinaryHeap;
1939        use std::cmp::Reverse;
1940
1941        let goal_pos = self.nodes.get(&goal)?.position;
1942        let heuristic = |node_id: u64| -> f32 {
1943            self.nodes.get(&node_id)
1944                .map(|n| (n.position - goal_pos).length())
1945                .unwrap_or(0.0)
1946        };
1947
1948        let mut g_score: HashMap<u64, f32> = HashMap::new();
1949        let mut prev: HashMap<u64, u64> = HashMap::new();
1950        let mut open: BinaryHeap<Reverse<(u32, u64)>> = BinaryHeap::new();
1951
1952        g_score.insert(start, 0.0);
1953        let f0 = heuristic(start);
1954        open.push(Reverse((f0.to_bits(), start)));
1955
1956        while let Some(Reverse((_, node))) = open.pop() {
1957            if node == goal {
1958                let mut path = vec![goal];
1959                let mut cur = goal;
1960                while let Some(&p) = prev.get(&cur) {
1961                    path.push(p);
1962                    cur = p;
1963                    if cur == start { break; }
1964                }
1965                path.reverse();
1966                return Some(path);
1967            }
1968            let g = g_score.get(&node).cloned().unwrap_or(f32::MAX);
1969            if let Some(neighbors) = self.adjacency.get(&node) {
1970                for &(edge_id, neighbor) in neighbors {
1971                    if let Some(edge) = self.edges.get(&edge_id) {
1972                        let new_g = g + edge.weight;
1973                        let cur_g = g_score.get(&neighbor).cloned().unwrap_or(f32::MAX);
1974                        if new_g < cur_g {
1975                            g_score.insert(neighbor, new_g);
1976                            prev.insert(neighbor, node);
1977                            let f = new_g + heuristic(neighbor);
1978                            open.push(Reverse((f.to_bits(), neighbor)));
1979                        }
1980                    }
1981                }
1982            }
1983        }
1984        None
1985    }
1986
1987    pub fn nearest_node(&self, pos: Vec3) -> Option<u64> {
1988        self.nodes.values()
1989            .min_by(|a, b| {
1990                let da = (a.position - pos).length_squared();
1991                let db = (b.position - pos).length_squared();
1992                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1993            })
1994            .map(|n| n.id)
1995    }
1996}
1997
1998// ============================================================
1999// TRAFFIC SYSTEM PARAMETERS
2000// ============================================================
2001
2002#[derive(Clone, Debug)]
2003pub struct TrafficAgent {
2004    pub id: u64,
2005    pub current_spline_id: u64,
2006    pub t: f32,
2007    pub speed: f32,
2008    pub max_speed: f32,
2009    pub path: Vec<u64>, // sequence of node IDs
2010    pub path_index: usize,
2011    pub braking_distance: f32,
2012    pub acceleration: f32,
2013}
2014
2015impl TrafficAgent {
2016    pub fn new(spline_id: u64, max_speed: f32) -> Self {
2017        TrafficAgent {
2018            id: rand_id(),
2019            current_spline_id: spline_id,
2020            t: 0.0,
2021            speed: 0.0,
2022            max_speed,
2023            path: Vec::new(),
2024            path_index: 0,
2025            braking_distance: 20.0,
2026            acceleration: 2.0,
2027        }
2028    }
2029
2030    pub fn update(&mut self, dt: f32, spline: &CatmullRomSpline) {
2031        // Simple kinematic update
2032        let target_speed = self.max_speed;
2033        if self.speed < target_speed {
2034            self.speed = (self.speed + self.acceleration * dt).min(target_speed);
2035        }
2036        let total_length = spline.total_arc_length();
2037        if total_length < EPSILON { return; }
2038        let ds = self.speed * dt;
2039        let current_s = self.t * total_length;
2040        let new_s = (current_s + ds).min(total_length);
2041        self.t = new_s / total_length;
2042    }
2043
2044    pub fn position(&self, spline: &CatmullRomSpline) -> Vec3 {
2045        spline.evaluate(self.t)
2046    }
2047}
2048
2049#[derive(Clone, Debug)]
2050pub struct TrafficSystem {
2051    pub agents: Vec<TrafficAgent>,
2052    pub network: PathNetwork,
2053    pub spawn_rate: f32,
2054    pub max_agents: usize,
2055}
2056
2057impl TrafficSystem {
2058    pub fn new(network: PathNetwork) -> Self {
2059        TrafficSystem {
2060            agents: Vec::new(),
2061            network,
2062            spawn_rate: 0.1,
2063            max_agents: 64,
2064        }
2065    }
2066
2067    pub fn spawn_agent(&mut self, spline_id: u64) {
2068        if self.agents.len() >= self.max_agents { return; }
2069        let agent = TrafficAgent::new(spline_id, 10.0 + (self.agents.len() as f32 % 5.0) * 2.0);
2070        self.agents.push(agent);
2071    }
2072
2073    pub fn update(&mut self, dt: f32) {
2074        for agent in &mut self.agents {
2075            if let Some(spline) = self.network.splines.get(&agent.current_spline_id) {
2076                // Clone to avoid borrow conflict
2077                let spline_clone = spline.clone();
2078                agent.update(dt, &spline_clone);
2079            }
2080        }
2081        // Remove agents that have reached end
2082        self.agents.retain(|a| a.t < 1.0);
2083    }
2084
2085    pub fn agent_separation_force(&self, agent_idx: usize) -> Vec3 {
2086        let agent = &self.agents[agent_idx];
2087        let spline = match self.network.splines.get(&agent.current_spline_id) {
2088            Some(s) => s,
2089            None => return Vec3::ZERO,
2090        };
2091        let my_pos = spline.evaluate(agent.t);
2092        let mut force = Vec3::ZERO;
2093        for (i, other) in self.agents.iter().enumerate() {
2094            if i == agent_idx { continue; }
2095            if other.current_spline_id != agent.current_spline_id { continue; }
2096            let other_pos = spline.evaluate(other.t);
2097            let diff = my_pos - other_pos;
2098            let dist = diff.length();
2099            if dist < 5.0 && dist > EPSILON {
2100                force += diff / (dist * dist);
2101            }
2102        }
2103        force
2104    }
2105}
2106
2107// ============================================================
2108// SPLINE PHYSICS
2109// ============================================================
2110
2111#[derive(Clone, Debug)]
2112pub struct SplineConstrainedObject {
2113    pub id: u64,
2114    pub spline_id: u64,
2115    pub t: f32,
2116    pub speed: f32,            // m/s along spline
2117    pub mass: f32,
2118    pub gravity: Vec3,
2119    pub friction: f32,         // coefficient
2120    pub normal_force: f32,     // computed per frame
2121}
2122
2123impl SplineConstrainedObject {
2124    pub fn new(spline_id: u64, t: f32, mass: f32) -> Self {
2125        SplineConstrainedObject {
2126            id: rand_id(),
2127            spline_id,
2128            t,
2129            speed: 0.0,
2130            mass,
2131            gravity: Vec3::new(0.0, -9.81, 0.0),
2132            friction: 0.1,
2133            normal_force: 0.0,
2134        }
2135    }
2136
2137    pub fn update(&mut self, dt: f32, spline: &CatmullRomSpline) {
2138        let frame = spline.frenet_frame_at(self.t);
2139        // Gravitational component along tangent
2140        let g_tangent = self.gravity.dot(frame.tangent);
2141        // Normal force = m * (g_normal + centripetal)
2142        let g_normal  = self.gravity.dot(frame.normal);
2143        let centripetal = self.speed * self.speed * frame.curvature;
2144        self.normal_force = self.mass * (g_normal + centripetal).abs();
2145        // Friction force (opposes motion)
2146        let friction_force = -self.speed.signum() * self.friction * self.normal_force;
2147        // Net tangential force
2148        let net_tangential = self.mass * g_tangent + friction_force;
2149        let tangential_accel = net_tangential / self.mass;
2150        self.speed += tangential_accel * dt;
2151        // Advance along spline
2152        let total_length = spline.total_arc_length();
2153        if total_length > EPSILON {
2154            let ds = self.speed * dt;
2155            let current_s = self.t * total_length;
2156            let new_s = (current_s + ds).clamp(0.0, total_length);
2157            self.t = new_s / total_length;
2158        }
2159    }
2160
2161    pub fn position(&self, spline: &CatmullRomSpline) -> Vec3 {
2162        spline.evaluate(self.t)
2163    }
2164
2165    pub fn centripetal_acceleration(&self, spline: &CatmullRomSpline) -> Vec3 {
2166        let frame = spline.frenet_frame_at(self.t);
2167        frame.normal * (self.speed * self.speed * frame.curvature)
2168    }
2169}
2170
2171// ============================================================
2172// CHAIN LINKS ALONG SPLINE
2173// ============================================================
2174
2175#[derive(Clone, Debug)]
2176pub struct ChainLink {
2177    pub t: f32,
2178    pub angle_twist: f32, // twist around tangent
2179    pub size: f32,
2180}
2181
2182#[derive(Clone, Debug)]
2183pub struct SplineChain {
2184    pub spline_id: u64,
2185    pub links: Vec<ChainLink>,
2186    pub link_length: f32,
2187    pub link_width: f32,
2188    pub link_height: f32,
2189    pub offset: f32, // animation offset in [0,1]
2190}
2191
2192impl SplineChain {
2193    pub fn new(spline: &CatmullRomSpline, spline_id: u64, link_length: f32) -> Self {
2194        let total = spline.total_arc_length();
2195        let n_links = (total / link_length.max(EPSILON)) as usize;
2196        let links = (0..n_links).map(|i| {
2197            let s = i as f32 * link_length;
2198            let t = spline.t_at_arc_length(s);
2199            ChainLink {
2200                t,
2201                angle_twist: if i % 2 == 0 { 0.0 } else { std::f32::consts::FRAC_PI_2 },
2202                size: link_length,
2203            }
2204        }).collect();
2205        SplineChain {
2206            spline_id,
2207            links,
2208            link_length,
2209            link_width:  link_length * 0.6,
2210            link_height: link_length * 0.15,
2211            offset: 0.0,
2212        }
2213    }
2214
2215    pub fn update_offset(&mut self, delta: f32) {
2216        self.offset = (self.offset + delta).fract();
2217    }
2218
2219    pub fn link_transform(&self, link_idx: usize, spline: &CatmullRomSpline) -> Mat4 {
2220        let link = &self.links[link_idx];
2221        let frame = spline.frenet_frame_at(link.t);
2222        let twist = Quat::from_axis_angle(frame.tangent, link.angle_twist);
2223        let normal   = twist * frame.normal;
2224        let binormal = twist * frame.binormal;
2225        Mat4::from_cols(
2226            Vec4::new(frame.tangent.x, frame.tangent.y, frame.tangent.z, 0.0),
2227            Vec4::new(normal.x, normal.y, normal.z, 0.0),
2228            Vec4::new(binormal.x, binormal.y, binormal.z, 0.0),
2229            Vec4::new(frame.position.x, frame.position.y, frame.position.z, 1.0),
2230        )
2231    }
2232}
2233
2234// ============================================================
2235// DEBUG VISUALIZATION DATA
2236// ============================================================
2237
2238#[derive(Clone, Debug)]
2239pub struct DebugLine {
2240    pub start: Vec3,
2241    pub end:   Vec3,
2242    pub color: Vec4,
2243}
2244
2245#[derive(Clone, Debug)]
2246pub struct DebugPoint {
2247    pub position: Vec3,
2248    pub color:    Vec4,
2249    pub size:     f32,
2250}
2251
2252#[derive(Clone, Debug)]
2253pub struct SplineDebugViz {
2254    pub lines:  Vec<DebugLine>,
2255    pub points: Vec<DebugPoint>,
2256    pub curvature_comb: Vec<(Vec3, Vec3)>, // base, tip
2257}
2258
2259impl SplineDebugViz {
2260    pub fn new() -> Self {
2261        SplineDebugViz {
2262            lines:  Vec::new(),
2263            points: Vec::new(),
2264            curvature_comb: Vec::new(),
2265        }
2266    }
2267
2268    pub fn clear(&mut self) {
2269        self.lines.clear();
2270        self.points.clear();
2271        self.curvature_comb.clear();
2272    }
2273
2274    pub fn draw_frenet_frames(
2275        &mut self,
2276        pos_fn:  &dyn Fn(f32) -> Vec3,
2277        d1_fn:   &dyn Fn(f32) -> Vec3,
2278        d2_fn:   &dyn Fn(f32) -> Vec3,
2279        d3_fn:   &dyn Fn(f32) -> Vec3,
2280        steps:   usize,
2281        scale:   f32,
2282    ) {
2283        for i in 0..=steps {
2284            let t = i as f32 / steps as f32;
2285            let pos = pos_fn(t);
2286            let d1  = d1_fn(t);
2287            let d2  = d2_fn(t);
2288            let d3  = d3_fn(t);
2289            let frame = FrenetFrame::compute(pos, d1, d2, d3);
2290            self.lines.push(DebugLine {
2291                start: pos,
2292                end:   pos + frame.tangent  * scale,
2293                color: Vec4::new(1.0, 0.0, 0.0, 1.0), // red = tangent
2294            });
2295            self.lines.push(DebugLine {
2296                start: pos,
2297                end:   pos + frame.normal   * scale,
2298                color: Vec4::new(0.0, 1.0, 0.0, 1.0), // green = normal
2299            });
2300            self.lines.push(DebugLine {
2301                start: pos,
2302                end:   pos + frame.binormal * scale,
2303                color: Vec4::new(0.0, 0.0, 1.0, 1.0), // blue = binormal
2304            });
2305        }
2306    }
2307
2308    pub fn draw_curvature_comb(
2309        &mut self,
2310        pos_fn:      &dyn Fn(f32) -> Vec3,
2311        curvature_fn: &dyn Fn(f32) -> f32,
2312        normal_fn:   &dyn Fn(f32) -> Vec3,
2313        steps:       usize,
2314        scale:       f32,
2315    ) {
2316        for i in 0..=steps {
2317            let t = i as f32 / steps as f32;
2318            let base = pos_fn(t);
2319            let kappa = curvature_fn(t);
2320            let normal = normal_fn(t);
2321            let tip = base + normal * kappa * scale;
2322            self.curvature_comb.push((base, tip));
2323            self.lines.push(DebugLine {
2324                start: base,
2325                end:   tip,
2326                color: Vec4::new(1.0, 1.0, 0.0, 0.8),
2327            });
2328        }
2329    }
2330
2331    pub fn draw_arc_length_marks(
2332        &mut self,
2333        pos_fn:        &dyn Fn(f32) -> Vec3,
2334        t_at_length_fn: &dyn Fn(f32) -> f32,
2335        total_length:  f32,
2336        interval:      f32,
2337        up:            Vec3,
2338        size:          f32,
2339    ) {
2340        let mut s = 0.0_f32;
2341        while s <= total_length {
2342            let t = t_at_length_fn(s);
2343            let pos = pos_fn(t);
2344            self.points.push(DebugPoint {
2345                position: pos,
2346                color:    Vec4::new(1.0, 0.5, 0.0, 1.0),
2347                size,
2348            });
2349            self.lines.push(DebugLine {
2350                start: pos - up * size,
2351                end:   pos + up * size,
2352                color: Vec4::new(1.0, 0.5, 0.0, 1.0),
2353            });
2354            s += interval;
2355        }
2356    }
2357
2358    pub fn draw_bounding_box(&mut self, min: Vec3, max: Vec3, color: Vec4) {
2359        let corners = [
2360            Vec3::new(min.x, min.y, min.z),
2361            Vec3::new(max.x, min.y, min.z),
2362            Vec3::new(max.x, max.y, min.z),
2363            Vec3::new(min.x, max.y, min.z),
2364            Vec3::new(min.x, min.y, max.z),
2365            Vec3::new(max.x, min.y, max.z),
2366            Vec3::new(max.x, max.y, max.z),
2367            Vec3::new(min.x, max.y, max.z),
2368        ];
2369        let edges = [
2370            (0,1),(1,2),(2,3),(3,0), // bottom
2371            (4,5),(5,6),(6,7),(7,4), // top
2372            (0,4),(1,5),(2,6),(3,7), // sides
2373        ];
2374        for (a, b) in edges {
2375            self.lines.push(DebugLine { start: corners[a], end: corners[b], color });
2376        }
2377    }
2378
2379    pub fn draw_spline_curve(
2380        &mut self,
2381        pos_fn: &dyn Fn(f32) -> Vec3,
2382        steps: usize,
2383        color: Vec4,
2384    ) {
2385        let mut prev = pos_fn(0.0);
2386        for i in 1..=steps {
2387            let t = i as f32 / steps as f32;
2388            let cur = pos_fn(t);
2389            self.lines.push(DebugLine { start: prev, end: cur, color });
2390            prev = cur;
2391        }
2392    }
2393
2394    pub fn draw_control_polygon(&mut self, points: &[Vec3], color: Vec4) {
2395        for i in 0..points.len().saturating_sub(1) {
2396            self.lines.push(DebugLine {
2397                start: points[i],
2398                end:   points[i + 1],
2399                color,
2400            });
2401        }
2402        for &p in points {
2403            self.points.push(DebugPoint {
2404                position: p,
2405                color,
2406                size: 6.0,
2407            });
2408        }
2409    }
2410}
2411
2412// ============================================================
2413// UNDO/REDO SYSTEM
2414// ============================================================
2415
2416#[derive(Clone, Debug)]
2417pub enum SplineEditorCommand {
2418    AddControlPoint  { spline_id: u64, index: usize, point: ControlPoint },
2419    RemoveControlPoint { spline_id: u64, index: usize, point: ControlPoint },
2420    MoveControlPoint { spline_id: u64, index: usize, old_pos: Vec3, new_pos: Vec3 },
2421    MoveTangent      { spline_id: u64, index: usize, which: TangentHandle, old_val: Vec3, new_val: Vec3 },
2422    InsertKnot       { spline_id: u64, t: f32 },
2423    SplitSpline      { spline_id: u64, t: f32 },
2424    JoinSplines      { spline_a: u64, spline_b: u64 },
2425    ToggleClosed     { spline_id: u64 },
2426    AddSpline        { spline_id: u64 },
2427    RemoveSpline     { spline_id: u64 },
2428    SetSplineType    { spline_id: u64, old_type: SplineType, new_type: SplineType },
2429}
2430
2431#[derive(Clone, Debug, PartialEq)]
2432pub enum TangentHandle {
2433    In,
2434    Out,
2435}
2436
2437#[derive(Debug)]
2438pub struct UndoHistory {
2439    past:   VecDeque<SplineEditorCommand>,
2440    future: VecDeque<SplineEditorCommand>,
2441    max_size: usize,
2442}
2443
2444impl UndoHistory {
2445    pub fn new(max_size: usize) -> Self {
2446        UndoHistory { past: VecDeque::new(), future: VecDeque::new(), max_size }
2447    }
2448
2449    pub fn push(&mut self, cmd: SplineEditorCommand) {
2450        self.future.clear();
2451        self.past.push_back(cmd);
2452        if self.past.len() > self.max_size {
2453            self.past.pop_front();
2454        }
2455    }
2456
2457    pub fn can_undo(&self) -> bool { !self.past.is_empty() }
2458    pub fn can_redo(&self) -> bool { !self.future.is_empty() }
2459
2460    pub fn undo(&mut self) -> Option<SplineEditorCommand> {
2461        let cmd = self.past.pop_back()?;
2462        self.future.push_back(cmd.clone());
2463        Some(cmd)
2464    }
2465
2466    pub fn redo(&mut self) -> Option<SplineEditorCommand> {
2467        let cmd = self.future.pop_back()?;
2468        self.past.push_back(cmd.clone());
2469        Some(cmd)
2470    }
2471}
2472
2473// ============================================================
2474// SELECTION STATE
2475// ============================================================
2476
2477#[derive(Clone, Debug, PartialEq)]
2478pub enum SelectionTarget {
2479    SplineId(u64),
2480    ControlPointIndex(u64, usize), // (spline_id, cp_index)
2481    TangentIn(u64, usize),
2482    TangentOut(u64, usize),
2483    NodeId(u64),
2484    EdgeId(u64),
2485}
2486
2487#[derive(Clone, Debug)]
2488pub struct SelectionState {
2489    pub selected: HashSet<u64>,        // spline IDs
2490    pub selected_cp: Vec<(u64, usize)>, // (spline_id, cp_index)
2491    pub hovered: Option<SelectionTarget>,
2492    pub active: Option<SelectionTarget>,
2493}
2494
2495impl SelectionState {
2496    pub fn new() -> Self {
2497        SelectionState {
2498            selected: HashSet::new(),
2499            selected_cp: Vec::new(),
2500            hovered: None,
2501            active: None,
2502        }
2503    }
2504
2505    pub fn clear(&mut self) {
2506        self.selected.clear();
2507        self.selected_cp.clear();
2508        self.hovered = None;
2509        self.active  = None;
2510    }
2511
2512    pub fn select_spline(&mut self, id: u64, add: bool) {
2513        if !add { self.selected.clear(); }
2514        self.selected.insert(id);
2515    }
2516
2517    pub fn select_cp(&mut self, spline_id: u64, index: usize, add: bool) {
2518        if !add { self.selected_cp.clear(); }
2519        self.selected_cp.push((spline_id, index));
2520    }
2521
2522    pub fn deselect_cp(&mut self, spline_id: u64, index: usize) {
2523        self.selected_cp.retain(|&(sid, ci)| !(sid == spline_id && ci == index));
2524    }
2525
2526    pub fn is_cp_selected(&self, spline_id: u64, index: usize) -> bool {
2527        self.selected_cp.iter().any(|&(sid, ci)| sid == spline_id && ci == index)
2528    }
2529}
2530
2531// ============================================================
2532// SERIALIZATION (simple text-based)
2533// ============================================================
2534
2535#[derive(Clone, Debug)]
2536pub struct SplineSerializedData {
2537    pub spline_id: u64,
2538    pub spline_type: SplineType,
2539    pub control_points: Vec<(Vec3, Vec3, Vec3, f32)>, // pos, t_in, t_out, weight
2540    pub closed: bool,
2541    pub name: String,
2542    pub metadata: HashMap<String, String>,
2543}
2544
2545impl SplineSerializedData {
2546    pub fn serialize_catmull_rom(spline: &CatmullRomSpline, id: u64, name: &str) -> Self {
2547        SplineSerializedData {
2548            spline_id: id,
2549            spline_type: SplineType::CatmullRom,
2550            control_points: spline.control_points.iter().map(|cp| {
2551                (cp.position, cp.tangent_in, cp.tangent_out, cp.weight)
2552            }).collect(),
2553            closed: spline.closed,
2554            name: name.to_string(),
2555            metadata: HashMap::new(),
2556        }
2557    }
2558
2559    pub fn to_bytes(&self) -> Vec<u8> {
2560        // Simple binary serialization: just f32 values packed
2561        let mut bytes = Vec::new();
2562        bytes.extend_from_slice(&self.spline_id.to_le_bytes());
2563        bytes.extend_from_slice(&(self.control_points.len() as u32).to_le_bytes());
2564        for (pos, t_in, t_out, w) in &self.control_points {
2565            for &v in &[pos.x, pos.y, pos.z, t_in.x, t_in.y, t_in.z,
2566                        t_out.x, t_out.y, t_out.z, *w] {
2567                bytes.extend_from_slice(&v.to_le_bytes());
2568            }
2569        }
2570        bytes.push(if self.closed { 1 } else { 0 });
2571        bytes
2572    }
2573
2574    pub fn from_bytes(data: &[u8]) -> Option<Self> {
2575        if data.len() < 12 { return None; }
2576        let mut cursor = 0usize;
2577        let spline_id = u64::from_le_bytes(data[cursor..cursor+8].try_into().ok()?);
2578        cursor += 8;
2579        let n = u32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?) as usize;
2580        cursor += 4;
2581        let floats_per_cp = 10usize;
2582        let mut control_points = Vec::with_capacity(n);
2583        for _ in 0..n {
2584            if cursor + floats_per_cp * 4 > data.len() { return None; }
2585            let mut vals = [0.0_f32; 10];
2586            for v in &mut vals {
2587                *v = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?);
2588                cursor += 4;
2589            }
2590            control_points.push((
2591                Vec3::new(vals[0], vals[1], vals[2]),
2592                Vec3::new(vals[3], vals[4], vals[5]),
2593                Vec3::new(vals[6], vals[7], vals[8]),
2594                vals[9],
2595            ));
2596        }
2597        let closed = if cursor < data.len() { data[cursor] != 0 } else { false };
2598        Some(SplineSerializedData {
2599            spline_id,
2600            spline_type: SplineType::CatmullRom,
2601            control_points,
2602            closed,
2603            name: String::new(),
2604            metadata: HashMap::new(),
2605        })
2606    }
2607}
2608
2609// ============================================================
2610// SPLINE EDITOR (main struct)
2611// ============================================================
2612
2613#[derive(Debug)]
2614pub struct SplineEditor {
2615    // Spline storage
2616    pub catmull_splines: HashMap<u64, CatmullRomSpline>,
2617    pub bezier_splines:  HashMap<u64, CubicBezierSpline>,
2618    pub bsplines:        HashMap<u64, BSpline>,
2619    pub nurbs_splines:   HashMap<u64, NurbsSpline>,
2620    pub hermite_splines: HashMap<u64, HermiteSpline>,
2621    pub spline_names:    HashMap<u64, String>,
2622    pub spline_types:    HashMap<u64, SplineType>,
2623
2624    // Rail system
2625    pub rail_tracks: HashMap<u64, RailTrack>,
2626    pub camera_rails: HashMap<u64, CameraRail>,
2627
2628    // Path network
2629    pub path_network: PathNetwork,
2630    pub traffic_system: Option<TrafficSystem>,
2631
2632    // Constrained objects
2633    pub constrained_objects: Vec<SplineConstrainedObject>,
2634    pub chains: Vec<SplineChain>,
2635
2636    // Editor state
2637    pub selection: SelectionState,
2638    pub undo_history: UndoHistory,
2639    pub debug_viz: SplineDebugViz,
2640
2641    // Settings
2642    pub default_alpha: f32,   // centripetal alpha for new Catmull-Rom splines
2643    pub snap_to_grid: bool,
2644    pub grid_size:    f32,
2645    pub show_debug:   bool,
2646    pub show_curvature_comb: bool,
2647    pub show_arc_length_marks: bool,
2648    pub curvature_comb_scale: f32,
2649    pub arc_length_mark_interval: f32,
2650    pub show_frenet_frames: bool,
2651    pub frenet_frame_scale: f32,
2652
2653    // Mesh generation settings
2654    pub mesh_section: CrossSection,
2655    pub mesh_resolution: usize,
2656    pub generated_meshes: HashMap<u64, SplineMesh>,
2657}
2658
2659impl SplineEditor {
2660    pub fn new() -> Self {
2661        SplineEditor {
2662            catmull_splines: HashMap::new(),
2663            bezier_splines:  HashMap::new(),
2664            bsplines:        HashMap::new(),
2665            nurbs_splines:   HashMap::new(),
2666            hermite_splines: HashMap::new(),
2667            spline_names:    HashMap::new(),
2668            spline_types:    HashMap::new(),
2669            rail_tracks:     HashMap::new(),
2670            camera_rails:    HashMap::new(),
2671            path_network:    PathNetwork::new(),
2672            traffic_system:  None,
2673            constrained_objects: Vec::new(),
2674            chains:          Vec::new(),
2675            selection:       SelectionState::new(),
2676            undo_history:    UndoHistory::new(128),
2677            debug_viz:       SplineDebugViz::new(),
2678            default_alpha:   0.5,
2679            snap_to_grid:    false,
2680            grid_size:       1.0,
2681            show_debug:      false,
2682            show_curvature_comb: false,
2683            show_arc_length_marks: false,
2684            curvature_comb_scale: CURVATURE_COMB_SCALE,
2685            arc_length_mark_interval: 1.0,
2686            show_frenet_frames: false,
2687            frenet_frame_scale: 0.3,
2688            mesh_section: CrossSection::circle(0.5, 12),
2689            mesh_resolution: 64,
2690            generated_meshes: HashMap::new(),
2691        }
2692    }
2693
2694    fn snap(&self, pos: Vec3) -> Vec3 {
2695        if self.snap_to_grid {
2696            let g = self.grid_size;
2697            Vec3::new(
2698                (pos.x / g).round() * g,
2699                (pos.y / g).round() * g,
2700                (pos.z / g).round() * g,
2701            )
2702        } else {
2703            pos
2704        }
2705    }
2706
2707    // ---- CATMULL-ROM OPERATIONS ----
2708
2709    pub fn create_catmull_spline(&mut self, points: Vec<Vec3>, name: &str) -> u64 {
2710        let id = rand_id();
2711        let points: Vec<Vec3> = points.into_iter().map(|p| self.snap(p)).collect();
2712        let spline = CatmullRomSpline::new(points, self.default_alpha, false);
2713        self.catmull_splines.insert(id, spline);
2714        self.spline_names.insert(id, name.to_string());
2715        self.spline_types.insert(id, SplineType::CatmullRom);
2716        self.undo_history.push(SplineEditorCommand::AddSpline { spline_id: id });
2717        id
2718    }
2719
2720    pub fn remove_catmull_spline(&mut self, id: u64) {
2721        if let Some(_) = self.catmull_splines.remove(&id) {
2722            self.spline_names.remove(&id);
2723            self.spline_types.remove(&id);
2724            self.undo_history.push(SplineEditorCommand::RemoveSpline { spline_id: id });
2725        }
2726    }
2727
2728    pub fn add_control_point(&mut self, spline_id: u64, position: Vec3) {
2729        let position = self.snap(position);
2730        if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
2731            let index = spline.control_points.len();
2732            let cp = ControlPoint::new(position);
2733            self.undo_history.push(SplineEditorCommand::AddControlPoint {
2734                spline_id, index, point: cp.clone(),
2735            });
2736            spline.control_points.push(cp);
2737            spline.rebuild_arc_length_table();
2738        }
2739    }
2740
2741    pub fn remove_control_point(&mut self, spline_id: u64, index: usize) {
2742        if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
2743            if index < spline.control_points.len() {
2744                let point = spline.control_points.remove(index);
2745                self.undo_history.push(SplineEditorCommand::RemoveControlPoint {
2746                    spline_id, index, point,
2747                });
2748                spline.rebuild_arc_length_table();
2749            }
2750        }
2751    }
2752
2753    pub fn move_control_point(&mut self, spline_id: u64, index: usize, new_pos: Vec3) {
2754        let new_pos = self.snap(new_pos);
2755        if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
2756            if index < spline.control_points.len() {
2757                let old_pos = spline.control_points[index].position;
2758                spline.control_points[index].position = new_pos;
2759                self.undo_history.push(SplineEditorCommand::MoveControlPoint {
2760                    spline_id, index, old_pos, new_pos,
2761                });
2762                spline.rebuild_arc_length_table();
2763            }
2764        }
2765    }
2766
2767    pub fn insert_knot_at(&mut self, spline_id: u64, t: f32) {
2768        if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
2769            self.undo_history.push(SplineEditorCommand::InsertKnot { spline_id, t });
2770            spline.insert_knot(t);
2771        }
2772    }
2773
2774    pub fn toggle_closed_spline(&mut self, spline_id: u64) {
2775        if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
2776            spline.toggle_closed();
2777            self.undo_history.push(SplineEditorCommand::ToggleClosed { spline_id });
2778        }
2779    }
2780
2781    pub fn split_spline(&mut self, spline_id: u64, t: f32) -> Option<(u64, u64)> {
2782        let spline = self.catmull_splines.remove(&spline_id)?;
2783        let (a, b) = spline.split_at(t);
2784        let id_a = rand_id();
2785        let id_b = rand_id();
2786        let name_a = format!("{}_A", self.spline_names.get(&spline_id).cloned().unwrap_or_default());
2787        let name_b = format!("{}_B", self.spline_names.get(&spline_id).cloned().unwrap_or_default());
2788        self.catmull_splines.insert(id_a, a);
2789        self.catmull_splines.insert(id_b, b);
2790        self.spline_names.insert(id_a, name_a);
2791        self.spline_names.insert(id_b, name_b);
2792        self.spline_types.insert(id_a, SplineType::CatmullRom);
2793        self.spline_types.insert(id_b, SplineType::CatmullRom);
2794        self.undo_history.push(SplineEditorCommand::SplitSpline { spline_id, t });
2795        Some((id_a, id_b))
2796    }
2797
2798    pub fn join_splines(&mut self, id_a: u64, id_b: u64) -> Option<u64> {
2799        let a = self.catmull_splines.remove(&id_a)?;
2800        let b = self.catmull_splines.remove(&id_b)?;
2801        let joined = CatmullRomSpline::join(a, b);
2802        let new_id = rand_id();
2803        let name = format!("{}_{}",
2804            self.spline_names.get(&id_a).cloned().unwrap_or_default(),
2805            self.spline_names.get(&id_b).cloned().unwrap_or_default(),
2806        );
2807        self.catmull_splines.insert(new_id, joined);
2808        self.spline_names.insert(new_id, name);
2809        self.spline_types.insert(new_id, SplineType::CatmullRom);
2810        self.undo_history.push(SplineEditorCommand::JoinSplines { spline_a: id_a, spline_b: id_b });
2811        Some(new_id)
2812    }
2813
2814    // ---- BEZIER OPERATIONS ----
2815
2816    pub fn create_bezier_spline(&mut self, points: &[Vec3], name: &str) -> u64 {
2817        let id = rand_id();
2818        let spline = CubicBezierSpline::from_points(points);
2819        self.bezier_splines.insert(id, spline);
2820        self.spline_names.insert(id, name.to_string());
2821        self.spline_types.insert(id, SplineType::CubicBezier);
2822        self.undo_history.push(SplineEditorCommand::AddSpline { spline_id: id });
2823        id
2824    }
2825
2826    pub fn split_bezier_segment(&mut self, spline_id: u64, seg: usize, u: f32) {
2827        if let Some(spline) = self.bezier_splines.get_mut(&spline_id) {
2828            spline.split_segment(seg, u);
2829        }
2830    }
2831
2832    // ---- B-SPLINE OPERATIONS ----
2833
2834    pub fn create_bspline(&mut self, points: Vec<Vec3>, degree: usize, name: &str) -> u64 {
2835        let id = rand_id();
2836        let spline = BSpline::new(points, degree, false);
2837        self.bsplines.insert(id, spline);
2838        self.spline_names.insert(id, name.to_string());
2839        self.spline_types.insert(id, SplineType::BSpline { degree });
2840        self.undo_history.push(SplineEditorCommand::AddSpline { spline_id: id });
2841        id
2842    }
2843
2844    pub fn insert_bspline_knot(&mut self, spline_id: u64, t: f32) {
2845        if let Some(spline) = self.bsplines.get_mut(&spline_id) {
2846            spline.insert_knot(t);
2847        }
2848    }
2849
2850    // ---- NURBS OPERATIONS ----
2851
2852    pub fn create_nurbs(&mut self, points: Vec<Vec3>, weights: Vec<f32>, degree: usize, name: &str) -> u64 {
2853        let id = rand_id();
2854        let spline = NurbsSpline::new(points, weights, degree);
2855        self.nurbs_splines.insert(id, spline);
2856        self.spline_names.insert(id, name.to_string());
2857        self.spline_types.insert(id, SplineType::Nurbs { degree });
2858        self.undo_history.push(SplineEditorCommand::AddSpline { spline_id: id });
2859        id
2860    }
2861
2862    // ---- HERMITE OPERATIONS ----
2863
2864    pub fn create_hermite_spline(&mut self, points: Vec<(Vec3, Vec3)>, name: &str) -> u64 {
2865        let id = rand_id();
2866        let mut spline = HermiteSpline::new(points);
2867        spline.auto_tangents();
2868        self.hermite_splines.insert(id, spline);
2869        self.spline_names.insert(id, name.to_string());
2870        self.spline_types.insert(id, SplineType::Hermite);
2871        self.undo_history.push(SplineEditorCommand::AddSpline { spline_id: id });
2872        id
2873    }
2874
2875    // ---- RAIL TRACK OPERATIONS ----
2876
2877    pub fn create_rail_track(&mut self, spline_id: u64, gauge: f32) -> Option<u64> {
2878        let spline = self.catmull_splines.get(&spline_id)?.clone();
2879        let track = RailTrack::new(spline, gauge);
2880        let id = track.id;
2881        self.rail_tracks.insert(id, track);
2882        Some(id)
2883    }
2884
2885    pub fn rail_banking_at(&self, track_id: u64, t: f32, speed_ms: f32) -> Option<f32> {
2886        let track = self.rail_tracks.get(&track_id)?;
2887        Some(track.banking_angle_at(t, speed_ms))
2888    }
2889
2890    pub fn get_rail_mesh(&self, track_id: u64, resolution: usize, speed_ms: f32) -> Option<RailMeshData> {
2891        let track = self.rail_tracks.get(&track_id)?;
2892        let mut mesh = track.rail_mesh_data(resolution, speed_ms);
2893        track.add_sleepers(&mut mesh, 0.6);
2894        Some(mesh)
2895    }
2896
2897    // ---- CAMERA RAIL OPERATIONS ----
2898
2899    pub fn create_camera_rail(&mut self, spline_id: u64) -> Option<u64> {
2900        let spline = self.catmull_splines.get(&spline_id)?.clone();
2901        let rail = CameraRail::new(spline);
2902        let id = rand_id();
2903        self.camera_rails.insert(id, rail);
2904        Some(id)
2905    }
2906
2907    pub fn camera_transform_at(&self, rail_id: u64, t: f32) -> Option<Mat4> {
2908        let rail = self.camera_rails.get(&rail_id)?;
2909        Some(rail.camera_transform_at(t))
2910    }
2911
2912    pub fn bake_camera_path(&self, rail_id: u64, steps: usize) -> Vec<(Mat4, f32)> {
2913        self.camera_rails.get(&rail_id)
2914            .map(|r| r.bake_camera_path(steps))
2915            .unwrap_or_default()
2916    }
2917
2918    // ---- MESH GENERATION ----
2919
2920    pub fn generate_mesh_for_spline(&mut self, spline_id: u64) -> bool {
2921        let spline = match self.catmull_splines.get(&spline_id) {
2922            Some(s) => s.clone(),
2923            None => return false,
2924        };
2925        let total_length = spline.total_arc_length();
2926        let section = self.mesh_section.clone();
2927        let resolution = self.mesh_resolution;
2928        let mesh = SplineMesh::generate_from_spline(
2929            &|t| spline.evaluate(t),
2930            &|t| spline.evaluate_derivative(t),
2931            &section,
2932            resolution,
2933            total_length,
2934        );
2935        self.generated_meshes.insert(spline_id, mesh);
2936        true
2937    }
2938
2939    pub fn generate_lod_mesh(&mut self, spline_id: u64, min_steps: usize, max_steps: usize) -> bool {
2940        let spline = match self.catmull_splines.get(&spline_id) {
2941            Some(s) => s.clone(),
2942            None => return false,
2943        };
2944        let total_length = spline.total_arc_length();
2945        let section = self.mesh_section.clone();
2946        let mesh = SplineMesh::generate_lod(
2947            &|t| spline.evaluate(t),
2948            &|t| spline.evaluate_derivative(t),
2949            &|t| spline.curvature_at(t),
2950            &section,
2951            min_steps,
2952            max_steps,
2953            total_length,
2954        );
2955        self.generated_meshes.insert(spline_id, mesh);
2956        true
2957    }
2958
2959    // ---- SPLINE PHYSICS ----
2960
2961    pub fn add_constrained_object(&mut self, spline_id: u64, t: f32, mass: f32) -> u64 {
2962        let obj = SplineConstrainedObject::new(spline_id, t, mass);
2963        let id = obj.id;
2964        self.constrained_objects.push(obj);
2965        id
2966    }
2967
2968    pub fn update_physics(&mut self, dt: f32) {
2969        for obj in &mut self.constrained_objects {
2970            if let Some(spline) = self.catmull_splines.get(&obj.spline_id) {
2971                let spline_clone = spline.clone();
2972                obj.update(dt, &spline_clone);
2973            }
2974        }
2975        if let Some(ts) = &mut self.traffic_system {
2976            ts.update(dt);
2977        }
2978    }
2979
2980    pub fn add_chain(&mut self, spline_id: u64, link_length: f32) {
2981        if let Some(spline) = self.catmull_splines.get(&spline_id) {
2982            let chain = SplineChain::new(spline, spline_id, link_length);
2983            self.chains.push(chain);
2984        }
2985    }
2986
2987    // ---- PATH NETWORK ----
2988
2989    pub fn setup_traffic_system(&mut self) {
2990        let network = self.path_network.clone();
2991        self.traffic_system = Some(TrafficSystem::new(network));
2992    }
2993
2994    pub fn plan_path(&self, start_node: u64, end_node: u64) -> Option<Vec<u64>> {
2995        self.path_network.astar(start_node, end_node)
2996    }
2997
2998    // ---- NEAREST POINT QUERIES ----
2999
3000    pub fn nearest_point_on_any_spline(&self, query: Vec3) -> Option<(u64, f32, Vec3)> {
3001        let mut best_id = 0u64;
3002        let mut best_t  = 0.0_f32;
3003        let mut best_p  = Vec3::ZERO;
3004        let mut best_d  = f32::MAX;
3005
3006        for (&id, spline) in &self.catmull_splines {
3007            let (t, p) = spline.nearest_point(query);
3008            let d = (p - query).length_squared();
3009            if d < best_d {
3010                best_d = d;
3011                best_id = id;
3012                best_t  = t;
3013                best_p  = p;
3014            }
3015        }
3016        for (&id, spline) in &self.bezier_splines {
3017            let (t, p) = spline.nearest_point(query);
3018            let d = (p - query).length_squared();
3019            if d < best_d {
3020                best_d = d;
3021                best_id = id;
3022                best_t  = t;
3023                best_p  = p;
3024            }
3025        }
3026        if best_id == 0 { None } else { Some((best_id, best_t, best_p)) }
3027    }
3028
3029    // ---- INTERSECTION QUERIES ----
3030
3031    pub fn find_spline_plane_intersections(
3032        &self, spline_id: u64, plane_normal: Vec3, plane_d: f32,
3033    ) -> Vec<SplinePlaneIntersection> {
3034        if let Some(spline) = self.catmull_splines.get(&spline_id) {
3035            intersect_spline_plane(&|t| spline.evaluate(t), plane_normal, plane_d, 200)
3036        } else { Vec::new() }
3037    }
3038
3039    pub fn find_spline_spline_intersections(
3040        &self, id_a: u64, id_b: u64, tol: f32,
3041    ) -> Vec<SplineSplineIntersection> {
3042        let a = self.catmull_splines.get(&id_a);
3043        let b = self.catmull_splines.get(&id_b);
3044        if let (Some(sa), Some(sb)) = (a, b) {
3045            intersect_spline_spline(
3046                &|t| sa.evaluate(t),
3047                &|t| sb.evaluate(t),
3048                32, tol,
3049            )
3050        } else { Vec::new() }
3051    }
3052
3053    // ---- DEBUG VIZ UPDATE ----
3054
3055    pub fn update_debug_viz(&mut self) {
3056        self.debug_viz.clear();
3057        if !self.show_debug { return; }
3058
3059        let ids: Vec<u64> = self.catmull_splines.keys().cloned().collect();
3060        for id in ids {
3061            let spline = match self.catmull_splines.get(&id) { Some(s) => s.clone(), None => continue };
3062            // Draw the spline curve
3063            let color = if self.selection.selected.contains(&id) {
3064                Vec4::new(1.0, 0.8, 0.0, 1.0)
3065            } else {
3066                Vec4::new(0.4, 0.9, 0.4, 1.0)
3067            };
3068            self.debug_viz.draw_spline_curve(&|t| spline.evaluate(t), 128, color);
3069            // Control polygon
3070            let pts: Vec<Vec3> = spline.control_points.iter().map(|cp| cp.position).collect();
3071            self.debug_viz.draw_control_polygon(&pts, Vec4::new(0.6, 0.6, 0.6, 0.5));
3072            // Bounding box
3073            let (bb_min, bb_max) = spline.bounding_box();
3074            self.debug_viz.draw_bounding_box(bb_min, bb_max, Vec4::new(0.3, 0.3, 1.0, 0.4));
3075            // Frenet frames
3076            if self.show_frenet_frames {
3077                let scale = self.frenet_frame_scale;
3078                self.debug_viz.draw_frenet_frames(
3079                    &|t| spline.evaluate(t),
3080                    &|t| spline.evaluate_derivative(t),
3081                    &|t| spline.evaluate_second_derivative(t),
3082                    &|t| {
3083                        let dt = 1e-4;
3084                        let a = spline.evaluate_second_derivative((t + dt).min(1.0));
3085                        let b = spline.evaluate_second_derivative((t - dt).max(0.0));
3086                        (a - b) / (2.0 * dt)
3087                    },
3088                    16,
3089                    scale,
3090                );
3091            }
3092            // Curvature comb
3093            if self.show_curvature_comb {
3094                let scale = self.curvature_comb_scale;
3095                self.debug_viz.draw_curvature_comb(
3096                    &|t| spline.evaluate(t),
3097                    &|t| spline.curvature_at(t),
3098                    &|t| spline.frenet_frame_at(t).normal,
3099                    64,
3100                    scale,
3101                );
3102            }
3103            // Arc length marks
3104            if self.show_arc_length_marks {
3105                let interval = self.arc_length_mark_interval;
3106                let total = spline.total_arc_length();
3107                self.debug_viz.draw_arc_length_marks(
3108                    &|t| spline.evaluate(t),
3109                    &|s| spline.t_at_arc_length(s),
3110                    total,
3111                    interval,
3112                    Vec3::Y,
3113                    0.15,
3114                );
3115            }
3116        }
3117    }
3118
3119    // ---- UNDO / REDO ----
3120
3121    pub fn undo(&mut self) {
3122        if let Some(cmd) = self.undo_history.undo() {
3123            self.apply_undo(cmd);
3124        }
3125    }
3126
3127    pub fn redo(&mut self) {
3128        if let Some(cmd) = self.undo_history.redo() {
3129            self.apply_redo(cmd);
3130        }
3131    }
3132
3133    fn apply_undo(&mut self, cmd: SplineEditorCommand) {
3134        match cmd {
3135            SplineEditorCommand::MoveControlPoint { spline_id, index, old_pos, .. } => {
3136                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3137                    if index < spline.control_points.len() {
3138                        spline.control_points[index].position = old_pos;
3139                        spline.rebuild_arc_length_table();
3140                    }
3141                }
3142            }
3143            SplineEditorCommand::AddControlPoint { spline_id, index, .. } => {
3144                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3145                    if index < spline.control_points.len() {
3146                        spline.control_points.remove(index);
3147                        spline.rebuild_arc_length_table();
3148                    }
3149                }
3150            }
3151            SplineEditorCommand::RemoveControlPoint { spline_id, index, point } => {
3152                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3153                    spline.control_points.insert(index.min(spline.control_points.len()), point);
3154                    spline.rebuild_arc_length_table();
3155                }
3156            }
3157            SplineEditorCommand::ToggleClosed { spline_id } => {
3158                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3159                    spline.toggle_closed();
3160                }
3161            }
3162            _ => { /* other commands handled separately */ }
3163        }
3164    }
3165
3166    fn apply_redo(&mut self, cmd: SplineEditorCommand) {
3167        match cmd {
3168            SplineEditorCommand::MoveControlPoint { spline_id, index, new_pos, .. } => {
3169                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3170                    if index < spline.control_points.len() {
3171                        spline.control_points[index].position = new_pos;
3172                        spline.rebuild_arc_length_table();
3173                    }
3174                }
3175            }
3176            SplineEditorCommand::AddControlPoint { spline_id, index, point } => {
3177                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3178                    spline.control_points.insert(index.min(spline.control_points.len()), point);
3179                    spline.rebuild_arc_length_table();
3180                }
3181            }
3182            SplineEditorCommand::RemoveControlPoint { spline_id, index, .. } => {
3183                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3184                    if index < spline.control_points.len() {
3185                        spline.control_points.remove(index);
3186                        spline.rebuild_arc_length_table();
3187                    }
3188                }
3189            }
3190            SplineEditorCommand::ToggleClosed { spline_id } => {
3191                if let Some(spline) = self.catmull_splines.get_mut(&spline_id) {
3192                    spline.toggle_closed();
3193                }
3194            }
3195            _ => {}
3196        }
3197    }
3198
3199    // ---- SERIALIZATION ----
3200
3201    pub fn serialize_spline(&self, spline_id: u64) -> Option<SplineSerializedData> {
3202        let spline = self.catmull_splines.get(&spline_id)?;
3203        let name = self.spline_names.get(&spline_id).cloned().unwrap_or_default();
3204        Some(SplineSerializedData::serialize_catmull_rom(spline, spline_id, &name))
3205    }
3206
3207    pub fn serialize_all(&self) -> Vec<SplineSerializedData> {
3208        self.catmull_splines.iter().map(|(&id, spline)| {
3209            let name = self.spline_names.get(&id).cloned().unwrap_or_default();
3210            SplineSerializedData::serialize_catmull_rom(spline, id, &name)
3211        }).collect()
3212    }
3213
3214    pub fn deserialize_and_add(&mut self, data: SplineSerializedData) {
3215        let points: Vec<Vec3> = data.control_points.iter().map(|(p, _, _, _)| *p).collect();
3216        let id = data.spline_id;
3217        let mut spline = CatmullRomSpline::new(points, self.default_alpha, data.closed);
3218        // Restore weights / tangents
3219        for (i, (_, t_in, t_out, w)) in data.control_points.iter().enumerate() {
3220            if i < spline.control_points.len() {
3221                spline.control_points[i].tangent_in  = *t_in;
3222                spline.control_points[i].tangent_out = *t_out;
3223                spline.control_points[i].weight      = *w;
3224            }
3225        }
3226        spline.rebuild_arc_length_table();
3227        self.catmull_splines.insert(id, spline);
3228        self.spline_names.insert(id, data.name);
3229        self.spline_types.insert(id, data.spline_type);
3230    }
3231
3232    // ---- QUERY HELPERS ----
3233
3234    pub fn spline_ids(&self) -> Vec<u64> {
3235        let mut ids: Vec<u64> = self.catmull_splines.keys().cloned().collect();
3236        ids.extend(self.bezier_splines.keys().cloned());
3237        ids.extend(self.bsplines.keys().cloned());
3238        ids.extend(self.nurbs_splines.keys().cloned());
3239        ids.extend(self.hermite_splines.keys().cloned());
3240        ids
3241    }
3242
3243    pub fn spline_count(&self) -> usize {
3244        self.catmull_splines.len()
3245            + self.bezier_splines.len()
3246            + self.bsplines.len()
3247            + self.nurbs_splines.len()
3248            + self.hermite_splines.len()
3249    }
3250
3251    pub fn evaluate_spline(&self, id: u64, t: f32) -> Option<Vec3> {
3252        if let Some(s) = self.catmull_splines.get(&id) { return Some(s.evaluate(t)); }
3253        if let Some(s) = self.bezier_splines.get(&id)  { return Some(s.evaluate(t)); }
3254        if let Some(s) = self.bsplines.get(&id)        { return Some(s.evaluate(t)); }
3255        if let Some(s) = self.nurbs_splines.get(&id)   { return Some(s.evaluate(t)); }
3256        if let Some(s) = self.hermite_splines.get(&id) { return Some(s.evaluate(t)); }
3257        None
3258    }
3259
3260    pub fn spline_arc_length(&self, id: u64) -> f32 {
3261        if let Some(s) = self.catmull_splines.get(&id) { return s.total_arc_length(); }
3262        if let Some(s) = self.bezier_splines.get(&id)  { return s.total_arc_length(); }
3263        if let Some(s) = self.bsplines.get(&id)        { return s.total_arc_length(); }
3264        if let Some(s) = self.nurbs_splines.get(&id)   { return s.total_arc_length(); }
3265        if let Some(s) = self.hermite_splines.get(&id) { return s.total_arc_length(); }
3266        0.0
3267    }
3268
3269    pub fn curvature_at(&self, id: u64, t: f32) -> f32 {
3270        if let Some(s) = self.catmull_splines.get(&id) { return s.curvature_at(t); }
3271        if let Some(s) = self.bezier_splines.get(&id)  { return s.curvature_at(t); }
3272        if let Some(s) = self.bsplines.get(&id)        { return s.curvature_at(t); }
3273        if let Some(s) = self.nurbs_splines.get(&id)   { return s.curvature_at(t); }
3274        0.0
3275    }
3276}
3277
3278// ============================================================
3279// ADDITIONAL SPLINE MATH UTILITIES
3280// ============================================================
3281
3282/// Reparameterize a polyline by arc length to produce evenly spaced points
3283pub fn resample_polyline(pts: &[Vec3], n_out: usize) -> Vec<Vec3> {
3284    if pts.len() < 2 || n_out < 2 { return pts.to_vec(); }
3285    // Compute cumulative arc lengths
3286    let mut lengths = Vec::with_capacity(pts.len());
3287    lengths.push(0.0_f32);
3288    for i in 1..pts.len() {
3289        lengths.push(lengths[i - 1] + (pts[i] - pts[i - 1]).length());
3290    }
3291    let total = *lengths.last().unwrap();
3292    let mut out = Vec::with_capacity(n_out);
3293    for i in 0..n_out {
3294        let target_s = i as f32 / (n_out - 1) as f32 * total;
3295        let idx = lengths.partition_point(|&l| l <= target_s);
3296        let p = if idx == 0 {
3297            pts[0]
3298        } else if idx >= pts.len() {
3299            *pts.last().unwrap()
3300        } else {
3301            let s0 = lengths[idx - 1];
3302            let s1 = lengths[idx];
3303            let frac = if (s1 - s0).abs() < EPSILON { 0.0 } else { (target_s - s0) / (s1 - s0) };
3304            lerp_vec3(pts[idx - 1], pts[idx], frac)
3305        };
3306        out.push(p);
3307    }
3308    out
3309}
3310
3311/// Compute signed curvature of a 2D polyline
3312pub fn polyline_signed_curvature_2d(pts: &[Vec2]) -> Vec<f32> {
3313    let n = pts.len();
3314    if n < 3 { return vec![0.0; n]; }
3315    let mut kappas = vec![0.0_f32; n];
3316    for i in 1..n - 1 {
3317        let a = pts[i - 1];
3318        let b = pts[i];
3319        let c = pts[i + 1];
3320        let ab = b - a;
3321        let bc = c - b;
3322        let cross = ab.x * bc.y - ab.y * bc.x; // 2D cross product
3323        let dot   = ab.dot(bc);
3324        let angle = cross.atan2(dot);
3325        let seg_len = (ab.length() + bc.length()) * 0.5;
3326        kappas[i] = if seg_len > EPSILON { angle / seg_len } else { 0.0 };
3327    }
3328    kappas[0]       = kappas[1];
3329    kappas[n - 1]   = kappas[n - 2];
3330    kappas
3331}
3332
3333/// Smooth a polyline with a Gaussian kernel
3334pub fn smooth_polyline(pts: &[Vec3], iterations: usize, strength: f32) -> Vec<Vec3> {
3335    let n = pts.len();
3336    if n < 3 { return pts.to_vec(); }
3337    let mut result = pts.to_vec();
3338    for _ in 0..iterations {
3339        let prev = result.clone();
3340        for i in 1..n - 1 {
3341            let avg = (prev[i - 1] + prev[i + 1]) * 0.5;
3342            result[i] = lerp_vec3(prev[i], avg, strength);
3343        }
3344    }
3345    result
3346}
3347
3348/// Douglas-Peucker polyline simplification
3349pub fn douglas_peucker(pts: &[Vec3], epsilon: f32) -> Vec<Vec3> {
3350    if pts.len() < 3 { return pts.to_vec(); }
3351    // Find point with max distance from line first-last
3352    let mut max_dist = 0.0_f32;
3353    let mut max_idx  = 0usize;
3354    let start = pts[0];
3355    let end   = *pts.last().unwrap();
3356    let seg = end - start;
3357    let seg_len_sq = seg.length_squared();
3358    for i in 1..pts.len() - 1 {
3359        let dist = if seg_len_sq < EPSILON {
3360            (pts[i] - start).length()
3361        } else {
3362            let t = ((pts[i] - start).dot(seg) / seg_len_sq).clamp(0.0, 1.0);
3363            let proj = start + seg * t;
3364            (pts[i] - proj).length()
3365        };
3366        if dist > max_dist {
3367            max_dist = dist;
3368            max_idx  = i;
3369        }
3370    }
3371    if max_dist > epsilon {
3372        let mut left  = douglas_peucker(&pts[..=max_idx], epsilon);
3373        let right = douglas_peucker(&pts[max_idx..], epsilon);
3374        left.pop(); // remove duplicate
3375        left.extend(right);
3376        left
3377    } else {
3378        vec![pts[0], *pts.last().unwrap()]
3379    }
3380}
3381
3382/// Catmull-Clark subdivision of a 3D polyline (single iteration)
3383pub fn catmull_clark_subdivide_1d(pts: &[Vec3], closed: bool) -> Vec<Vec3> {
3384    let n = pts.len();
3385    if n < 2 { return pts.to_vec(); }
3386    let mut out = Vec::with_capacity(n * 2);
3387    for i in 0..n - 1 {
3388        out.push(pts[i]);
3389        out.push((pts[i] + pts[i + 1]) * 0.5);
3390    }
3391    out.push(*pts.last().unwrap());
3392    // Smooth
3393    let raw = out.clone();
3394    let m = raw.len();
3395    let mut smoothed = vec![Vec3::ZERO; m];
3396    smoothed[0]     = raw[0];
3397    smoothed[m - 1] = raw[m - 1];
3398    for i in 1..m - 1 {
3399        smoothed[i] = raw[i - 1] * 0.25 + raw[i] * 0.5 + raw[i + 1] * 0.25;
3400    }
3401    smoothed
3402}
3403
3404/// Compute the osculating circle at a point: returns (center, radius)
3405pub fn osculating_circle(pos: Vec3, tangent: Vec3, normal: Vec3, curvature: f32) -> (Vec3, f32) {
3406    if curvature < EPSILON {
3407        return (pos + normal * 1e9, 1e9);
3408    }
3409    let r = 1.0 / curvature;
3410    let center = pos + normal * r;
3411    (center, r)
3412}
3413
3414/// Evolute of a curve: locus of centers of osculating circles
3415pub fn compute_evolute(
3416    pos_fn: &dyn Fn(f32) -> Vec3,
3417    normal_fn: &dyn Fn(f32) -> Vec3,
3418    curvature_fn: &dyn Fn(f32) -> f32,
3419    steps: usize,
3420) -> Vec<Vec3> {
3421    (0..=steps).map(|i| {
3422        let t = i as f32 / steps as f32;
3423        let (center, _) = osculating_circle(pos_fn(t), Vec3::ZERO, normal_fn(t), curvature_fn(t));
3424        center
3425    }).collect()
3426}
3427
3428/// Involute of a spline: unrolling a string from the curve
3429pub fn compute_involute(
3430    pos_fn: &dyn Fn(f32) -> Vec3,
3431    tangent_fn: &dyn Fn(f32) -> Vec3,
3432    t_at_len_fn: &dyn Fn(f32) -> f32,
3433    total_length: f32,
3434    start_s: f32,
3435    steps: usize,
3436) -> Vec<Vec3> {
3437    (0..=steps).map(|i| {
3438        let t = i as f32 / steps as f32;
3439        let s = t * total_length;
3440        let p = pos_fn(t);
3441        let tang = safe_normalize(tangent_fn(t));
3442        let arc_remaining = (s - start_s).max(0.0);
3443        p - tang * arc_remaining
3444    }).collect()
3445}
3446
3447/// Compute the writhe of a closed curve (Gauss integral)
3448pub fn compute_writhe(pts: &[Vec3]) -> f32 {
3449    let n = pts.len();
3450    if n < 3 { return 0.0; }
3451    let mut writhe = 0.0_f32;
3452    for i in 0..n {
3453        let r1 = pts[i];
3454        let r1n = pts[(i + 1) % n];
3455        let dr1 = r1n - r1;
3456        for j in (i + 2)..n {
3457            if i == 0 && j == n - 1 { continue; }
3458            let r2 = pts[j];
3459            let r2n = pts[(j + 1) % n];
3460            let dr2 = r2n - r2;
3461            let r = r2 - r1;
3462            let r_len = r.length();
3463            if r_len < EPSILON { continue; }
3464            let cross = dr1.cross(dr2);
3465            writhe += cross.dot(r) / (r_len * r_len * r_len);
3466        }
3467    }
3468    writhe / (4.0 * std::f32::consts::PI)
3469}
3470
3471// ============================================================
3472// EXTRA SPLINE EDITOR UI HELPERS
3473// ============================================================
3474
3475#[derive(Clone, Debug)]
3476pub struct SplineEditorUIState {
3477    pub active_tool: SplineTool,
3478    pub drag_start: Option<Vec3>,
3479    pub drag_current: Option<Vec3>,
3480    pub hover_t: f32,
3481    pub hover_position: Vec3,
3482    pub show_tangent_handles: bool,
3483    pub tangent_handle_scale: f32,
3484    pub tangent_mirror: bool,         // mirror in/out tangents
3485    pub show_weights:   bool,
3486    pub edit_mode: SplineEditMode,
3487    pub snap_angle: f32,              // degrees, for tangent snapping
3488    pub snap_angle_enabled: bool,
3489}
3490
3491#[derive(Clone, Debug, PartialEq)]
3492pub enum SplineTool {
3493    Select,
3494    AddPoint,
3495    RemovePoint,
3496    MoveTangent,
3497    SliceAtCursor,
3498    MeasureLength,
3499}
3500
3501#[derive(Clone, Debug, PartialEq)]
3502pub enum SplineEditMode {
3503    Points,
3504    Tangents,
3505    Knots,
3506    Weights,
3507}
3508
3509impl SplineEditorUIState {
3510    pub fn new() -> Self {
3511        SplineEditorUIState {
3512            active_tool: SplineTool::Select,
3513            drag_start: None,
3514            drag_current: None,
3515            hover_t: 0.0,
3516            hover_position: Vec3::ZERO,
3517            show_tangent_handles: true,
3518            tangent_handle_scale: 1.0,
3519            tangent_mirror: true,
3520            show_weights: false,
3521            edit_mode: SplineEditMode::Points,
3522            snap_angle: 15.0,
3523            snap_angle_enabled: false,
3524        }
3525    }
3526
3527    pub fn snap_tangent_to_angle(&self, tangent: Vec3) -> Vec3 {
3528        if !self.snap_angle_enabled { return tangent; }
3529        let snap_rad = self.snap_angle.to_radians();
3530        let len = tangent.length();
3531        if len < EPSILON { return tangent; }
3532        let dir = tangent / len;
3533        // Snap to nearest multiple of snap_angle around Y axis
3534        let angle = dir.x.atan2(dir.z);
3535        let snapped = (angle / snap_rad).round() * snap_rad;
3536        Vec3::new(snapped.sin() * len, tangent.y, snapped.cos() * len)
3537    }
3538
3539    pub fn mirror_tangent(&self, tangent: Vec3) -> Vec3 {
3540        if self.tangent_mirror { -tangent } else { tangent }
3541    }
3542
3543    pub fn drag_delta(&self) -> Vec3 {
3544        match (self.drag_start, self.drag_current) {
3545            (Some(s), Some(c)) => c - s,
3546            _ => Vec3::ZERO,
3547        }
3548    }
3549}
3550
3551// ============================================================
3552// SPLINE SAMPLING UTILITIES
3553// ============================================================
3554
3555/// Sample evenly in arc length
3556pub fn sample_arc_length_uniform(
3557    pos_fn: &dyn Fn(f32) -> Vec3,
3558    t_at_len_fn: &dyn Fn(f32) -> f32,
3559    total_length: f32,
3560    n: usize,
3561) -> Vec<Vec3> {
3562    if n == 0 { return Vec::new(); }
3563    (0..n).map(|i| {
3564        let s = i as f32 / (n - 1).max(1) as f32 * total_length;
3565        pos_fn(t_at_len_fn(s))
3566    }).collect()
3567}
3568
3569/// Sample using chord-length parameterization
3570pub fn sample_chord_length(pts: &[Vec3], n: usize) -> Vec<Vec3> {
3571    if pts.len() < 2 { return pts.to_vec(); }
3572    let total: f32 = pts.windows(2).map(|w| (w[1] - w[0]).length()).sum();
3573    let mut cum = vec![0.0_f32];
3574    for w in pts.windows(2) {
3575        cum.push(*cum.last().unwrap() + (w[1] - w[0]).length());
3576    }
3577    (0..n).map(|i| {
3578        let target = i as f32 / (n - 1).max(1) as f32 * total;
3579        let idx = cum.partition_point(|&c| c <= target).min(cum.len() - 1);
3580        let idx = idx.max(1);
3581        let s0 = cum[idx - 1];
3582        let s1 = cum[idx];
3583        let f = if (s1 - s0).abs() < EPSILON { 0.0 } else { (target - s0) / (s1 - s0) };
3584        lerp_vec3(pts[idx - 1], pts[idx.min(pts.len() - 1)], f)
3585    }).collect()
3586}
3587
3588// ============================================================
3589// BEZIER FITTING (least-squares fitting to point cloud)
3590// ============================================================
3591
3592pub struct BezierFitter {
3593    pub max_error: f32,
3594    pub max_iterations: usize,
3595}
3596
3597impl BezierFitter {
3598    pub fn new(max_error: f32) -> Self {
3599        BezierFitter { max_error, max_iterations: 32 }
3600    }
3601
3602    /// Fit a single cubic bezier to a sequence of points
3603    pub fn fit_cubic(&self, pts: &[Vec3]) -> Option<[Vec3; 4]> {
3604        let n = pts.len();
3605        if n < 2 { return None; }
3606        if n == 2 {
3607            let t1 = (pts[1] - pts[0]) / 3.0;
3608            return Some([pts[0], pts[0] + t1, pts[1] - t1, pts[1]]);
3609        }
3610        // Chord-length parameterization
3611        let params = chord_length_params(pts);
3612        let d1 = safe_normalize(pts[1] - pts[0]);
3613        let dn = safe_normalize(pts[n - 1] - pts[n - 2]);
3614        // Least-squares
3615        self.fit_cubic_with_tangents(pts, &params, d1, dn)
3616    }
3617
3618    fn fit_cubic_with_tangents(
3619        &self, pts: &[Vec3], params: &[f32], t0: Vec3, t1: Vec3
3620    ) -> Option<[Vec3; 4]> {
3621        let n = pts.len();
3622        let p0 = pts[0];
3623        let p3 = pts[n - 1];
3624        // Solve 2x2 linear system for alpha0, alpha1
3625        let mut a00 = 0.0_f32;
3626        let mut a01 = 0.0_f32;
3627        let mut a11 = 0.0_f32;
3628        let mut b0  = Vec3::ZERO;
3629        let mut b1  = Vec3::ZERO;
3630        for (i, &t) in params.iter().enumerate() {
3631            let b0_t = bernstein(0, 3, t);
3632            let b1_t = bernstein(1, 3, t);
3633            let b2_t = bernstein(2, 3, t);
3634            let b3_t = bernstein(3, 3, t);
3635            let a0i = t0 * b1_t;
3636            let a1i = t1 * b2_t;
3637            a00 += a0i.dot(a0i);
3638            a01 += a0i.dot(a1i);
3639            a11 += a1i.dot(a1i);
3640            let tmp = pts[i] - (p0 * (b0_t + b1_t) + p3 * (b2_t + b3_t));
3641            b0 += a0i * tmp.dot(a0i) / a0i.dot(a0i).max(EPSILON);
3642            b1 += a1i * tmp.dot(a1i) / a1i.dot(a1i).max(EPSILON);
3643        }
3644        let det = a00 * a11 - a01 * a01;
3645        let (alpha0, alpha1) = if det.abs() > EPSILON {
3646            let b0s = b0.length();
3647            let b1s = b1.length();
3648            let al0 = (a11 * b0s - a01 * b1s) / det;
3649            let al1 = (a00 * b1s - a01 * b0s) / det;
3650            (al0.max(EPSILON), al1.max(EPSILON))
3651        } else {
3652            let chord = (p3 - p0).length() / 3.0;
3653            (chord, chord)
3654        };
3655        Some([p0, p0 + t0 * alpha0, p3 - t1 * alpha1, p3])
3656    }
3657}
3658
3659fn bernstein(i: usize, n: usize, t: f32) -> f32 {
3660    fn binom(n: usize, k: usize) -> f32 {
3661        if k > n { return 0.0; }
3662        let mut result = 1.0_f32;
3663        for j in 0..k {
3664            result *= (n - j) as f32 / (j + 1) as f32;
3665        }
3666        result
3667    }
3668    binom(n, i) * t.powi(i as i32) * (1.0 - t).powi((n - i) as i32)
3669}
3670
3671fn chord_length_params(pts: &[Vec3]) -> Vec<f32> {
3672    let n = pts.len();
3673    let mut lengths = vec![0.0_f32; n];
3674    for i in 1..n {
3675        lengths[i] = lengths[i - 1] + (pts[i] - pts[i - 1]).length();
3676    }
3677    let total = lengths[n - 1];
3678    if total < EPSILON {
3679        return (0..n).map(|i| i as f32 / (n - 1).max(1) as f32).collect();
3680    }
3681    lengths.iter().map(|&l| l / total).collect()
3682}
3683
3684// ============================================================
3685// SPLINE OFFSET (parallel curve)
3686// ============================================================
3687
3688pub fn offset_spline(
3689    pos_fn:    &dyn Fn(f32) -> Vec3,
3690    normal_fn: &dyn Fn(f32) -> Vec3,
3691    offset:    f32,
3692    steps:     usize,
3693) -> Vec<Vec3> {
3694    (0..=steps).map(|i| {
3695        let t = i as f32 / steps as f32;
3696        pos_fn(t) + normal_fn(t) * offset
3697    }).collect()
3698}
3699
3700/// Tube mesh: a circle cross-section swept along a spline
3701pub fn build_tube_mesh(
3702    pos_fn:    &dyn Fn(f32) -> Vec3,
3703    tang_fn:   &dyn Fn(f32) -> Vec3,
3704    radius:    f32,
3705    seg_count: usize,
3706    ring_count: usize,
3707) -> SplineMesh {
3708    let section = CrossSection::circle(radius, seg_count);
3709    let total_length = {
3710        let mut s = 0.0_f32;
3711        let mut prev = pos_fn(0.0);
3712        for i in 1..=256 {
3713            let t = i as f32 / 256.0;
3714            let cur = pos_fn(t);
3715            s += (cur - prev).length();
3716            prev = cur;
3717        }
3718        s
3719    };
3720    SplineMesh::generate_from_spline(pos_fn, tang_fn, &section, ring_count, total_length)
3721}
3722
3723// ============================================================
3724// SPLINE LOFTING
3725// ============================================================
3726
3727pub struct LoftedSurface {
3728    pub vertices: Vec<Vec3>,
3729    pub normals:  Vec<Vec3>,
3730    pub uvs:      Vec<Vec2>,
3731    pub indices:  Vec<u32>,
3732}
3733
3734impl LoftedSurface {
3735    /// Loft between two splines
3736    pub fn loft(
3737        spline_a: &dyn Fn(f32) -> Vec3,
3738        spline_b: &dyn Fn(f32) -> Vec3,
3739        u_steps: usize,
3740        v_steps: usize,
3741    ) -> Self {
3742        let mut verts   = Vec::new();
3743        let mut normals = Vec::new();
3744        let mut uvs     = Vec::new();
3745        let mut indices = Vec::new();
3746
3747        for j in 0..=v_steps {
3748            let v = j as f32 / v_steps as f32;
3749            for i in 0..=u_steps {
3750                let u = i as f32 / u_steps as f32;
3751                let pa = spline_a(u);
3752                let pb = spline_b(u);
3753                let p  = lerp_vec3(pa, pb, v);
3754                // Approximate normal via finite differences
3755                let pa_u = spline_a((u + 1e-3).min(1.0));
3756                let pb_u = spline_b((u + 1e-3).min(1.0));
3757                let pu = lerp_vec3(pa_u, pb_u, v) - p;
3758                let pv = pb - pa;
3759                let n = safe_normalize(pu.cross(pv));
3760                verts.push(p);
3761                normals.push(n);
3762                uvs.push(Vec2::new(u, v));
3763            }
3764        }
3765
3766        for j in 0..v_steps {
3767            for i in 0..u_steps {
3768                let a = (j * (u_steps + 1) + i) as u32;
3769                let b = a + 1;
3770                let c = ((j + 1) * (u_steps + 1) + i) as u32;
3771                let d = c + 1;
3772                indices.extend_from_slice(&[a, b, c, b, d, c]);
3773            }
3774        }
3775
3776        LoftedSurface { vertices: verts, normals, uvs, indices }
3777    }
3778}
3779
3780// ============================================================
3781// EXTENDED CATMULL-ROM: CHORD-LENGTH VS CENTRIPETAL COMPARISON
3782// ============================================================
3783
3784pub fn catmull_rom_compare_parameterizations(
3785    p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3,
3786    num_samples: usize,
3787) -> (Vec<Vec3>, Vec<Vec3>, Vec<Vec3>) {
3788    // Uniform
3789    let uniform_pts: Vec<Vec3> = (0..=num_samples).map(|i| {
3790        let t = i as f32 / num_samples as f32;
3791        CatmullRomSpline::new(vec![p0, p1, p2, p3], 0.0, false).evaluate(t)
3792    }).collect();
3793    // Centripetal
3794    let centripetal_pts: Vec<Vec3> = (0..=num_samples).map(|i| {
3795        let t = i as f32 / num_samples as f32;
3796        CatmullRomSpline::new(vec![p0, p1, p2, p3], 0.5, false).evaluate(t)
3797    }).collect();
3798    // Chordal
3799    let chordal_pts: Vec<Vec3> = (0..=num_samples).map(|i| {
3800        let t = i as f32 / num_samples as f32;
3801        CatmullRomSpline::new(vec![p0, p1, p2, p3], 1.0, false).evaluate(t)
3802    }).collect();
3803    (uniform_pts, centripetal_pts, chordal_pts)
3804}
3805
3806// ============================================================
3807// SPLINE DEFORMATION (FFD-style along spline)
3808// ============================================================
3809
3810pub struct SplineDeformer {
3811    pub spline_id: u64,
3812    pub falloff_radius: f32,
3813    pub strength: f32,
3814    pub deform_axis: Vec3,
3815}
3816
3817impl SplineDeformer {
3818    pub fn new(spline_id: u64, falloff_radius: f32, strength: f32) -> Self {
3819        SplineDeformer {
3820            spline_id,
3821            falloff_radius,
3822            strength,
3823            deform_axis: Vec3::Y,
3824        }
3825    }
3826
3827    /// Deform a mesh vertex based on proximity to spline
3828    pub fn deform_point(&self, point: Vec3, spline: &CatmullRomSpline) -> Vec3 {
3829        let (t, closest) = spline.nearest_point(point);
3830        let dist = (point - closest).length();
3831        if dist > self.falloff_radius { return point; }
3832        let frame = spline.frenet_frame_at(t);
3833        let falloff = 1.0 - (dist / self.falloff_radius).powi(2);
3834        let displacement = frame.normal * self.strength * falloff;
3835        point + displacement
3836    }
3837
3838    pub fn deform_mesh(&self, vertices: &mut [Vec3], spline: &CatmullRomSpline) {
3839        for v in vertices.iter_mut() {
3840            *v = self.deform_point(*v, spline);
3841        }
3842    }
3843}
3844
3845// ============================================================
3846// SPEED CURVES (for cinematic/rail use)
3847// ============================================================
3848
3849#[derive(Clone, Debug)]
3850pub struct SpeedCurve {
3851    /// Control points (t, speed) with tangents for smooth interpolation
3852    pub keyframes: Vec<SpeedKey>,
3853}
3854
3855#[derive(Clone, Debug)]
3856pub struct SpeedKey {
3857    pub t:     f32,
3858    pub speed: f32,
3859    pub tan_in:  f32,
3860    pub tan_out: f32,
3861}
3862
3863impl SpeedCurve {
3864    pub fn new() -> Self { SpeedCurve { keyframes: Vec::new() } }
3865
3866    pub fn add_key(&mut self, t: f32, speed: f32) {
3867        let idx = self.keyframes.partition_point(|k| k.t < t);
3868        self.keyframes.insert(idx, SpeedKey { t, speed, tan_in: 0.0, tan_out: 0.0 });
3869        self.auto_tangents();
3870    }
3871
3872    pub fn auto_tangents(&mut self) {
3873        let n = self.keyframes.len();
3874        for i in 0..n {
3875            let prev_speed = if i > 0 { self.keyframes[i-1].speed } else { self.keyframes[i].speed };
3876            let next_speed = if i+1 < n { self.keyframes[i+1].speed } else { self.keyframes[i].speed };
3877            let tan = (next_speed - prev_speed) * 0.5;
3878            self.keyframes[i].tan_in  = tan;
3879            self.keyframes[i].tan_out = tan;
3880        }
3881    }
3882
3883    pub fn evaluate(&self, t: f32) -> f32 {
3884        let n = self.keyframes.len();
3885        if n == 0 { return 0.0; }
3886        if n == 1 { return self.keyframes[0].speed; }
3887        let idx = self.keyframes.partition_point(|k| k.t <= t);
3888        if idx == 0 { return self.keyframes[0].speed; }
3889        if idx >= n { return self.keyframes[n-1].speed; }
3890        let k0 = &self.keyframes[idx-1];
3891        let k1 = &self.keyframes[idx];
3892        let dt = k1.t - k0.t;
3893        if dt.abs() < EPSILON { return k0.speed; }
3894        let u = (t - k0.t) / dt;
3895        // Cubic Hermite
3896        let u2 = u * u;
3897        let u3 = u2 * u;
3898        let h00 =  2.0*u3 - 3.0*u2 + 1.0;
3899        let h10 =     u3  - 2.0*u2 + u;
3900        let h01 = -2.0*u3 + 3.0*u2;
3901        let h11 =     u3  -     u2;
3902        k0.speed * h00 + k0.tan_out * h10 * dt
3903            + k1.speed * h01 + k1.tan_in * h11 * dt
3904    }
3905
3906    /// Integrate speed over [0, t] to get arc-length
3907    pub fn integrate_to(&self, t: f32, steps: usize) -> f32 {
3908        let dt = t / steps.max(1) as f32;
3909        let mut s = 0.0_f32;
3910        for i in 0..steps {
3911            let t0 = i as f32 * dt;
3912            let t1 = (i + 1) as f32 * dt;
3913            s += (self.evaluate(t0) + self.evaluate(t1)) * 0.5 * dt;
3914        }
3915        s
3916    }
3917}
3918
3919// ============================================================
3920// SIGNAL TRACK (for game events along spline)
3921// ============================================================
3922
3923#[derive(Clone, Debug)]
3924pub struct SplineSignal {
3925    pub t: f32,       // position along spline [0,1]
3926    pub kind: String,
3927    pub data: HashMap<String, f32>,
3928    pub triggered: bool,
3929}
3930
3931impl SplineSignal {
3932    pub fn new(t: f32, kind: &str) -> Self {
3933        SplineSignal { t, kind: kind.to_string(), data: HashMap::new(), triggered: false }
3934    }
3935
3936    pub fn with_data(mut self, key: &str, val: f32) -> Self {
3937        self.data.insert(key.to_string(), val);
3938        self
3939    }
3940}
3941
3942#[derive(Clone, Debug)]
3943pub struct SplineSignalTrack {
3944    pub spline_id: u64,
3945    pub signals: Vec<SplineSignal>,
3946    pub loop_signals: bool,
3947}
3948
3949impl SplineSignalTrack {
3950    pub fn new(spline_id: u64) -> Self {
3951        SplineSignalTrack { spline_id, signals: Vec::new(), loop_signals: false }
3952    }
3953
3954    pub fn add_signal(&mut self, t: f32, kind: &str) {
3955        let sig = SplineSignal::new(t, kind);
3956        let idx = self.signals.partition_point(|s| s.t < t);
3957        self.signals.insert(idx, sig);
3958    }
3959
3960    /// Poll for signals triggered as t passes from prev_t to cur_t
3961    pub fn poll(&mut self, prev_t: f32, cur_t: f32) -> Vec<SplineSignal> {
3962        let mut triggered = Vec::new();
3963        for sig in &mut self.signals {
3964            if sig.t > prev_t && sig.t <= cur_t && !sig.triggered {
3965                sig.triggered = true;
3966                triggered.push(sig.clone());
3967            }
3968        }
3969        if self.loop_signals && cur_t >= 1.0 {
3970            for sig in &mut self.signals {
3971                sig.triggered = false;
3972            }
3973        }
3974        triggered
3975    }
3976
3977    pub fn reset(&mut self) {
3978        for sig in &mut self.signals {
3979            sig.triggered = false;
3980        }
3981    }
3982}
3983
3984// ============================================================
3985// SPLINE LOD MANAGER
3986// ============================================================
3987
3988#[derive(Clone, Debug)]
3989pub struct SplineLodLevel {
3990    pub max_camera_distance: f32,
3991    pub resolution: usize,       // number of segments for mesh gen
3992    pub show_debug: bool,
3993}
3994
3995#[derive(Clone, Debug)]
3996pub struct SplineLodManager {
3997    pub levels: Vec<SplineLodLevel>,
3998}
3999
4000impl SplineLodManager {
4001    pub fn new() -> Self {
4002        SplineLodManager {
4003            levels: vec![
4004                SplineLodLevel { max_camera_distance: 20.0,  resolution: 128, show_debug: true  },
4005                SplineLodLevel { max_camera_distance: 50.0,  resolution: 64,  show_debug: false },
4006                SplineLodLevel { max_camera_distance: 150.0, resolution: 32,  show_debug: false },
4007                SplineLodLevel { max_camera_distance: f32::MAX, resolution: 16, show_debug: false },
4008            ],
4009        }
4010    }
4011
4012    pub fn select_level(&self, camera_dist: f32) -> &SplineLodLevel {
4013        self.levels.iter()
4014            .find(|l| camera_dist <= l.max_camera_distance)
4015            .unwrap_or(self.levels.last().unwrap())
4016    }
4017
4018    pub fn resolution_at_distance(&self, dist: f32) -> usize {
4019        self.select_level(dist).resolution
4020    }
4021}
4022
4023// ============================================================
4024// TESTING / EXAMPLE USAGE
4025// ============================================================
4026
4027pub fn example_build_roller_coaster() -> SplineEditor {
4028    let mut editor = SplineEditor::new();
4029
4030    // Create a looping roller coaster track
4031    let loop_pts = vec![
4032        Vec3::new(  0.0, 0.0,   0.0),
4033        Vec3::new( 20.0, 5.0,   0.0),
4034        Vec3::new( 40.0,15.0,   0.0),
4035        Vec3::new( 50.0,15.0,  20.0),
4036        Vec3::new( 40.0,25.0,  40.0),
4037        Vec3::new( 20.0,30.0,  40.0),
4038        Vec3::new(  0.0,30.0,  20.0),
4039        Vec3::new(-10.0,15.0,   0.0),
4040        Vec3::new(  0.0, 0.0,   0.0), // back to start
4041    ];
4042    let spline_id = editor.create_catmull_spline(loop_pts, "RollerCoaster");
4043    editor.toggle_closed_spline(spline_id);
4044
4045    // Add a rail track
4046    editor.create_rail_track(spline_id, DEFAULT_RAIL_GAUGE);
4047
4048    // Set mesh section to a rail profile
4049    editor.mesh_section = CrossSection::i_beam(0.15, 0.2, 0.03, 0.02);
4050    editor.mesh_resolution = 128;
4051    editor.generate_lod_mesh(spline_id, 32, 256);
4052
4053    // Add a physics ball
4054    editor.add_constrained_object(spline_id, 0.0, 5.0);
4055
4056    editor
4057}
4058
4059pub fn example_camera_path() -> (SplineEditor, u64) {
4060    let mut editor = SplineEditor::new();
4061
4062    let cam_pts = vec![
4063        Vec3::new( 0.0, 3.0,  10.0),
4064        Vec3::new( 5.0, 4.0,   5.0),
4065        Vec3::new(10.0, 3.5,   0.0),
4066        Vec3::new(10.0, 3.0,  -5.0),
4067        Vec3::new( 5.0, 2.5, -10.0),
4068        Vec3::new( 0.0, 2.0,  -8.0),
4069    ];
4070    let spline_id = editor.create_catmull_spline(cam_pts, "CameraPath");
4071    let rail_id = editor.create_camera_rail(spline_id).unwrap();
4072    (editor, rail_id)
4073}
4074
4075// ============================================================
4076// ADDITIONAL MATH: SPLINE TORSION INTEGRAL (total torsion)
4077// ============================================================
4078
4079pub fn total_torsion(
4080    frenet_fn: &dyn Fn(f32) -> FrenetFrame,
4081    steps: usize,
4082) -> f32 {
4083    let dt = 1.0 / steps as f32;
4084    let mut total = 0.0_f32;
4085    for i in 0..steps {
4086        let t = i as f32 * dt;
4087        let frame = frenet_fn(t + dt * 0.5);
4088        total += frame.torsion.abs() * dt;
4089    }
4090    total
4091}
4092
4093/// Total absolute curvature (integral of |κ| ds)
4094pub fn total_absolute_curvature(
4095    curvature_fn: &dyn Fn(f32) -> f32,
4096    deriv_fn: &dyn Fn(f32) -> Vec3,
4097    steps: usize,
4098) -> f32 {
4099    let dt = 1.0 / steps as f32;
4100    let mut total = 0.0_f32;
4101    for i in 0..steps {
4102        let t = (i as f32 + 0.5) * dt;
4103        let kappa = curvature_fn(t);
4104        let speed = deriv_fn(t).length();
4105        total += kappa * speed * dt;
4106    }
4107    total
4108}
4109
4110/// Turning number of a closed planar curve
4111pub fn turning_number(pts: &[Vec2]) -> i32 {
4112    let n = pts.len();
4113    if n < 3 { return 0; }
4114    let mut angle_sum = 0.0_f32;
4115    for i in 0..n {
4116        let a = pts[i];
4117        let b = pts[(i + 1) % n];
4118        let c = pts[(i + 2) % n];
4119        let ab = b - a;
4120        let bc = c - b;
4121        angle_sum += (ab.x * bc.y - ab.y * bc.x).atan2(ab.dot(bc));
4122    }
4123    (angle_sum / std::f32::consts::TAU).round() as i32
4124}
4125
4126// ============================================================
4127// CURVATURE FLOW (curve shortening flow)
4128// ============================================================
4129
4130pub fn curvature_flow_step(pts: &[Vec3], dt: f32) -> Vec<Vec3> {
4131    let n = pts.len();
4132    if n < 3 { return pts.to_vec(); }
4133    let mut out = pts.to_vec();
4134    for i in 1..n - 1 {
4135        let prev = pts[i - 1];
4136        let cur  = pts[i];
4137        let next = pts[i + 1];
4138        // Discrete Laplacian
4139        let laplacian = prev + next - 2.0 * cur;
4140        out[i] = cur + laplacian * dt;
4141    }
4142    out
4143}
4144
4145pub fn run_curvature_flow(pts: &[Vec3], iterations: usize, dt: f32) -> Vec<Vec3> {
4146    let mut result = pts.to_vec();
4147    for _ in 0..iterations {
4148        result = curvature_flow_step(&result, dt);
4149    }
4150    result
4151}
4152
4153// ============================================================
4154// SPLINE FRAME EXPORT (for use with animation systems)
4155// ============================================================
4156
4157#[derive(Clone, Debug)]
4158pub struct SplineFrameExport {
4159    pub time: f32,
4160    pub position: Vec3,
4161    pub rotation: Quat,
4162    pub tangent: Vec3,
4163    pub curvature: f32,
4164    pub arc_length: f32,
4165}
4166
4167pub fn export_spline_frames(
4168    spline: &CatmullRomSpline,
4169    duration: f32,
4170    fps: f32,
4171    speed: f32,
4172) -> Vec<SplineFrameExport> {
4173    let total_length = spline.total_arc_length();
4174    let n_frames = (duration * fps) as usize + 1;
4175    let mut frames = Vec::with_capacity(n_frames);
4176    for i in 0..n_frames {
4177        let time = i as f32 / fps;
4178        let arc_s = (time * speed).min(total_length);
4179        let t = spline.t_at_arc_length(arc_s);
4180        let pos = spline.evaluate(t);
4181        let tan = safe_normalize(spline.evaluate_derivative(t));
4182        let frame = spline.frenet_frame_at(t);
4183        let rot = Quat::from_mat4(&frame.to_matrix());
4184        frames.push(SplineFrameExport {
4185            time,
4186            position: pos,
4187            rotation: rot,
4188            tangent: tan,
4189            curvature: frame.curvature,
4190            arc_length: arc_s,
4191        });
4192    }
4193    frames
4194}
4195
4196// ============================================================
4197// SPLINE WINDING / HELIX GENERATOR
4198// ============================================================
4199
4200pub fn generate_helix(
4201    center: Vec3,
4202    radius: f32,
4203    pitch: f32,       // height per revolution
4204    turns: f32,
4205    n_pts: usize,
4206) -> Vec<Vec3> {
4207    (0..n_pts).map(|i| {
4208        let t = i as f32 / (n_pts - 1).max(1) as f32;
4209        let angle = t * turns * std::f32::consts::TAU;
4210        Vec3::new(
4211            center.x + angle.cos() * radius,
4212            center.y + t * turns * pitch,
4213            center.z + angle.sin() * radius,
4214        )
4215    }).collect()
4216}
4217
4218pub fn generate_toroidal_helix(
4219    big_radius: f32,
4220    small_radius: f32,
4221    p: u32, // wraps p times around big axis
4222    q: u32, // wraps q times around small axis
4223    n_pts: usize,
4224) -> Vec<Vec3> {
4225    (0..n_pts).map(|i| {
4226        let t = i as f32 / (n_pts - 1).max(1) as f32 * std::f32::consts::TAU;
4227        let phi = t * p as f32;
4228        let theta = t * q as f32;
4229        let r = big_radius + small_radius * theta.cos();
4230        Vec3::new(
4231            r * phi.cos(),
4232            small_radius * theta.sin(),
4233            r * phi.sin(),
4234        )
4235    }).collect()
4236}
4237
4238// ============================================================
4239// ARC LENGTH INTEGRATION TESTS (internal verification)
4240// ============================================================
4241
4242fn verify_arc_length_integration() -> bool {
4243    // A circle of radius r should have arc length 2πr
4244    let r = 5.0_f32;
4245    let circle_pos = |t: f32| Vec3::new(
4246        r * (t * std::f32::consts::TAU).cos(),
4247        0.0,
4248        r * (t * std::f32::consts::TAU).sin(),
4249    );
4250    let table = build_arc_length_table(1024, &circle_pos);
4251    let measured = table.last().map(|e| e.1).unwrap_or(0.0);
4252    let expected = std::f32::consts::TAU * r;
4253    (measured - expected).abs() < 0.01 * expected // within 1%
4254}
4255
4256// ============================================================
4257// PARALLEL TRANSPORT CHAIN: whole-spline PT frame sequence
4258// ============================================================
4259
4260pub fn build_parallel_transport_frames(
4261    pos_fn:  &dyn Fn(f32) -> Vec3,
4262    tang_fn: &dyn Fn(f32) -> Vec3,
4263    steps:   usize,
4264) -> Vec<ParallelTransportFrame> {
4265    let mut frames = Vec::with_capacity(steps + 1);
4266    let p0 = pos_fn(0.0);
4267    let t0 = tang_fn(0.0);
4268    frames.push(ParallelTransportFrame::initial(p0, t0));
4269    for i in 1..=steps {
4270        let t = i as f32 / steps as f32;
4271        let p = pos_fn(t);
4272        let tang = safe_normalize(tang_fn(t));
4273        let prev = frames.last().unwrap().clone();
4274        frames.push(ParallelTransportFrame::transport(&prev, p, tang));
4275    }
4276    frames
4277}
4278
4279// ============================================================
4280// KNOT VECTOR UTILITIES
4281// ============================================================
4282
4283pub fn knot_vector_uniform(n: usize, k: usize) -> Vec<f32> {
4284    let m = n + k + 1;
4285    (0..m).map(|i| i as f32 / (m - 1) as f32).collect()
4286}
4287
4288pub fn knot_vector_clamped(n: usize, k: usize) -> Vec<f32> {
4289    let m = n + k + 1;
4290    let mut v = Vec::with_capacity(m);
4291    for i in 0..m {
4292        if i < k + 1 { v.push(0.0); }
4293        else if i > n { v.push(1.0); }
4294        else { v.push((i - k) as f32 / (n - k) as f32); }
4295    }
4296    v
4297}
4298
4299pub fn knot_vector_periodic(n: usize, k: usize) -> Vec<f32> {
4300    let m = n + k + 1;
4301    (0..m).map(|i| (i as f32 - k as f32) / (n - k + 1) as f32).collect()
4302}
4303
4304// ============================================================
4305// WHOLE-EDITOR UPDATE TICK
4306// ============================================================
4307
4308impl SplineEditor {
4309    pub fn update(&mut self, dt: f32) {
4310        self.update_physics(dt);
4311        for chain in &mut self.chains {
4312            chain.update_offset(dt * 0.5);
4313        }
4314        if self.show_debug {
4315            self.update_debug_viz();
4316        }
4317    }
4318
4319    pub fn stats(&self) -> SplineEditorStats {
4320        let total_verts: usize = self.generated_meshes.values()
4321            .map(|m| m.vertex_count()).sum();
4322        let total_tris: usize = self.generated_meshes.values()
4323            .map(|m| m.triangle_count()).sum();
4324        SplineEditorStats {
4325            spline_count:  self.spline_count(),
4326            rail_count:    self.rail_tracks.len(),
4327            camera_rail_count: self.camera_rails.len(),
4328            constrained_objects: self.constrained_objects.len(),
4329            chain_count:   self.chains.len(),
4330            mesh_count:    self.generated_meshes.len(),
4331            total_vertices: total_verts,
4332            total_triangles: total_tris,
4333        }
4334    }
4335}
4336
4337#[derive(Clone, Debug)]
4338pub struct SplineEditorStats {
4339    pub spline_count: usize,
4340    pub rail_count: usize,
4341    pub camera_rail_count: usize,
4342    pub constrained_objects: usize,
4343    pub chain_count: usize,
4344    pub mesh_count: usize,
4345    pub total_vertices: usize,
4346    pub total_triangles: usize,
4347}
4348
4349// ============================================================
4350// MULTI-SEGMENT BSPLINE FITTING
4351// ============================================================
4352
4353pub struct BSplineFitter {
4354    pub degree: usize,
4355    pub max_control_points: usize,
4356    pub tolerance: f32,
4357}
4358
4359impl BSplineFitter {
4360    pub fn new(degree: usize, tolerance: f32) -> Self {
4361        BSplineFitter { degree, max_control_points: 32, tolerance }
4362    }
4363
4364    pub fn fit(&self, pts: &[Vec3]) -> BSpline {
4365        let n = pts.len().min(self.max_control_points);
4366        // Use Greville abscissae for chord-length parameterized fit
4367        let params = chord_length_params(pts);
4368        let mut cps = Vec::with_capacity(n);
4369        // Simple approach: select n control points spaced evenly in parameter
4370        for i in 0..n {
4371            let t = i as f32 / (n - 1).max(1) as f32;
4372            let idx = (t * (pts.len() - 1) as f32) as usize;
4373            cps.push(pts[idx.min(pts.len() - 1)]);
4374        }
4375        let mut spline = BSpline::new(cps, self.degree, false);
4376        // Iterative refinement: move control points to minimize least-squares error
4377        for _iter in 0..self.max_control_points {
4378            let mut error = 0.0_f32;
4379            for (&t, &p) in params.iter().zip(pts.iter()) {
4380                let q = spline.evaluate(t);
4381                error += (q - p).length_squared();
4382            }
4383            if error.sqrt() < self.tolerance { break; }
4384            // Gradient descent step
4385            let n_cps = spline.control_points.len();
4386            for (idx, cp) in spline.control_points.iter_mut().enumerate() {
4387                let cp_t = idx as f32 / (n_cps - 1).max(1) as f32;
4388                let nearby: Vec3 = params.iter().zip(pts.iter())
4389                    .filter(|(&t, _)| (t - cp_t).abs() < 0.1)
4390                    .map(|(_, &p)| p)
4391                    .fold(Vec3::ZERO, |a, b| a + b);
4392                let count = params.iter()
4393                    .filter(|&&t| (t - cp_t).abs() < 0.1)
4394                    .count();
4395                if count > 0 {
4396                    let target = nearby / count as f32;
4397                    *cp = lerp_vec3(*cp, target, 0.1);
4398                }
4399            }
4400            spline.rebuild_arc_length_table();
4401        }
4402        spline
4403    }
4404}
4405
4406// ============================================================
4407// SURFACE OF REVOLUTION ALONG SPLINE
4408// ============================================================
4409
4410pub fn surface_of_revolution(
4411    profile_pts: &[Vec2],  // 2D profile in (r, z) space
4412    axis: Vec3,
4413    n_revolutions: usize,
4414) -> SplineMesh {
4415    let n_profile = profile_pts.len();
4416    let n_angular  = n_revolutions;
4417    let mut mesh   = SplineMesh::new();
4418
4419    for j in 0..=n_angular {
4420        let angle = j as f32 / n_angular as f32 * std::f32::consts::TAU;
4421        let cos_a = angle.cos();
4422        let sin_a = angle.sin();
4423        for (i, &pt) in profile_pts.iter().enumerate() {
4424            let r = pt.x;
4425            let z = pt.y;
4426            // Rotate r around axis
4427            let right = safe_normalize(axis.cross(Vec3::Y));
4428            let up    = safe_normalize(axis.cross(right));
4429            let world = axis * z + right * (r * cos_a) + up * (r * sin_a);
4430            let normal = safe_normalize(right * cos_a + up * sin_a);
4431            let u = j as f32 / n_angular as f32;
4432            let v = i as f32 / (n_profile - 1).max(1) as f32;
4433            mesh.vertices.push(world);
4434            mesh.normals.push(normal);
4435            mesh.uvs.push(Vec2::new(u, v));
4436            mesh.tangents.push(axis);
4437        }
4438    }
4439
4440    for j in 0..n_angular {
4441        for i in 0..n_profile.saturating_sub(1) {
4442            let a = (j * n_profile + i) as u32;
4443            let b = (j * n_profile + i + 1) as u32;
4444            let c = ((j + 1) * n_profile + i) as u32;
4445            let d = ((j + 1) * n_profile + i + 1) as u32;
4446            mesh.indices.extend_from_slice(&[a, b, c, b, d, c]);
4447        }
4448    }
4449
4450    mesh
4451}
4452
4453// ============================================================
4454// SPLINE ANIMATION SAMPLING (baking to keyframes)
4455// ============================================================
4456
4457#[derive(Clone, Debug)]
4458pub struct BakedSplineAnimation {
4459    pub positions: Vec<Vec3>,
4460    pub rotations: Vec<Quat>,
4461    pub times:     Vec<f32>,
4462    pub fps:       f32,
4463}
4464
4465impl BakedSplineAnimation {
4466    pub fn bake(spline: &CatmullRomSpline, fps: f32, duration: f32, speed: f32) -> Self {
4467        let frames = export_spline_frames(spline, duration, fps, speed);
4468        BakedSplineAnimation {
4469            positions: frames.iter().map(|f| f.position).collect(),
4470            rotations: frames.iter().map(|f| f.rotation).collect(),
4471            times:     frames.iter().map(|f| f.time).collect(),
4472            fps,
4473        }
4474    }
4475
4476    pub fn sample_position(&self, time: f32) -> Vec3 {
4477        if self.times.is_empty() { return Vec3::ZERO; }
4478        let idx = self.times.partition_point(|&t| t <= time);
4479        if idx == 0 { return self.positions[0]; }
4480        if idx >= self.positions.len() { return *self.positions.last().unwrap(); }
4481        let t0 = self.times[idx - 1];
4482        let t1 = self.times[idx];
4483        let frac = if (t1 - t0).abs() < EPSILON { 0.0 } else { (time - t0) / (t1 - t0) };
4484        lerp_vec3(self.positions[idx - 1], self.positions[idx], frac)
4485    }
4486
4487    pub fn sample_rotation(&self, time: f32) -> Quat {
4488        if self.times.is_empty() { return Quat::IDENTITY; }
4489        let idx = self.times.partition_point(|&t| t <= time);
4490        if idx == 0 { return self.rotations[0]; }
4491        if idx >= self.rotations.len() { return *self.rotations.last().unwrap(); }
4492        let t0 = self.times[idx - 1];
4493        let t1 = self.times[idx];
4494        let frac = if (t1 - t0).abs() < EPSILON { 0.0 } else { (time - t0) / (t1 - t0) };
4495        self.rotations[idx - 1].slerp(self.rotations[idx], frac)
4496    }
4497}
4498
4499// ============================================================
4500// SIGNED DISTANCE FIELD ALONG SPLINE (capsule approximation)
4501// ============================================================
4502
4503pub fn sdf_spline_capsule(
4504    point: Vec3,
4505    pos_fn: &dyn Fn(f32) -> Vec3,
4506    total_length: f32,
4507    radius: f32,
4508    steps: usize,
4509) -> f32 {
4510    let mut min_dist = f32::MAX;
4511    let mut prev = pos_fn(0.0);
4512    for i in 1..=steps {
4513        let t = i as f32 / steps as f32;
4514        let cur = pos_fn(t);
4515        // SDF to segment
4516        let seg = cur - prev;
4517        let seg_len_sq = seg.length_squared();
4518        let t_seg = if seg_len_sq < EPSILON { 0.0 }
4519                    else { ((point - prev).dot(seg) / seg_len_sq).clamp(0.0, 1.0) };
4520        let closest = prev + seg * t_seg;
4521        let dist = (point - closest).length() - radius;
4522        if dist < min_dist { min_dist = dist; }
4523        prev = cur;
4524    }
4525    min_dist
4526}
4527
4528// ============================================================
4529// SPLINE GROUNDING (project spline onto terrain heightmap)
4530// ============================================================
4531
4532pub fn ground_spline_to_terrain(
4533    pts: &mut [Vec3],
4534    height_fn: &dyn Fn(f32, f32) -> f32,
4535    offset: f32,
4536) {
4537    for p in pts.iter_mut() {
4538        let ground = height_fn(p.x, p.z);
4539        p.y = p.y.max(ground + offset);
4540    }
4541}
4542
4543// ============================================================
4544// UNIT TEST STUBS
4545// ============================================================
4546
4547#[cfg(test)]
4548mod tests {
4549    use super::*;
4550
4551    #[test]
4552    fn test_catmull_rom_endpoints() {
4553        let pts = vec![
4554            Vec3::new(0.0, 0.0, 0.0),
4555            Vec3::new(1.0, 0.0, 0.0),
4556            Vec3::new(2.0, 0.0, 0.0),
4557            Vec3::new(3.0, 0.0, 0.0),
4558        ];
4559        let s = CatmullRomSpline::new(pts, 0.5, false);
4560        let start = s.evaluate(0.0);
4561        let end   = s.evaluate(1.0);
4562        assert!((start.x - 0.0).abs() < 0.1, "Start x should be near 0");
4563        assert!((end.x   - 3.0).abs() < 0.1, "End x should be near 3");
4564    }
4565
4566    #[test]
4567    fn test_bezier_de_casteljau_endpoints() {
4568        let p0 = Vec3::new(0.0, 0.0, 0.0);
4569        let p1 = Vec3::new(1.0, 2.0, 0.0);
4570        let p2 = Vec3::new(2.0, 2.0, 0.0);
4571        let p3 = Vec3::new(3.0, 0.0, 0.0);
4572        let at0 = CubicBezierSpline::de_casteljau(p0, p1, p2, p3, 0.0);
4573        let at1 = CubicBezierSpline::de_casteljau(p0, p1, p2, p3, 1.0);
4574        assert!((at0 - p0).length() < EPSILON);
4575        assert!((at1 - p3).length() < EPSILON);
4576    }
4577
4578    #[test]
4579    fn test_arc_length_circle() {
4580        assert!(verify_arc_length_integration(), "Circle arc length should be within 1%");
4581    }
4582
4583    #[test]
4584    fn test_arc_length_inverse() {
4585        let pts = vec![
4586            Vec3::new(0.0, 0.0, 0.0),
4587            Vec3::new(3.0, 4.0, 0.0), // length 5
4588        ];
4589        let s = CatmullRomSpline::new(pts, 0.5, false);
4590        let total = s.total_arc_length();
4591        let t_half = s.t_at_arc_length(total * 0.5);
4592        assert!((t_half - 0.5).abs() < 0.05, "Midpoint should be near t=0.5");
4593    }
4594
4595    #[test]
4596    fn test_bspline_partition_of_unity() {
4597        let pts: Vec<Vec3> = (0..6).map(|i| Vec3::new(i as f32, 0.0, 0.0)).collect();
4598        let s = BSpline::new(pts, 3, false);
4599        // Sum of basis functions should be ~1 everywhere
4600        for j in 0..10 {
4601            let t = 0.05 + j as f32 * 0.09;
4602            let sum: f32 = (0..s.control_points.len())
4603                .map(|i| s.basis(i, s.degree, t))
4604                .sum();
4605            assert!((sum - 1.0).abs() < 0.01, "B-spline partition of unity failed at t={}", t);
4606        }
4607    }
4608
4609    #[test]
4610    fn test_frenet_frame_orthonormality() {
4611        let pts = vec![
4612            Vec3::new(0.0, 0.0, 0.0),
4613            Vec3::new(1.0, 0.5, 0.0),
4614            Vec3::new(2.0, 0.0, 0.5),
4615            Vec3::new(3.0, 0.0, 0.0),
4616        ];
4617        let s = CatmullRomSpline::new(pts, 0.5, false);
4618        for i in 1..9 {
4619            let t = i as f32 / 9.0;
4620            let frame = s.frenet_frame_at(t);
4621            let tt = frame.tangent.dot(frame.tangent);
4622            let nn = frame.normal.dot(frame.normal);
4623            let tn = frame.tangent.dot(frame.normal);
4624            assert!((tt - 1.0).abs() < 0.01, "Tangent not unit");
4625            assert!((nn - 1.0).abs() < 0.01, "Normal not unit");
4626            assert!(tn.abs() < 0.01, "T·N not zero");
4627        }
4628    }
4629
4630    #[test]
4631    fn test_undo_redo() {
4632        let mut editor = SplineEditor::new();
4633        let id = editor.create_catmull_spline(
4634            vec![Vec3::ZERO, Vec3::X, Vec3::X + Vec3::Y],
4635            "Test"
4636        );
4637        editor.move_control_point(id, 0, Vec3::new(1.0, 0.0, 0.0));
4638        let pos_after = editor.catmull_splines[&id].control_points[0].position;
4639        assert!((pos_after.x - 1.0).abs() < EPSILON);
4640        editor.undo();
4641        let pos_undone = editor.catmull_splines[&id].control_points[0].position;
4642        assert!(pos_undone.x.abs() < EPSILON, "Undo should restore position");
4643    }
4644
4645    #[test]
4646    fn test_hermite_tangent_continuity() {
4647        let pts = vec![
4648            (Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
4649            (Vec3::new(2.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
4650            (Vec3::new(4.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
4651        ];
4652        let s = HermiteSpline::new(pts);
4653        // Check derivative at segment boundary matches specified tangent
4654        let d = s.eval_segment_derivative(0, 1.0);
4655        // Should be proportional to the end tangent
4656        assert!(d.length() > EPSILON, "Derivative at boundary should be non-zero");
4657    }
4658}
4659
4660// ============================================================
4661// SPLINE WARP DEFORMER
4662// ============================================================
4663
4664#[derive(Clone, Debug)]
4665pub struct SplineWarpDeformer {
4666    pub axis_spline_id:  u64,
4667    pub falloff_curve:   Vec<(f32, f32)>,
4668    pub world_up:        Vec3,
4669}
4670
4671impl SplineWarpDeformer {
4672    pub fn new(axis_spline_id: u64) -> Self {
4673        SplineWarpDeformer {
4674            axis_spline_id,
4675            falloff_curve: vec![(0.0, 1.0), (1.0, 0.0)],
4676            world_up: Vec3::Y,
4677        }
4678    }
4679
4680    pub fn falloff_at(&self, dist: f32) -> f32 {
4681        let n = self.falloff_curve.len();
4682        if n == 0 { return 1.0; }
4683        if n == 1 { return self.falloff_curve[0].1; }
4684        let idx = self.falloff_curve.partition_point(|&(d, _)| d <= dist);
4685        if idx == 0 { return self.falloff_curve[0].1; }
4686        if idx >= n { return self.falloff_curve[n-1].1; }
4687        let (d0, w0) = self.falloff_curve[idx-1];
4688        let (d1, w1) = self.falloff_curve[idx];
4689        let frac = if (d1 - d0).abs() < EPSILON { 0.0 } else { (dist - d0) / (d1 - d0) };
4690        lerp(w0, w1, frac)
4691    }
4692
4693    pub fn warp_point(&self, point: Vec3, spline: &CatmullRomSpline, radius: f32) -> Vec3 {
4694        let (t, closest) = spline.nearest_point(point);
4695        let dist = (point - closest).length();
4696        if dist > radius { return point; }
4697        let weight = self.falloff_at(dist / radius.max(EPSILON));
4698        let frame  = spline.frenet_frame_at(t);
4699        let local   = point - closest;
4700        let local_n = local.dot(frame.normal);
4701        let local_b = local.dot(frame.binormal);
4702        let twist_angle = frame.torsion * weight * 0.1;
4703        let cos_t = twist_angle.cos();
4704        let sin_t = twist_angle.sin();
4705        let new_n = local_n * cos_t - local_b * sin_t;
4706        let new_b = local_n * sin_t + local_b * cos_t;
4707        let warped_local = frame.normal * new_n + frame.binormal * new_b;
4708        lerp_vec3(point, closest + warped_local, weight)
4709    }
4710
4711    pub fn warp_mesh(&self, verts: &mut [Vec3], spline: &CatmullRomSpline, radius: f32) {
4712        for v in verts.iter_mut() {
4713            *v = self.warp_point(*v, spline, radius);
4714        }
4715    }
4716}
4717
4718// ============================================================
4719// ROAD PROFILE
4720// ============================================================
4721
4722#[derive(Clone, Debug)]
4723pub struct RoadProfile {
4724    pub lane_width:      f32,
4725    pub lane_count:      u32,
4726    pub shoulder_width:  f32,
4727    pub curb_height:     f32,
4728    pub median_width:    f32,
4729    pub has_sidewalk:    bool,
4730    pub sidewalk_width:  f32,
4731    pub sidewalk_height: f32,
4732}
4733
4734impl RoadProfile {
4735    pub fn two_lane_road() -> Self {
4736        RoadProfile {
4737            lane_width: 3.7, lane_count: 2, shoulder_width: 1.2,
4738            curb_height: 0.15, median_width: 0.0,
4739            has_sidewalk: true, sidewalk_width: 2.0, sidewalk_height: 0.15,
4740        }
4741    }
4742
4743    pub fn highway() -> Self {
4744        RoadProfile {
4745            lane_width: 3.7, lane_count: 6, shoulder_width: 3.0,
4746            curb_height: 0.0, median_width: 4.0,
4747            has_sidewalk: false, sidewalk_width: 0.0, sidewalk_height: 0.0,
4748        }
4749    }
4750
4751    pub fn total_width(&self) -> f32 {
4752        self.lane_width * self.lane_count as f32
4753            + self.shoulder_width * 2.0
4754            + self.median_width
4755            + if self.has_sidewalk { self.sidewalk_width * 2.0 } else { 0.0 }
4756    }
4757
4758    pub fn generate_cross_section(&self) -> CrossSection {
4759        let hw = self.total_width() * 0.5;
4760        let road_hw = (self.lane_width * self.lane_count as f32 * 0.5) + self.shoulder_width;
4761        let mut pts = Vec::new();
4762        pts.push(Vec2::new(-hw, 0.0));
4763        if self.has_sidewalk {
4764            pts.push(Vec2::new(-hw, self.sidewalk_height));
4765            pts.push(Vec2::new(-road_hw - self.sidewalk_width, self.sidewalk_height));
4766        }
4767        pts.push(Vec2::new(-road_hw, self.curb_height));
4768        pts.push(Vec2::new(-road_hw, 0.0));
4769        pts.push(Vec2::new( road_hw, 0.0));
4770        pts.push(Vec2::new( road_hw, self.curb_height));
4771        if self.has_sidewalk {
4772            pts.push(Vec2::new(road_hw + self.sidewalk_width, self.sidewalk_height));
4773            pts.push(Vec2::new(hw, self.sidewalk_height));
4774        }
4775        pts.push(Vec2::new(hw, 0.0));
4776        CrossSection { points: pts, closed: false }
4777    }
4778}
4779
4780// ============================================================
4781// ROAD MARKING
4782// ============================================================
4783
4784#[derive(Clone, Debug, PartialEq)]
4785pub enum RoadMarkingKind {
4786    Solid, Dashed, DoubleSolid, StopLine, Crosswalk,
4787}
4788
4789#[derive(Clone, Debug)]
4790pub struct RoadMarking {
4791    pub kind:     RoadMarkingKind,
4792    pub offset:   f32,
4793    pub t_start:  f32,
4794    pub t_end:    f32,
4795    pub dash_len: f32,
4796    pub dash_gap: f32,
4797    pub color:    Vec4,
4798}
4799
4800#[derive(Clone, Debug)]
4801pub struct RoadSegment {
4802    pub spline_id: u64,
4803    pub profile:   RoadProfile,
4804    pub mesh_id:   Option<u64>,
4805    pub markings:  Vec<RoadMarking>,
4806}
4807
4808impl RoadSegment {
4809    pub fn new(spline_id: u64, profile: RoadProfile) -> Self {
4810        RoadSegment { spline_id, profile, mesh_id: None, markings: Vec::new() }
4811    }
4812
4813    pub fn add_center_line(&mut self) {
4814        self.markings.push(RoadMarking {
4815            kind: RoadMarkingKind::Dashed, offset: 0.0,
4816            t_start: 0.0, t_end: 1.0, dash_len: 3.0, dash_gap: 9.0,
4817            color: Vec4::new(1.0, 1.0, 0.0, 1.0),
4818        });
4819    }
4820
4821    pub fn add_edge_lines(&mut self) {
4822        let hw = (self.profile.lane_width * self.profile.lane_count as f32 * 0.5) + self.profile.shoulder_width;
4823        for &side in &[-hw, hw] {
4824            self.markings.push(RoadMarking {
4825                kind: RoadMarkingKind::Solid, offset: side,
4826                t_start: 0.0, t_end: 1.0, dash_len: 0.0, dash_gap: 0.0,
4827                color: Vec4::new(1.0, 1.0, 1.0, 1.0),
4828            });
4829        }
4830    }
4831
4832    pub fn marking_line_segments(&self, marking_idx: usize, spline: &CatmullRomSpline) -> Vec<(Vec3, Vec3)> {
4833        let m = &self.markings[marking_idx];
4834        let total = spline.total_arc_length();
4835        let mut result = Vec::new();
4836        match m.kind {
4837            RoadMarkingKind::Solid | RoadMarkingKind::DoubleSolid => {
4838                let steps = 64usize;
4839                for i in 0..steps {
4840                    let t0 = lerp(m.t_start, m.t_end, i as f32 / steps as f32);
4841                    let t1 = lerp(m.t_start, m.t_end, (i+1) as f32 / steps as f32);
4842                    let f0 = spline.frenet_frame_at(t0);
4843                    let f1 = spline.frenet_frame_at(t1);
4844                    result.push((f0.position + f0.normal * m.offset, f1.position + f1.normal * m.offset));
4845                }
4846            }
4847            RoadMarkingKind::Dashed => {
4848                let cycle = m.dash_len + m.dash_gap;
4849                let mut s = m.t_start * total;
4850                let s_end = m.t_end * total;
4851                while s < s_end {
4852                    let s_end_dash = (s + m.dash_len).min(s_end);
4853                    let t0 = spline.t_at_arc_length(s);
4854                    let t1 = spline.t_at_arc_length(s_end_dash);
4855                    let steps = 8usize;
4856                    for i in 0..steps {
4857                        let ta = lerp(t0, t1, i as f32 / steps as f32);
4858                        let tb = lerp(t0, t1, (i+1) as f32 / steps as f32);
4859                        let fa = spline.frenet_frame_at(ta);
4860                        let fb = spline.frenet_frame_at(tb);
4861                        result.push((fa.position + fa.normal * m.offset, fb.position + fb.normal * m.offset));
4862                    }
4863                    s += cycle;
4864                }
4865            }
4866            _ => {}
4867        }
4868        result
4869    }
4870}
4871
4872// ============================================================
4873// SPLINE INTERSECTION GRAPH
4874// ============================================================
4875
4876#[derive(Clone, Debug)]
4877pub struct IntersectionPoint {
4878    pub position:    Vec3,
4879    pub spline_ids:  Vec<u64>,
4880    pub t_values:    Vec<f32>,
4881    pub is_junction: bool,
4882}
4883
4884#[derive(Clone, Debug)]
4885pub struct SplineIntersectionGraph {
4886    pub intersections: Vec<IntersectionPoint>,
4887}
4888
4889impl SplineIntersectionGraph {
4890    pub fn new() -> Self { SplineIntersectionGraph { intersections: Vec::new() } }
4891
4892    pub fn compute_all(splines: &HashMap<u64, CatmullRomSpline>, tol: f32) -> Self {
4893        let mut graph = Self::new();
4894        let ids: Vec<u64> = splines.keys().cloned().collect();
4895        for i in 0..ids.len() {
4896            for j in i+1..ids.len() {
4897                let sa = &splines[&ids[i]];
4898                let sb = &splines[&ids[j]];
4899                let hits = intersect_spline_spline(
4900                    &|t| sa.evaluate(t),
4901                    &|t| sb.evaluate(t),
4902                    24, tol,
4903                );
4904                for hit in hits {
4905                    graph.intersections.push(IntersectionPoint {
4906                        position:   hit.point_a,
4907                        spline_ids: vec![ids[i], ids[j]],
4908                        t_values:   vec![hit.t_a, hit.t_b],
4909                        is_junction: true,
4910                    });
4911                }
4912            }
4913        }
4914        graph
4915    }
4916
4917    pub fn junctions_near(&self, pos: Vec3, radius: f32) -> Vec<&IntersectionPoint> {
4918        self.intersections.iter()
4919            .filter(|p| (p.position - pos).length() <= radius)
4920            .collect()
4921    }
4922}
4923
4924// ============================================================
4925// VOLUMETRIC SPLINE REGION
4926// ============================================================
4927
4928#[derive(Clone, Debug)]
4929pub struct SplineVolume {
4930    pub spline_id:   u64,
4931    pub radius:      f32,
4932    pub taper_start: f32,
4933    pub taper_end:   f32,
4934}
4935
4936impl SplineVolume {
4937    pub fn new(spline_id: u64, radius: f32) -> Self {
4938        SplineVolume { spline_id, radius, taper_start: 1.0, taper_end: 1.0 }
4939    }
4940
4941    pub fn radius_at(&self, t: f32) -> f32 {
4942        self.radius * lerp(self.taper_start, self.taper_end, t)
4943    }
4944
4945    pub fn contains(&self, point: Vec3, spline: &CatmullRomSpline) -> bool {
4946        let (t, closest) = spline.nearest_point(point);
4947        (point - closest).length() <= self.radius_at(t)
4948    }
4949
4950    pub fn density_at(&self, point: Vec3, spline: &CatmullRomSpline) -> f32 {
4951        let (t, closest) = spline.nearest_point(point);
4952        let r = self.radius_at(t);
4953        let dist = (point - closest).length();
4954        if dist >= r { 0.0 } else { 1.0 - dist / r }
4955    }
4956
4957    pub fn surface_sdf(&self, point: Vec3, spline: &CatmullRomSpline) -> f32 {
4958        let (t, closest) = spline.nearest_point(point);
4959        let r = self.radius_at(t);
4960        (point - closest).length() - r
4961    }
4962}
4963
4964// ============================================================
4965// SPLINE GRADIENT (color along spline)
4966// ============================================================
4967
4968#[derive(Clone, Debug)]
4969pub struct SplineGradient {
4970    pub stops: Vec<(f32, Vec4)>,
4971}
4972
4973impl SplineGradient {
4974    pub fn new() -> Self { SplineGradient { stops: Vec::new() } }
4975
4976    pub fn add_stop(mut self, t: f32, color: Vec4) -> Self {
4977        let idx = self.stops.partition_point(|s| s.0 < t);
4978        self.stops.insert(idx, (t, color));
4979        self
4980    }
4981
4982    pub fn evaluate(&self, t: f32) -> Vec4 {
4983        let n = self.stops.len();
4984        if n == 0 { return Vec4::ONE; }
4985        if n == 1 { return self.stops[0].1; }
4986        let idx = self.stops.partition_point(|s| s.0 <= t);
4987        if idx == 0 { return self.stops[0].1; }
4988        if idx >= n { return self.stops[n-1].1; }
4989        let (t0, c0) = self.stops[idx-1];
4990        let (t1, c1) = self.stops[idx];
4991        let frac = if (t1 - t0).abs() < EPSILON { 0.0 } else { (t - t0) / (t1 - t0) };
4992        Self::lerp_vec4(c0, c1, frac)
4993    }
4994
4995    fn lerp_vec4(a: Vec4, b: Vec4, t: f32) -> Vec4 { a + (b - a) * t }
4996
4997    pub fn rainbow() -> Self {
4998        SplineGradient::new()
4999            .add_stop(0.0,   Vec4::new(1.0, 0.0, 0.0, 1.0))
5000            .add_stop(0.166, Vec4::new(1.0, 0.5, 0.0, 1.0))
5001            .add_stop(0.333, Vec4::new(1.0, 1.0, 0.0, 1.0))
5002            .add_stop(0.5,   Vec4::new(0.0, 1.0, 0.0, 1.0))
5003            .add_stop(0.666, Vec4::new(0.0, 0.0, 1.0, 1.0))
5004            .add_stop(0.833, Vec4::new(0.5, 0.0, 1.0, 1.0))
5005            .add_stop(1.0,   Vec4::new(1.0, 0.0, 1.0, 1.0))
5006    }
5007}
5008
5009// ============================================================
5010// ANIMATED SPLINE
5011// ============================================================
5012
5013#[derive(Clone, Debug)]
5014pub struct AnimatedSplineKeyframe {
5015    pub time:           f32,
5016    pub control_points: Vec<Vec3>,
5017}
5018
5019#[derive(Clone, Debug)]
5020pub struct AnimatedSpline {
5021    pub spline_id: u64,
5022    pub keyframes: Vec<AnimatedSplineKeyframe>,
5023    pub loop_anim: bool,
5024    pub duration:  f32,
5025}
5026
5027impl AnimatedSpline {
5028    pub fn new(spline_id: u64, duration: f32) -> Self {
5029        AnimatedSpline { spline_id, keyframes: Vec::new(), loop_anim: true, duration }
5030    }
5031
5032    pub fn add_keyframe(&mut self, time: f32, points: Vec<Vec3>) {
5033        let idx = self.keyframes.partition_point(|k| k.time < time);
5034        self.keyframes.insert(idx, AnimatedSplineKeyframe { time, control_points: points });
5035    }
5036
5037    pub fn evaluate_points(&self, time: f32) -> Option<Vec<Vec3>> {
5038        let t = if self.loop_anim { time % self.duration.max(EPSILON) } else { time.min(self.duration) };
5039        let n = self.keyframes.len();
5040        if n == 0 { return None; }
5041        if n == 1 { return Some(self.keyframes[0].control_points.clone()); }
5042        let idx = self.keyframes.partition_point(|k| k.time <= t);
5043        let k0 = &self.keyframes[(idx.saturating_sub(1)).min(n-1)];
5044        let k1 = &self.keyframes[idx.min(n-1)];
5045        let dt = k1.time - k0.time;
5046        let frac = if dt.abs() < EPSILON { 0.0 } else { (t - k0.time) / dt };
5047        let n_pts = k0.control_points.len().min(k1.control_points.len());
5048        Some((0..n_pts).map(|i| lerp_vec3(k0.control_points[i], k1.control_points[i], frac)).collect())
5049    }
5050
5051    pub fn apply(&self, time: f32, spline: &mut CatmullRomSpline) {
5052        if let Some(pts) = self.evaluate_points(time) {
5053            for (i, pt) in pts.iter().enumerate() {
5054                if i < spline.control_points.len() {
5055                    spline.control_points[i].position = *pt;
5056                }
5057            }
5058            spline.rebuild_arc_length_table();
5059        }
5060    }
5061}
5062
5063// ============================================================
5064// CATENARY CURVE
5065// ============================================================
5066
5067pub struct Catenary {
5068    pub anchor_a: Vec3,
5069    pub anchor_b: Vec3,
5070    pub slack:    f32,
5071}
5072
5073impl Catenary {
5074    pub fn new(a: Vec3, b: Vec3, slack: f32) -> Self {
5075        Catenary { anchor_a: a, anchor_b: b, slack }
5076    }
5077
5078    pub fn evaluate(&self, t: f32) -> Vec3 {
5079        let dir = self.anchor_b - self.anchor_a;
5080        let horiz = Vec2::new(dir.x, dir.z).length();
5081        let vert  = dir.y;
5082        let chain_len = horiz + self.slack;
5083        let a = Self::solve_a(horiz, vert, chain_len);
5084        let x_offset = -horiz * 0.5;
5085        let x = x_offset + t * horiz;
5086        let y0 = a * (x_offset / a).cosh();
5087        let y  = a * (x / a).cosh() - y0;
5088        let horiz_dir = if horiz > EPSILON {
5089            Vec3::new(dir.x, 0.0, dir.z) / horiz
5090        } else { Vec3::X };
5091        self.anchor_a + horiz_dir * (t * horiz) + Vec3::Y * (y + vert * t - self.slack * 0.3)
5092    }
5093
5094    fn solve_a(h: f32, v: f32, l: f32) -> f32 {
5095        let target = (l * l - v * v).max(0.0);
5096        let mut a = h.max(EPSILON);
5097        for _ in 0..64 {
5098            let s  = 2.0 * a * (h / (2.0 * a)).sinh();
5099            let err = s * s - target;
5100            let ds  = 2.0 * (h / (2.0 * a)).sinh() - (h / a) * (h / (2.0 * a)).cosh();
5101            let d   = 2.0 * s * ds;
5102            if d.abs() < EPSILON { break; }
5103            a -= err / d;
5104            a = a.max(EPSILON);
5105        }
5106        a
5107    }
5108
5109    pub fn to_polyline(&self, steps: usize) -> Vec<Vec3> {
5110        (0..=steps).map(|i| self.evaluate(i as f32 / steps as f32)).collect()
5111    }
5112}
5113
5114// ============================================================
5115// SPLINE ELEVATOR PROFILE
5116// ============================================================
5117
5118#[derive(Clone, Debug)]
5119pub struct ElevationProfile {
5120    pub samples:       Vec<(f32, f32)>,
5121    pub max_grade:     f32,
5122    pub avg_grade:     f32,
5123    pub total_ascent:  f32,
5124    pub total_descent: f32,
5125}
5126
5127impl ElevationProfile {
5128    pub fn compute(spline: &CatmullRomSpline, n: usize) -> Self {
5129        let total = spline.total_arc_length();
5130        let samples: Vec<(f32, f32)> = (0..=n).map(|i| {
5131            let s = i as f32 / n as f32 * total;
5132            let t = spline.t_at_arc_length(s);
5133            (s, spline.evaluate(t).y)
5134        }).collect();
5135        let mut max_grade = 0.0_f32;
5136        let mut ascent = 0.0_f32;
5137        let mut descent = 0.0_f32;
5138        for i in 1..samples.len() {
5139            let ds = samples[i].0 - samples[i-1].0;
5140            let dy = samples[i].1 - samples[i-1].1;
5141            if ds > EPSILON { let g = (dy / ds).abs() * 100.0; if g > max_grade { max_grade = g; } }
5142            if dy > 0.0 { ascent += dy; } else { descent += dy.abs(); }
5143        }
5144        let avg_grade = if total > EPSILON { (ascent + descent) / total * 100.0 } else { 0.0 };
5145        ElevationProfile { samples, max_grade, avg_grade, total_ascent: ascent, total_descent: descent }
5146    }
5147
5148    pub fn elevation_at(&self, s: f32) -> f32 {
5149        let n = self.samples.len();
5150        if n == 0 { return 0.0; }
5151        let idx = self.samples.partition_point(|&(sa, _)| sa <= s);
5152        if idx == 0 { return self.samples[0].1; }
5153        if idx >= n { return self.samples[n-1].1; }
5154        let (s0, e0) = self.samples[idx-1];
5155        let (s1, e1) = self.samples[idx];
5156        let f = if (s1-s0).abs() < EPSILON { 0.0 } else { (s-s0)/(s1-s0) };
5157        lerp(e0, e1, f)
5158    }
5159}
5160
5161// ============================================================
5162// TRAFFIC LIGHT
5163// ============================================================
5164
5165#[derive(Clone, Debug, PartialEq)]
5166pub enum TrafficLightPhase { Green, Yellow, Red, FlashingRed }
5167
5168#[derive(Clone, Debug)]
5169pub struct TrafficLight {
5170    pub id:              u64,
5171    pub position:        Vec3,
5172    pub phase:           TrafficLightPhase,
5173    pub phase_timer:     f32,
5174    pub green_time:      f32,
5175    pub yellow_time:     f32,
5176    pub red_time:        f32,
5177    pub controlled_edges: Vec<u64>,
5178}
5179
5180impl TrafficLight {
5181    pub fn new(position: Vec3) -> Self {
5182        TrafficLight {
5183            id: rand_id(), position,
5184            phase: TrafficLightPhase::Green, phase_timer: 0.0,
5185            green_time: 30.0, yellow_time: 5.0, red_time: 30.0,
5186            controlled_edges: Vec::new(),
5187        }
5188    }
5189
5190    pub fn update(&mut self, dt: f32) {
5191        self.phase_timer += dt;
5192        let (next, dur) = match self.phase {
5193            TrafficLightPhase::Green       => (TrafficLightPhase::Yellow, self.green_time),
5194            TrafficLightPhase::Yellow      => (TrafficLightPhase::Red,    self.yellow_time),
5195            TrafficLightPhase::Red         => (TrafficLightPhase::Green,  self.red_time),
5196            TrafficLightPhase::FlashingRed => (TrafficLightPhase::Red,    2.0),
5197        };
5198        if self.phase_timer >= dur { self.phase = next; self.phase_timer -= dur; }
5199    }
5200
5201    pub fn can_pass(&self) -> bool { self.phase == TrafficLightPhase::Green }
5202
5203    pub fn color_rgba(&self) -> Vec4 {
5204        match self.phase {
5205            TrafficLightPhase::Green       => Vec4::new(0.0, 1.0, 0.0, 1.0),
5206            TrafficLightPhase::Yellow      => Vec4::new(1.0, 1.0, 0.0, 1.0),
5207            TrafficLightPhase::Red         => Vec4::new(1.0, 0.0, 0.0, 1.0),
5208            TrafficLightPhase::FlashingRed => {
5209                if (self.phase_timer * 2.0) as u32 % 2 == 0 {
5210                    Vec4::new(1.0, 0.0, 0.0, 1.0)
5211                } else { Vec4::new(0.2, 0.0, 0.0, 1.0) }
5212            }
5213        }
5214    }
5215}
5216
5217// ============================================================
5218// SPLINE STATISTICS
5219// ============================================================
5220
5221#[derive(Clone, Debug)]
5222pub struct SplineStatistics {
5223    pub id:                     u64,
5224    pub name:                   String,
5225    pub total_length:           f32,
5226    pub num_segments:           usize,
5227    pub num_control_pts:        usize,
5228    pub min_curvature:          f32,
5229    pub max_curvature:          f32,
5230    pub avg_curvature:          f32,
5231    pub total_torsion_integral: f32,
5232    pub bounding_box_volume:    f32,
5233    pub is_closed:              bool,
5234}
5235
5236impl SplineStatistics {
5237    pub fn compute(spline: &CatmullRomSpline, id: u64, name: &str) -> Self {
5238        let n = 128usize;
5239        let mut curvatures = Vec::with_capacity(n+1);
5240        let mut torsion_int = 0.0_f32;
5241        let dt = 1.0 / n as f32;
5242        for i in 0..=n {
5243            let t = i as f32 * dt;
5244            let frame = spline.frenet_frame_at(t);
5245            curvatures.push(frame.curvature);
5246            torsion_int += frame.torsion.abs() * dt;
5247        }
5248        let min_k = curvatures.iter().cloned().fold(f32::MAX,  f32::min);
5249        let max_k = curvatures.iter().cloned().fold(f32::MIN,  f32::max);
5250        let avg_k = curvatures.iter().sum::<f32>() / curvatures.len() as f32;
5251        let (bmin, bmax) = spline.bounding_box();
5252        let sz = bmax - bmin;
5253        SplineStatistics {
5254            id, name: name.to_string(),
5255            total_length: spline.total_arc_length(),
5256            num_segments: spline.num_segments(),
5257            num_control_pts: spline.control_points.len(),
5258            min_curvature: min_k, max_curvature: max_k, avg_curvature: avg_k,
5259            total_torsion_integral: torsion_int,
5260            bounding_box_volume: sz.x * sz.y * sz.z,
5261            is_closed: spline.closed,
5262        }
5263    }
5264}
5265
5266impl SplineEditor {
5267    pub fn compute_statistics(&self, id: u64) -> Option<SplineStatistics> {
5268        let s = self.catmull_splines.get(&id)?;
5269        let n = self.spline_names.get(&id).cloned().unwrap_or_default();
5270        Some(SplineStatistics::compute(s, id, &n))
5271    }
5272
5273    pub fn all_statistics(&self) -> Vec<SplineStatistics> {
5274        self.catmull_splines.iter().map(|(&id, s)| {
5275            let n = self.spline_names.get(&id).cloned().unwrap_or_default();
5276            SplineStatistics::compute(s, id, &n)
5277        }).collect()
5278    }
5279
5280    pub fn find_by_name(&self, name: &str) -> Option<u64> {
5281        self.spline_names.iter().find(|(_, n)| n.as_str() == name).map(|(&id, _)| id)
5282    }
5283
5284    pub fn rename_spline(&mut self, id: u64, new_name: &str) {
5285        if let Some(n) = self.spline_names.get_mut(&id) { *n = new_name.to_string(); }
5286    }
5287
5288    pub fn duplicate_spline(&mut self, id: u64) -> Option<u64> {
5289        let s = self.catmull_splines.get(&id)?.clone();
5290        let name = self.spline_names.get(&id).cloned().unwrap_or_default();
5291        let new_id = rand_id();
5292        self.catmull_splines.insert(new_id, s);
5293        self.spline_names.insert(new_id, format!("{}_copy", name));
5294        self.spline_types.insert(new_id, SplineType::CatmullRom);
5295        Some(new_id)
5296    }
5297
5298    pub fn translate_spline(&mut self, id: u64, delta: Vec3) {
5299        if let Some(s) = self.catmull_splines.get_mut(&id) {
5300            for cp in &mut s.control_points { cp.position += delta; }
5301            s.rebuild_arc_length_table();
5302        }
5303    }
5304
5305    pub fn scale_spline(&mut self, id: u64, origin: Vec3, scale: Vec3) {
5306        if let Some(s) = self.catmull_splines.get_mut(&id) {
5307            for cp in &mut s.control_points {
5308                cp.position = origin + (cp.position - origin) * scale;
5309            }
5310            s.rebuild_arc_length_table();
5311        }
5312    }
5313
5314    pub fn rotate_spline(&mut self, id: u64, origin: Vec3, rotation: Quat) {
5315        if let Some(s) = self.catmull_splines.get_mut(&id) {
5316            for cp in &mut s.control_points {
5317                cp.position = origin + rotation * (cp.position - origin);
5318            }
5319            s.rebuild_arc_length_table();
5320        }
5321    }
5322
5323    pub fn mirror_spline(&mut self, id: u64, plane_normal: Vec3, plane_d: f32) -> Option<u64> {
5324        let new_id = self.duplicate_spline(id)?;
5325        if let Some(s) = self.catmull_splines.get_mut(&new_id) {
5326            for cp in &mut s.control_points {
5327                let d = plane_normal.dot(cp.position) - plane_d;
5328                cp.position -= plane_normal * 2.0 * d;
5329            }
5330            s.rebuild_arc_length_table();
5331        }
5332        Some(new_id)
5333    }
5334}
5335
5336// ============================================================
5337// ACCELERATION PROFILE
5338// ============================================================
5339
5340#[derive(Clone, Debug)]
5341pub struct AccelerationProfile {
5342    pub max_speed:        f32,
5343    pub acceleration:     f32,
5344    pub deceleration:     f32,
5345    pub approach_radius:  f32,
5346}
5347
5348impl AccelerationProfile {
5349    pub fn new(max_speed: f32, accel: f32, decel: f32) -> Self {
5350        AccelerationProfile { max_speed, acceleration: accel, deceleration: decel, approach_radius: 5.0 }
5351    }
5352
5353    pub fn speed_at(&self, current: f32, dist_to_end: f32, dt: f32) -> f32 {
5354        let target = if dist_to_end < self.approach_radius {
5355            self.max_speed * (dist_to_end / self.approach_radius.max(EPSILON))
5356        } else { self.max_speed };
5357        if current < target { (current + self.acceleration * dt).min(target) }
5358        else { (current - self.deceleration * dt).max(target).max(0.0) }
5359    }
5360
5361    pub fn stopping_distance(&self, speed: f32) -> f32 {
5362        speed * speed / (2.0 * self.deceleration.max(EPSILON))
5363    }
5364
5365    pub fn travel_time(&self, arc_length: f32) -> f32 {
5366        let ad = self.max_speed * self.max_speed / (2.0 * self.acceleration.max(EPSILON));
5367        let dd = self.stopping_distance(self.max_speed);
5368        let ramp = ad + dd;
5369        if arc_length < ramp {
5370            let pv = (arc_length * self.acceleration * self.deceleration
5371                / (self.acceleration + self.deceleration)).sqrt();
5372            pv / self.acceleration + pv / self.deceleration
5373        } else {
5374            self.max_speed / self.acceleration
5375                + (arc_length - ramp) / self.max_speed
5376                + self.max_speed / self.deceleration
5377        }
5378    }
5379}
5380
5381// ============================================================
5382// SPLINE GROWTH (vine / tendril)
5383// ============================================================
5384
5385#[derive(Clone, Debug)]
5386pub struct SplineGrowthParams {
5387    pub direction:   Vec3,
5388    pub gravity:     f32,
5389    pub seed:        u32,
5390    pub step_length: f32,
5391    pub max_steps:   usize,
5392    pub turn_rate:   f32,
5393}
5394
5395impl SplineGrowthParams {
5396    pub fn vine() -> Self {
5397        SplineGrowthParams {
5398            direction: Vec3::Y, gravity: -0.05, seed: 42,
5399            step_length: 0.3, max_steps: 64, turn_rate: 0.2,
5400        }
5401    }
5402
5403    pub fn grow(&self) -> Vec<Vec3> {
5404        let mut pts = vec![Vec3::ZERO];
5405        let mut dir = safe_normalize(self.direction);
5406        let mut rng = self.seed as f32;
5407        for _ in 0..self.max_steps {
5408            rng = (rng * 1664525.0 + 1013904223.0) % 4294967296.0;
5409            let r  = rng / 4294967296.0;
5410            rng = (rng * 1664525.0 + 1013904223.0) % 4294967296.0;
5411            let r2 = rng / 4294967296.0;
5412            let turn = Vec3::new((r - 0.5) * 2.0 * self.turn_rate, self.gravity, (r2 - 0.5) * 2.0 * self.turn_rate);
5413            dir = safe_normalize(dir + turn);
5414            let last = *pts.last().unwrap();
5415            pts.push(last + dir * self.step_length);
5416        }
5417        pts
5418    }
5419}
5420
5421// ============================================================
5422// FENCE GENERATOR
5423// ============================================================
5424
5425#[derive(Clone, Debug)]
5426pub struct FenceProfile {
5427    pub post_height:    f32,
5428    pub post_width:     f32,
5429    pub post_spacing:   f32,
5430    pub rail_count:     u32,
5431}
5432
5433impl FenceProfile {
5434    pub fn wooden_rail() -> Self {
5435        FenceProfile { post_height: 1.2, post_width: 0.1, post_spacing: 2.5, rail_count: 3 }
5436    }
5437}
5438
5439#[derive(Clone, Debug)]
5440pub struct FenceGeometry {
5441    pub posts:    Vec<FrenetFrame>,
5442    pub profile:  FenceProfile,
5443}
5444
5445impl FenceGeometry {
5446    pub fn build(spline: &CatmullRomSpline, profile: FenceProfile) -> Self {
5447        let total = spline.total_arc_length();
5448        let n_posts = (total / profile.post_spacing).floor() as usize + 1;
5449        let posts = (0..n_posts).map(|i| {
5450            let t = spline.t_at_arc_length(i as f32 * profile.post_spacing);
5451            spline.frenet_frame_at(t)
5452        }).collect();
5453        FenceGeometry { posts, profile }
5454    }
5455
5456    pub fn post_matrix(&self, idx: usize) -> Mat4 {
5457        let f = &self.posts[idx];
5458        Mat4::from_cols(
5459            Vec4::new(f.normal.x,   f.normal.y,   f.normal.z,   0.0),
5460            Vec4::new(0.0,          1.0,           0.0,          0.0),
5461            Vec4::new(f.tangent.x,  f.tangent.y,  f.tangent.z,  0.0),
5462            Vec4::new(f.position.x, f.position.y, f.position.z, 1.0),
5463        )
5464    }
5465
5466    pub fn rail_endpoints(&self, rail_idx: u32) -> Vec<(Vec3, Vec3)> {
5467        let y = (rail_idx + 1) as f32 * (self.profile.post_height / (self.profile.rail_count + 1) as f32);
5468        self.posts.windows(2).map(|w| {
5469            (w[0].position + Vec3::Y * y, w[1].position + Vec3::Y * y)
5470        }).collect()
5471    }
5472}
5473
5474// ============================================================
5475// FIGURE-EIGHT / HELIX TEST HELPERS
5476// ============================================================
5477
5478pub fn test_create_figure_eight() -> CatmullRomSpline {
5479    let pts = vec![
5480        Vec3::new( 0.0, 0.0,  0.0), Vec3::new( 5.0, 0.0,  5.0),
5481        Vec3::new(10.0, 0.0,  0.0), Vec3::new( 5.0, 0.0, -5.0),
5482        Vec3::new( 0.0, 0.0,  0.0), Vec3::new(-5.0, 0.0,  5.0),
5483        Vec3::new(-10.0,0.0,  0.0), Vec3::new(-5.0, 0.0, -5.0),
5484        Vec3::new( 0.0, 0.0,  0.0),
5485    ];
5486    CatmullRomSpline::new(pts, 0.5, false)
5487}
5488
5489pub fn test_create_spiral() -> CatmullRomSpline {
5490    let pts = generate_helix(Vec3::ZERO, 5.0, 2.0, 3.0, 64);
5491    CatmullRomSpline::new(pts, 0.5, false)
5492}
5493
5494pub fn test_create_sine_wave() -> CatmullRomSpline {
5495    let pts: Vec<Vec3> = (0..32).map(|i| {
5496        let x = i as f32 * 0.5;
5497        Vec3::new(x, (x * 0.5).sin() * 2.0, 0.0)
5498    }).collect();
5499    CatmullRomSpline::new(pts, 0.5, false)
5500}
5501
5502// ============================================================
5503// BEZIER TIGHT BOUNDING BOX
5504// ============================================================
5505
5506pub fn bezier_tight_bounding_box(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> (Vec3, Vec3) {
5507    let mut mn = p0.min(p3);
5508    let mut mx = p0.max(p3);
5509    for dim in 0..3usize {
5510        let v = [p0, p1, p2, p3].map(|p| [p.x, p.y, p.z][dim]);
5511        let a = -3.0*v[0] + 9.0*v[1] - 9.0*v[2] + 3.0*v[3];
5512        let b =  6.0*v[0] - 12.0*v[1] + 6.0*v[2];
5513        let c = -3.0*v[0] + 3.0*v[1];
5514        let mut test_t = |t: f32| {
5515            if t > 0.0 && t < 1.0 {
5516                let pt = CubicBezierSpline::de_casteljau(p0, p1, p2, p3, t);
5517                mn = mn.min(pt);
5518                mx = mx.max(pt);
5519            }
5520        };
5521        if a.abs() < EPSILON {
5522            if b.abs() > EPSILON { test_t(-c / b); }
5523        } else {
5524            let disc = b*b - 4.0*a*c;
5525            if disc >= 0.0 {
5526                let sq = disc.sqrt();
5527                test_t((-b + sq) / (2.0*a));
5528                test_t((-b - sq) / (2.0*a));
5529            }
5530        }
5531    }
5532    (mn, mx)
5533}
5534
5535// ============================================================
5536
5537impl PathNetwork {
5538    pub fn total_length(&self) -> f32 {
5539        self.edges.values().map(|e| e.weight).sum()
5540    }
5541
5542    pub fn node_degree(&self, id: u64) -> usize {
5543        self.adjacency.get(&id).map(|v| v.len()).unwrap_or(0)
5544    }
5545
5546    pub fn junction_nodes(&self) -> Vec<u64> {
5547        self.nodes.keys().filter(|&&id| self.node_degree(id) > 2).cloned().collect()
5548    }
5549
5550    pub fn dead_end_nodes(&self) -> Vec<u64> {
5551        self.nodes.keys().filter(|&&id| self.node_degree(id) == 1).cloned().collect()
5552    }
5553
5554    pub fn edge_passable(&self, edge_id: u64) -> bool {
5555        self.edges.contains_key(&edge_id)
5556    }
5557}
5558
5559// ============================================================
5560// SPLINE ATTACHMENT
5561// ============================================================
5562
5563#[derive(Clone, Debug)]
5564pub struct SplineAttachment {
5565    pub spline_id:      u64,
5566    pub t:              f32,
5567    pub local_offset:   Vec3,
5568    pub local_rotation: Quat,
5569}
5570
5571impl SplineAttachment {
5572    pub fn new(spline_id: u64, t: f32) -> Self {
5573        SplineAttachment { spline_id, t, local_offset: Vec3::ZERO, local_rotation: Quat::IDENTITY }
5574    }
5575
5576    pub fn world_transform(&self, spline: &CatmullRomSpline) -> Mat4 {
5577        let frame = spline.frenet_frame_at(self.t);
5578        let rot   = Quat::from_mat4(&frame.to_matrix()) * self.local_rotation;
5579        let pos   = frame.position
5580            + frame.tangent  * self.local_offset.x
5581            + frame.normal   * self.local_offset.y
5582            + frame.binormal * self.local_offset.z;
5583        Mat4::from_rotation_translation(rot, pos)
5584    }
5585}
5586
5587// ============================================================
5588// DOUGLAS-PEUCKER POLYLINE SIMPLIFICATION (public API)
5589// ============================================================
5590
5591pub fn simplify_polyline(pts: &[Vec3], epsilon: f32) -> Vec<Vec3> {
5592    douglas_peucker(pts, epsilon)
5593}
5594
5595// ============================================================
5596// SPLINE EDITOR FINALIZATION
5597// ============================================================
5598
5599impl SplineEditor {
5600    /// Clear all data
5601    pub fn clear(&mut self) {
5602        self.catmull_splines.clear();
5603        self.bezier_splines.clear();
5604        self.bsplines.clear();
5605        self.nurbs_splines.clear();
5606        self.hermite_splines.clear();
5607        self.spline_names.clear();
5608        self.spline_types.clear();
5609        self.rail_tracks.clear();
5610        self.camera_rails.clear();
5611        self.constrained_objects.clear();
5612        self.chains.clear();
5613        self.generated_meshes.clear();
5614        self.selection.clear();
5615        self.debug_viz.clear();
5616        self.undo_history = UndoHistory::new(128);
5617    }
5618
5619    /// Set mesh section preset
5620    pub fn use_circle_section(&mut self, radius: f32, segments: usize) {
5621        self.mesh_section = CrossSection::circle(radius, segments);
5622    }
5623
5624    pub fn use_rectangle_section(&mut self, w: f32, h: f32) {
5625        self.mesh_section = CrossSection::rectangle(w, h);
5626    }
5627
5628    pub fn use_ibeam_section(&mut self, w: f32, h: f32, flange: f32, web: f32) {
5629        self.mesh_section = CrossSection::i_beam(w, h, flange, web);
5630    }
5631
5632    /// Select all splines
5633    pub fn select_all(&mut self) {
5634        for &id in self.catmull_splines.keys() {
5635            self.selection.selected.insert(id);
5636        }
5637    }
5638
5639    /// Deselect all
5640    pub fn deselect_all(&mut self) {
5641        self.selection.clear();
5642    }
5643
5644    /// Delete selected splines
5645    pub fn delete_selected(&mut self) {
5646        let to_delete: Vec<u64> = self.selection.selected.iter().cloned().collect();
5647        for id in to_delete {
5648            self.catmull_splines.remove(&id);
5649            self.spline_names.remove(&id);
5650            self.spline_types.remove(&id);
5651            self.generated_meshes.remove(&id);
5652        }
5653        self.selection.clear();
5654    }
5655
5656    /// Regenerate all meshes
5657    pub fn regenerate_all_meshes(&mut self) {
5658        let ids: Vec<u64> = self.catmull_splines.keys().cloned().collect();
5659        for id in ids {
5660            self.generate_mesh_for_spline(id);
5661        }
5662    }
5663}
5664
5665// ============================================================
5666// SPLINE SMOOTHING & FAIRING
5667// ============================================================
5668
5669/// Laplacian smoothing: move each interior control point toward
5670/// the average of its two neighbours by factor `lambda`.
5671pub fn laplacian_smooth_catmull(spline: &mut CatmullRomSpline, lambda: f32, iterations: u32) {
5672    for _ in 0..iterations {
5673        let n = spline.control_points.len();
5674        if n < 3 { break; }
5675        let old: Vec<Vec3> = spline.control_points.iter().map(|cp| cp.position).collect();
5676        for i in 1..n - 1 {
5677            let avg = (old[i - 1] + old[i + 1]) * 0.5;
5678            spline.control_points[i].position = old[i] + (avg - old[i]) * lambda;
5679        }
5680    }
5681}
5682
5683/// Taubin smoothing: two-pass (lambda / -mu) to avoid shrinkage.
5684pub fn taubin_smooth_catmull(spline: &mut CatmullRomSpline, lambda: f32, mu: f32, iterations: u32) {
5685    for _ in 0..iterations {
5686        laplacian_smooth_catmull(spline, lambda, 1);
5687        laplacian_smooth_catmull(spline, mu, 1);
5688    }
5689}
5690
5691/// Compute the total variation (sum of |dp_i| changes) of a spline.
5692pub fn spline_total_variation(spline: &CatmullRomSpline) -> f32 {
5693    let pts: Vec<Vec3> = spline.control_points.iter().map(|cp| cp.position).collect();
5694    pts.windows(2).map(|w| (w[1] - w[0]).length()).sum()
5695}
5696
5697/// Redistribute control points to be uniformly spaced along the arc.
5698pub fn equidistribute_catmull(spline: &mut CatmullRomSpline, new_count: usize) {
5699    if spline.control_points.len() < 2 || new_count < 2 { return; }
5700    let table = build_arc_length_table(512, &|t| spline.evaluate(t));
5701    let total  = table.last().map(|&(_, s)| s).unwrap_or(1.0);
5702    let step   = total / (new_count - 1) as f32;
5703    let new_pts: Vec<Vec3> = (0..new_count).map(|i| {
5704        let s = (i as f32 * step).min(total);
5705        let t = arc_length_to_t(&table, s);
5706        spline.evaluate(t)
5707    }).collect();
5708    spline.control_points = new_pts.into_iter().map(|p| ControlPoint {
5709        position: p, weight: 1.0, ..ControlPoint::new(p)
5710    }).collect();
5711}
5712
5713// ============================================================
5714// CURVE FITTING (least-squares cubic Bezier)
5715// ============================================================
5716
5717/// Fit a single cubic Bezier to a set of ordered sample points using chord-length
5718/// parameterization and the Moore-Penrose pseudoinverse.  Returns [P0, P1, P2, P3].
5719pub fn fit_cubic_bezier(points: &[Vec3]) -> [Vec3; 4] {
5720    if points.len() < 2 {
5721        let p = points.first().copied().unwrap_or(Vec3::ZERO);
5722        return [p, p, p, p];
5723    }
5724    let mut params: Vec<f32> = vec![0.0];
5725    for i in 1..points.len() {
5726        let d = (points[i] - points[i-1]).length();
5727        params.push(params[i-1] + d);
5728    }
5729    let total = *params.last().unwrap();
5730    if total < 1e-10 { let p = points[0]; return [p, p, p, p]; }
5731    for p in &mut params { *p /= total; }
5732
5733    let p0 = points[0];
5734    let p3 = *points.last().unwrap();
5735
5736    fn b0(t: f32) -> f32 { let u=1.0-t; u*u*u }
5737    fn b1(t: f32) -> f32 { let u=1.0-t; 3.0*u*u*t }
5738    fn b2(t: f32) -> f32 { let u=1.0-t; 3.0*u*t*t }
5739    fn b3(t: f32) -> f32 { t*t*t }
5740
5741    let n = points.len();
5742    let mut ata = [[0.0f32; 2]; 2];
5743    let mut atr = [[0.0f32; 3]; 2];
5744
5745    for i in 0..n {
5746        let t  = params[i];
5747        let a  = [b1(t), b2(t)];
5748        let rhs = points[i] - p0 * b0(t) - p3 * b3(t);
5749        for r in 0..2 {
5750            for c in 0..2 { ata[r][c] += a[r] * a[c]; }
5751            atr[r][0] += a[r] * rhs.x;
5752            atr[r][1] += a[r] * rhs.y;
5753            atr[r][2] += a[r] * rhs.z;
5754        }
5755    }
5756    let det = ata[0][0]*ata[1][1] - ata[0][1]*ata[1][0];
5757    if det.abs() < 1e-12 { return [p0, p0, p3, p3]; }
5758    let inv = [[ ata[1][1]/det, -ata[0][1]/det],
5759               [-ata[1][0]/det,  ata[0][0]/det]];
5760    let mut p1 = Vec3::ZERO;
5761    let mut p2 = Vec3::ZERO;
5762    for r in 0..2 {
5763        let vx = inv[r][0]*atr[0][0] + inv[r][1]*atr[1][0];
5764        let vy = inv[r][0]*atr[0][1] + inv[r][1]*atr[1][1];
5765        let vz = inv[r][0]*atr[0][2] + inv[r][1]*atr[1][2];
5766        if r == 0 { p1 = Vec3::new(vx, vy, vz); }
5767        else      { p2 = Vec3::new(vx, vy, vz); }
5768    }
5769    [p0, p1, p2, p3]
5770}
5771
5772/// Fit a piecewise cubic Bezier (G1 continuous) to points.
5773pub fn fit_piecewise_cubic_bezier(points: &[Vec3], max_error: f32) -> CubicBezierSpline {
5774    let mut spline = CubicBezierSpline { segments: Vec::new(), closed: false, arc_length_table: Vec::new(), total_length: 0.0 };
5775    if points.len() < 2 { return spline; }
5776
5777    fn fit_and_check(pts: &[Vec3], tol: f32, out: &mut Vec<[Vec3; 4]>) {
5778        if pts.len() < 2 { return; }
5779        let seg = fit_cubic_bezier(pts);
5780        let mut max_err = 0.0f32;
5781        let mut worst  = pts.len() / 2;
5782        for (i, &pt) in pts.iter().enumerate() {
5783            let t = i as f32 / (pts.len() - 1).max(1) as f32;
5784            let fitted = CubicBezierSpline::de_casteljau(seg[0], seg[1], seg[2], seg[3], t);
5785            let err = (fitted - pt).length();
5786            if err > max_err { max_err = err; worst = i; }
5787        }
5788        if max_err <= tol || pts.len() <= 3 { out.push(seg); }
5789        else {
5790            fit_and_check(&pts[..=worst], tol, out);
5791            fit_and_check(&pts[worst..], tol, out);
5792        }
5793    }
5794
5795    fit_and_check(points, max_error, &mut spline.segments);
5796    spline
5797}
5798
5799// ============================================================
5800// SPLINE OFFSET (2-D parallel offset)
5801// ============================================================
5802
5803/// Offset a CatmullRom spline in the XZ plane by `distance` (positive = left).
5804pub fn offset_spline_xz(spline: &CatmullRomSpline, distance: f32, samples: usize) -> CatmullRomSpline {
5805    let table = build_arc_length_table(samples * 4, &|t| spline.evaluate(t));
5806    let total  = table.last().map(|&(_, s)| s).unwrap_or(1.0);
5807    let step   = total / (samples - 1).max(1) as f32;
5808    let pts: Vec<Vec3> = (0..samples).map(|i| {
5809        let s = (i as f32 * step).min(total);
5810        let t = arc_length_to_t(&table, s);
5811        let pos  = spline.evaluate(t);
5812        let tang = spline.evaluate_derivative(t);
5813        let n = Vec3::new(-tang.z, 0.0, tang.x).normalize_or_zero();
5814        pos + n * distance
5815    }).collect();
5816    CatmullRomSpline::new(pts.into_iter().map(|p| p).collect(), spline.alpha, spline.closed)
5817}
5818
5819// ============================================================
5820// RIBBON MESH
5821// ============================================================
5822
5823/// A flat ribbon along the spline with configurable width taper.
5824pub struct RibbonMesh {
5825    pub vertices: Vec<Vec3>,
5826    pub normals:  Vec<Vec3>,
5827    pub uvs:      Vec<Vec2>,
5828    pub indices:  Vec<u32>,
5829    pub width_at: Vec<f32>,
5830}
5831
5832impl RibbonMesh {
5833    pub fn generate(spline: &CatmullRomSpline, steps: usize, width_fn: &dyn Fn(f32) -> f32) -> Self {
5834        let table  = build_arc_length_table(steps * 8, &|t| spline.evaluate(t));
5835        let total  = table.last().map(|&(_, s)| s).unwrap_or(1.0);
5836        let mut verts   = Vec::new();
5837        let mut normals = Vec::new();
5838        let mut uvs     = Vec::new();
5839        let mut indices = Vec::new();
5840        let mut width_at = Vec::new();
5841
5842        let mut frames: Vec<ParallelTransportFrame> = Vec::with_capacity(steps + 1);
5843        for i in 0..=steps {
5844            let t_param = i as f32 / steps as f32;
5845            let s  = t_param * total;
5846            let t  = arc_length_to_t(&table, s);
5847            let p  = spline.evaluate(t);
5848            let tn = spline.evaluate_derivative(t);
5849            if i == 0 { frames.push(ParallelTransportFrame::initial(p, tn)); }
5850            else {
5851                let prev = &frames[i - 1];
5852                frames.push(ParallelTransportFrame::transport(prev, p, tn));
5853            }
5854        }
5855
5856        for (i, frame) in frames.iter().enumerate() {
5857            let t_param = i as f32 / steps as f32;
5858            let hw = width_fn(t_param);
5859            width_at.push(hw);
5860            let u = i as f32 / steps as f32;
5861            let left  = frame.position - frame.normal * hw;
5862            let right = frame.position + frame.normal * hw;
5863            verts.push(left);
5864            verts.push(right);
5865            normals.push(frame.binormal);
5866            normals.push(frame.binormal);
5867            uvs.push(Vec2::new(u, 0.0));
5868            uvs.push(Vec2::new(u, 1.0));
5869        }
5870
5871        for i in 0..steps {
5872            let bl = (i * 2) as u32;
5873            let br = bl + 1;
5874            let tl = bl + 2;
5875            let tr = bl + 3;
5876            indices.extend_from_slice(&[bl, br, tl, br, tr, tl]);
5877        }
5878
5879        RibbonMesh { vertices: verts, normals, uvs, indices, width_at }
5880    }
5881
5882    pub fn surface_area(&self) -> f32 {
5883        let mut area = 0.0f32;
5884        for tri in self.indices.chunks(3) {
5885            if tri.len() < 3 { continue; }
5886            let a = self.vertices[tri[0] as usize];
5887            let b = self.vertices[tri[1] as usize];
5888            let c = self.vertices[tri[2] as usize];
5889            area += (b - a).cross(c - a).length() * 0.5;
5890        }
5891        area
5892    }
5893}
5894
5895// ============================================================
5896// EXTRUDED PROFILE MESH
5897// ============================================================
5898
5899pub struct ExtrudedProfile {
5900    pub vertices: Vec<Vec3>,
5901    pub normals:  Vec<Vec3>,
5902    pub uvs:      Vec<Vec2>,
5903    pub indices:  Vec<u32>,
5904}
5905
5906impl ExtrudedProfile {
5907    pub fn generate(
5908        spline:   &CatmullRomSpline,
5909        profile:  &[Vec2],
5910        steps:    usize,
5911        scale_fn: &dyn Fn(f32) -> f32,
5912    ) -> Self {
5913        let table  = build_arc_length_table(steps * 8, &|t| spline.evaluate(t));
5914        let total  = table.last().map(|&(_, s)| s).unwrap_or(1.0);
5915        let np     = profile.len();
5916        let mut verts   = Vec::new();
5917        let mut normals = Vec::new();
5918        let mut uvs_out = Vec::new();
5919        let mut indices = Vec::new();
5920
5921        let mut frames: Vec<ParallelTransportFrame> = Vec::with_capacity(steps + 1);
5922        for i in 0..=steps {
5923            let t_p = i as f32 / steps as f32;
5924            let s   = t_p * total;
5925            let t   = arc_length_to_t(&table, s);
5926            let p   = spline.evaluate(t);
5927            let tn  = spline.evaluate_derivative(t);
5928            if i == 0 { frames.push(ParallelTransportFrame::initial(p, tn)); }
5929            else {
5930                let prev = &frames[i - 1];
5931                frames.push(ParallelTransportFrame::transport(prev, p, tn));
5932            }
5933        }
5934
5935        for (i, frame) in frames.iter().enumerate() {
5936            let t_p   = i as f32 / steps as f32;
5937            let scale = scale_fn(t_p);
5938            let u_val = t_p;
5939            for (j, &pv) in profile.iter().enumerate() {
5940                let world = frame.position
5941                    + frame.normal   * pv.x * scale
5942                    + frame.binormal * pv.y * scale;
5943                let pn = Vec2::new(pv.y, -pv.x).normalize_or_zero();
5944                let wn = (frame.normal * pn.x + frame.binormal * pn.y).normalize_or_zero();
5945                verts.push(world);
5946                normals.push(wn);
5947                uvs_out.push(Vec2::new(u_val, j as f32 / np as f32));
5948            }
5949        }
5950
5951        for i in 0..steps {
5952            for j in 0..np {
5953                let jn = (j + 1) % np;
5954                let a = (i * np + j)  as u32;
5955                let b = (i * np + jn) as u32;
5956                let c = ((i + 1) * np + j)  as u32;
5957                let d = ((i + 1) * np + jn) as u32;
5958                indices.extend_from_slice(&[a, b, c, b, d, c]);
5959            }
5960        }
5961
5962        ExtrudedProfile { vertices: verts, normals, uvs: uvs_out, indices }
5963    }
5964}
5965
5966// ============================================================
5967// SPLINE CAGE DEFORMER
5968// ============================================================
5969
5970pub struct SplineCage {
5971    pub source_spline: CatmullRomSpline,
5972    pub target_spline: CatmullRomSpline,
5973}
5974
5975impl SplineCage {
5976    pub fn deform(&self, point: Vec3) -> Vec3 {
5977        let (t, _) = self.source_spline.nearest_point(point);
5978        let src_pos  = self.source_spline.evaluate(t);
5979        let src_tang = self.source_spline.evaluate_derivative(t);
5980        let src_norm = {
5981            let up = if src_tang.y.abs() < 0.99 { Vec3::Y } else { Vec3::Z };
5982            src_tang.cross(up).normalize_or_zero()
5983        };
5984        let src_bi = src_tang.cross(src_norm).normalize_or_zero();
5985        let offset = point - src_pos;
5986        let local_t = offset.dot(src_tang);
5987        let local_n = offset.dot(src_norm);
5988        let local_b = offset.dot(src_bi);
5989
5990        let tgt_pos  = self.target_spline.evaluate(t);
5991        let tgt_tang = self.target_spline.evaluate_derivative(t);
5992        let tgt_up   = if tgt_tang.y.abs() < 0.99 { Vec3::Y } else { Vec3::Z };
5993        let tgt_norm = tgt_tang.cross(tgt_up).normalize_or_zero();
5994        let tgt_bi   = tgt_tang.cross(tgt_norm).normalize_or_zero();
5995
5996        tgt_pos + tgt_tang * local_t + tgt_norm * local_n + tgt_bi * local_b
5997    }
5998
5999    pub fn deform_mesh(&self, points: &mut [Vec3]) {
6000        for p in points.iter_mut() { *p = self.deform(*p); }
6001    }
6002}
6003
6004// ============================================================
6005// SPLINE LATTICE DEFORMER (two-rail)
6006// ============================================================
6007
6008pub struct SplineLattice {
6009    pub rail_a: CatmullRomSpline,
6010    pub rail_b: CatmullRomSpline,
6011}
6012
6013impl SplineLattice {
6014    pub fn evaluate(&self, u: f32, v: f32) -> Vec3 {
6015        let pa = self.rail_a.evaluate(u.clamp(0.0, 1.0));
6016        let pb = self.rail_b.evaluate(u.clamp(0.0, 1.0));
6017        pa.lerp(pb, v.clamp(0.0, 1.0))
6018    }
6019
6020    pub fn deform(&self, point: Vec3) -> Vec3 {
6021        let (u, _) = self.rail_a.nearest_point(point);
6022        let a = self.rail_a.evaluate(u);
6023        let b = self.rail_b.evaluate(u);
6024        let ab   = b - a;
6025        let len2 = ab.length_squared();
6026        let v = if len2 < 1e-10 { 0.0 } else { (point - a).dot(ab) / len2 };
6027        self.evaluate(u, v)
6028    }
6029}
6030
6031// ============================================================
6032// SPLINE DEFORMATION HISTORY
6033// ============================================================
6034
6035#[derive(Clone, Debug)]
6036pub struct SplineDeformHistory {
6037    pub snapshots: VecDeque<Vec<Vec3>>,
6038    pub max_size:  usize,
6039}
6040
6041impl SplineDeformHistory {
6042    pub fn new(max_size: usize) -> Self {
6043        SplineDeformHistory { snapshots: VecDeque::new(), max_size }
6044    }
6045
6046    pub fn push(&mut self, positions: Vec<Vec3>) {
6047        if self.snapshots.len() >= self.max_size { self.snapshots.pop_front(); }
6048        self.snapshots.push_back(positions);
6049    }
6050
6051    pub fn undo(&mut self) -> Option<Vec<Vec3>> { self.snapshots.pop_back() }
6052
6053    pub fn blend(&self, t: f32) -> Option<Vec<Vec3>> {
6054        let n = self.snapshots.len();
6055        if n < 2 { return self.snapshots.back().cloned(); }
6056        let fi = (t.clamp(0.0, 1.0) * (n - 1) as f32).floor() as usize;
6057        let fi = fi.min(n - 2);
6058        let alpha = t * (n - 1) as f32 - fi as f32;
6059        let a = &self.snapshots[fi];
6060        let b = &self.snapshots[fi + 1];
6061        if a.len() != b.len() { return Some(a.clone()); }
6062        Some(a.iter().zip(b.iter()).map(|(&pa, &pb)| pa.lerp(pb, alpha)).collect())
6063    }
6064}
6065
6066// ============================================================
6067// SPLINE MASS-SPRING DYNAMICS
6068// ============================================================
6069
6070#[derive(Clone, Debug)]
6071pub struct SplineParticle {
6072    pub position: Vec3,
6073    pub velocity: Vec3,
6074    pub mass:     f32,
6075    pub pinned:   bool,
6076}
6077
6078pub struct SplineDynamics {
6079    pub particles:    Vec<SplineParticle>,
6080    pub rest_lengths: Vec<f32>,
6081    pub stiffness:    f32,
6082    pub damping:      f32,
6083    pub gravity:      Vec3,
6084}
6085
6086impl SplineDynamics {
6087    pub fn from_catmull(spline: &CatmullRomSpline, stiffness: f32, damping: f32) -> Self {
6088        let particles: Vec<SplineParticle> = spline.control_points.iter()
6089            .map(|cp| SplineParticle { position: cp.position, velocity: Vec3::ZERO, mass: 1.0, pinned: false })
6090            .collect();
6091        let rest_lengths: Vec<f32> = particles.windows(2)
6092            .map(|w| (w[1].position - w[0].position).length())
6093            .collect();
6094        SplineDynamics { particles, rest_lengths, stiffness, damping, gravity: Vec3::new(0.0, -9.81, 0.0) }
6095    }
6096
6097    pub fn step(&mut self, dt: f32) {
6098        let n = self.particles.len();
6099        let mut forces: Vec<Vec3> = vec![Vec3::ZERO; n];
6100
6101        for i in 0..n.saturating_sub(1) {
6102            let rest  = self.rest_lengths[i];
6103            let pa    = self.particles[i].position;
6104            let pb    = self.particles[i + 1].position;
6105            let delta = pb - pa;
6106            let dist  = delta.length();
6107            if dist < 1e-10 { continue; }
6108            let f = delta.normalize() * self.stiffness * (dist - rest);
6109            forces[i]     += f;
6110            forces[i + 1] -= f;
6111        }
6112
6113        for i in 0..n {
6114            if self.particles[i].pinned { continue; }
6115            let m     = self.particles[i].mass;
6116            let accel = (forces[i] + self.gravity * m) / m - self.particles[i].velocity * self.damping;
6117            self.particles[i].velocity += accel * dt;
6118            let vel = self.particles[i].velocity;
6119            self.particles[i].position += vel * dt;
6120        }
6121    }
6122
6123    pub fn apply_to_catmull(&self, spline: &mut CatmullRomSpline) {
6124        for (i, p) in self.particles.iter().enumerate() {
6125            if let Some(cp) = spline.control_points.get_mut(i) { cp.position = p.position; }
6126        }
6127        spline.rebuild_arc_length_table();
6128    }
6129}
6130
6131// ============================================================
6132// CLOTHOID (EULER SPIRAL) APPROXIMATION
6133// ============================================================
6134
6135fn fresnel_s_approx(t: f32) -> f32 {
6136    let t2 = t * t;
6137    let mut s    = t * t2 / 3.0;
6138    let mut sign = -1.0f32;
6139    let mut term = t * t2 * t2 * t2 / (3.0 * 14.0);
6140    for k in 1u32..12 {
6141        s += sign * term;
6142        sign = -sign;
6143        let f = (2 * k + 1) as f32;
6144        term *= t2 * t2 / (f * (f + 2.0) * 2.0 * (k + 1) as f32);
6145        if term.abs() < 1e-10 { break; }
6146    }
6147    s
6148}
6149
6150fn fresnel_c_approx(t: f32) -> f32 {
6151    let t2 = t * t;
6152    let mut c    = t;
6153    let mut sign = -1.0f32;
6154    let mut term = t * t2 * t2 / (2.0 * 5.0);
6155    for k in 1u32..12 {
6156        c += sign * term;
6157        sign = -sign;
6158        let f = (2 * k) as f32;
6159        term *= t2 * t2 / (f * (f + 1.0) * 2.0 * (k + 1) as f32);
6160        if term.abs() < 1e-10 { break; }
6161    }
6162    c
6163}
6164
6165/// Sample a Clothoid (Euler spiral) with scale `a`, returning XZ positions.
6166pub fn sample_clothoid(a: f32, n: usize, flip_z: bool) -> Vec<Vec3> {
6167    let max_t = std::f32::consts::PI.sqrt();
6168    (0..n).map(|i| {
6169        let t = i as f32 / n.max(1) as f32 * max_t;
6170        let x = a * fresnel_c_approx(t);
6171        let z = a * fresnel_s_approx(t) * if flip_z { -1.0 } else { 1.0 };
6172        Vec3::new(x, 0.0, z)
6173    }).collect()
6174}
6175
6176/// Insert a clothoid transition between two tangent directions.
6177pub fn clothoid_transition(length: f32, n: usize) -> (Vec<Vec3>, Vec<Vec3>) {
6178    let a = length.sqrt();
6179    let left  = sample_clothoid(a, n, false);
6180    let right = sample_clothoid(a, n, true);
6181    (left, right)
6182}
6183
6184// ============================================================
6185// BIARC FIT
6186// ============================================================
6187
6188#[derive(Clone, Debug)]
6189pub struct CircularArc {
6190    pub centre:    Vec3,
6191    pub radius:    f32,
6192    pub start_pt:  Vec3,
6193    pub end_pt:    Vec3,
6194    pub start_ang: f32,
6195    pub end_ang:   f32,
6196    pub axis:      Vec3,
6197}
6198
6199impl CircularArc {
6200    pub fn evaluate(&self, t: f32) -> Vec3 {
6201        let angle = self.start_ang + (self.end_ang - self.start_ang) * t;
6202        let fwd   = (self.start_pt - self.centre).normalize_or_zero();
6203        let right = self.axis.cross(fwd).normalize_or_zero();
6204        self.centre + fwd * (angle.cos() * self.radius) + right * (angle.sin() * self.radius)
6205    }
6206
6207    pub fn arc_length(&self) -> f32 {
6208        (self.end_ang - self.start_ang).abs() * self.radius
6209    }
6210}
6211
6212/// Approximate a G1 segment with two circular arcs (biarc).
6213pub fn biarc_fit(p0: Vec3, t0: Vec3, p1: Vec3, t1: Vec3) -> (CircularArc, CircularArc) {
6214    let t0 = t0.normalize_or_zero();
6215    let chord  = p1 - p0;
6216    let chord_len = chord.length();
6217    let j = p0 + chord * 0.5; // simple midpoint join
6218
6219    fn make_arc(a: Vec3, ta: Vec3, b: Vec3) -> CircularArc {
6220        let perp_ta = Vec3::new(-ta.z, 0.0, ta.x).normalize_or_zero();
6221        let d    = b - a;
6222        let proj = d.dot(perp_ta);
6223        let r    = if proj.abs() < 1e-8 { 1e6 } else { d.length_squared() / (2.0 * proj) };
6224        let centre = a + perp_ta * r;
6225        let axis = ta.cross(d).normalize_or_zero();
6226        CircularArc { centre, radius: r.abs(), start_pt: a, end_pt: b, start_ang: 0.0, end_ang: 1.0, axis }
6227    }
6228
6229    let arc0 = make_arc(p0, t0, j);
6230    let d1   = (j - p0).normalize_or_zero();
6231    let arc1 = make_arc(j, d1, p1);
6232    let _ = (chord_len, t1);
6233    (arc0, arc1)
6234}
6235
6236// ============================================================
6237// SPLINE IK CHAIN (FABRIK)
6238// ============================================================
6239
6240#[derive(Clone, Debug)]
6241pub struct SplineIKChain {
6242    pub joints:       Vec<Vec3>,
6243    pub bone_lengths: Vec<f32>,
6244    pub root_fixed:   bool,
6245}
6246
6247impl SplineIKChain {
6248    pub fn new(joints: Vec<Vec3>) -> Self {
6249        let bone_lengths = joints.windows(2).map(|w| (w[1] - w[0]).length()).collect();
6250        SplineIKChain { joints, bone_lengths, root_fixed: true }
6251    }
6252
6253    pub fn solve_fabrik(&mut self, target: Vec3, max_iter: u32, tolerance: f32) {
6254        let n    = self.joints.len();
6255        if n < 2 { return; }
6256        let root = self.joints[0];
6257        let total_len: f32 = self.bone_lengths.iter().sum();
6258        if (target - root).length() >= total_len {
6259            let dir = (target - root).normalize_or_zero();
6260            for i in 1..n {
6261                let len: f32 = self.bone_lengths[..i].iter().sum();
6262                self.joints[i] = root + dir * len;
6263            }
6264            return;
6265        }
6266        for _ in 0..max_iter {
6267            self.joints[n - 1] = target;
6268            for i in (0..n - 1).rev() {
6269                let d = (self.joints[i] - self.joints[i + 1]).normalize_or_zero();
6270                self.joints[i] = self.joints[i + 1] + d * self.bone_lengths[i];
6271            }
6272            if self.root_fixed { self.joints[0] = root; }
6273            for i in 0..n - 1 {
6274                let d = (self.joints[i + 1] - self.joints[i]).normalize_or_zero();
6275                self.joints[i + 1] = self.joints[i] + d * self.bone_lengths[i];
6276            }
6277            if (self.joints[n - 1] - target).length() < tolerance { break; }
6278        }
6279    }
6280
6281    pub fn to_spline(&self) -> CatmullRomSpline {
6282        CatmullRomSpline::new(self.joints.iter().copied().collect(), 0.5, false)
6283    }
6284}
6285
6286// ============================================================
6287// TRANSFORM UTILITIES
6288// ============================================================
6289
6290pub fn snap_to_grid(spline: &mut CatmullRomSpline, cell_size: f32) {
6291    if cell_size < 1e-10 { return; }
6292    for cp in &mut spline.control_points {
6293        cp.position.x = (cp.position.x / cell_size).round() * cell_size;
6294        cp.position.y = (cp.position.y / cell_size).round() * cell_size;
6295        cp.position.z = (cp.position.z / cell_size).round() * cell_size;
6296    }
6297    spline.rebuild_arc_length_table();
6298}
6299
6300pub fn snap_to_grid_xz(spline: &mut CatmullRomSpline, cell_size: f32) {
6301    if cell_size < 1e-10 { return; }
6302    for cp in &mut spline.control_points {
6303        cp.position.x = (cp.position.x / cell_size).round() * cell_size;
6304        cp.position.z = (cp.position.z / cell_size).round() * cell_size;
6305    }
6306    spline.rebuild_arc_length_table();
6307}
6308
6309pub fn mirror_spline_x(spline: &mut CatmullRomSpline) {
6310    for cp in &mut spline.control_points { cp.position.x = -cp.position.x; }
6311    spline.control_points.reverse();
6312    spline.rebuild_arc_length_table();
6313}
6314
6315pub fn mirror_spline_y(spline: &mut CatmullRomSpline) {
6316    for cp in &mut spline.control_points { cp.position.y = -cp.position.y; }
6317    spline.control_points.reverse();
6318    spline.rebuild_arc_length_table();
6319}
6320
6321pub fn mirror_spline_z(spline: &mut CatmullRomSpline) {
6322    for cp in &mut spline.control_points { cp.position.z = -cp.position.z; }
6323    spline.control_points.reverse();
6324    spline.rebuild_arc_length_table();
6325}
6326
6327pub fn translate_spline(spline: &mut CatmullRomSpline, delta: Vec3) {
6328    for cp in &mut spline.control_points { cp.position += delta; }
6329    spline.rebuild_arc_length_table();
6330}
6331
6332pub fn rotate_spline(spline: &mut CatmullRomSpline, rot: Quat) {
6333    for cp in &mut spline.control_points { cp.position = rot * cp.position; }
6334    spline.rebuild_arc_length_table();
6335}
6336
6337pub fn scale_spline_uniform(spline: &mut CatmullRomSpline, scale: f32) {
6338    for cp in &mut spline.control_points { cp.position *= scale; }
6339    spline.rebuild_arc_length_table();
6340}
6341
6342pub fn scale_spline(spline: &mut CatmullRomSpline, sx: f32, sy: f32, sz: f32) {
6343    for cp in &mut spline.control_points {
6344        cp.position.x *= sx;
6345        cp.position.y *= sy;
6346        cp.position.z *= sz;
6347    }
6348    spline.rebuild_arc_length_table();
6349}
6350
6351// ============================================================
6352// SAMPLING STRATEGIES
6353// ============================================================
6354
6355pub fn sample_uniform_arc_length(spline: &CatmullRomSpline, n: usize) -> Vec<Vec3> {
6356    let table = build_arc_length_table(n * 8, &|t| spline.evaluate(t));
6357    let total  = table.last().map(|&(_, s)| s).unwrap_or(0.0);
6358    (0..n).map(|i| {
6359        let s = total * i as f32 / (n - 1).max(1) as f32;
6360        let t = arc_length_to_t(&table, s);
6361        spline.evaluate(t)
6362    }).collect()
6363}
6364
6365pub fn sample_by_world_step(spline: &CatmullRomSpline, step: f32) -> Vec<(Vec3, f32)> {
6366    let table = build_arc_length_table(2048, &|t| spline.evaluate(t));
6367    let total  = table.last().map(|&(_, s)| s).unwrap_or(0.0);
6368    if step <= 0.0 || total <= 0.0 { return Vec::new(); }
6369    let count = (total / step).ceil() as usize + 1;
6370    (0..count).map(|i| {
6371        let s = (i as f32 * step).min(total);
6372        let t = arc_length_to_t(&table, s);
6373        (spline.evaluate(t), t)
6374    }).collect()
6375}
6376
6377pub fn sample_adaptive_curvature(spline: &CatmullRomSpline, min_samples: usize, max_samples: usize, threshold: f32) -> Vec<Vec3> {
6378    let base: Vec<(f32, f32)> = (0..=max_samples).map(|i| {
6379        let t = i as f32 / max_samples as f32;
6380        let frenet = spline.frenet_frame_at(t);
6381        (t, frenet.curvature)
6382    }).collect();
6383    let mut selected: Vec<f32> = vec![0.0, 1.0];
6384    for &(t, kappa) in &base {
6385        if kappa > threshold { selected.push(t); }
6386    }
6387    selected.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6388    selected.dedup_by(|a, b| (*a - *b).abs() < 1e-5);
6389    while selected.len() < min_samples {
6390        let mut best_gap = 0.0f32;
6391        let mut best_idx = 0;
6392        for i in 0..selected.len().saturating_sub(1) {
6393            let g = selected[i + 1] - selected[i];
6394            if g > best_gap { best_gap = g; best_idx = i; }
6395        }
6396        let mid = (selected[best_idx] + selected[best_idx + 1]) * 0.5;
6397        selected.insert(best_idx + 1, mid);
6398    }
6399    selected.into_iter().map(|t| spline.evaluate(t)).collect()
6400}
6401
6402// ============================================================
6403// ANALYSIS REPORT
6404// ============================================================
6405
6406#[derive(Clone, Debug)]
6407pub struct SplineAnalysisReport {
6408    pub total_arc_length:    f32,
6409    pub min_curvature:       f32,
6410    pub max_curvature:       f32,
6411    pub mean_curvature:      f32,
6412    pub min_torsion:         f32,
6413    pub max_torsion:         f32,
6414    pub inflection_count:    usize,
6415    pub control_point_count: usize,
6416    pub self_intersection:   bool,
6417    pub bounding_box_min:    Vec3,
6418    pub bounding_box_max:    Vec3,
6419}
6420
6421impl SplineAnalysisReport {
6422    pub fn compute(spline: &CatmullRomSpline, samples: usize) -> Self {
6423        let mut min_k   = f32::MAX;
6424        let mut max_k   = f32::MIN;
6425        let mut sum_k   = 0.0f32;
6426        let mut min_tau = f32::MAX;
6427        let mut max_tau = f32::MIN;
6428        let mut inflections = 0usize;
6429        let mut prev_sign   = 0i32;
6430        let mut bb_min = Vec3::splat(f32::MAX);
6431        let mut bb_max = Vec3::splat(f32::MIN);
6432
6433        let table = build_arc_length_table(samples * 4, &|t| spline.evaluate(t));
6434        let total  = table.last().map(|&(_, s)| s).unwrap_or(0.0);
6435
6436        for i in 0..=samples {
6437            let t     = i as f32 / samples as f32;
6438            let frame = spline.frenet_frame_at(t);
6439            min_k = min_k.min(frame.curvature);
6440            max_k = max_k.max(frame.curvature);
6441            sum_k += frame.curvature;
6442            min_tau = min_tau.min(frame.torsion);
6443            max_tau = max_tau.max(frame.torsion);
6444            let sign = if frame.torsion > 0.0 { 1i32 } else if frame.torsion < 0.0 { -1 } else { 0 };
6445            if prev_sign != 0 && sign != 0 && sign != prev_sign { inflections += 1; }
6446            if sign != 0 { prev_sign = sign; }
6447            let p = frame.position;
6448            bb_min = bb_min.min(p);
6449            bb_max = bb_max.max(p);
6450        }
6451
6452        let pts: Vec<Vec3> = (0..=samples).map(|i| spline.evaluate(i as f32 / samples as f32)).collect();
6453        let si = detect_self_intersection_coarse(&pts, 0.1);
6454
6455        SplineAnalysisReport {
6456            total_arc_length:    total,
6457            min_curvature:       if min_k  == f32::MAX { 0.0 } else { min_k },
6458            max_curvature:       if max_k  == f32::MIN { 0.0 } else { max_k },
6459            mean_curvature:      sum_k / (samples + 1) as f32,
6460            min_torsion:         if min_tau == f32::MAX { 0.0 } else { min_tau },
6461            max_torsion:         if max_tau == f32::MIN { 0.0 } else { max_tau },
6462            inflection_count:    inflections,
6463            control_point_count: spline.control_points.len(),
6464            self_intersection:   si,
6465            bounding_box_min:    if bb_min == Vec3::splat(f32::MAX) { Vec3::ZERO } else { bb_min },
6466            bounding_box_max:    if bb_max == Vec3::splat(f32::MIN) { Vec3::ZERO } else { bb_max },
6467        }
6468    }
6469
6470    pub fn summary(&self) -> String {
6471        format!(
6472            "Arc length: {:.3}  CPs: {}  k[{:.4},{:.4}] tau[{:.4},{:.4}]  inflections: {}  si: {}",
6473            self.total_arc_length, self.control_point_count,
6474            self.min_curvature, self.max_curvature,
6475            self.min_torsion, self.max_torsion,
6476            self.inflection_count, self.self_intersection
6477        )
6478    }
6479}
6480
6481fn detect_self_intersection_coarse(pts: &[Vec3], cell: f32) -> bool {
6482    let mut grid: HashMap<(i32, i32, i32), Vec<usize>> = HashMap::new();
6483    for (i, p) in pts.iter().enumerate() {
6484        let key = ((p.x / cell) as i32, (p.y / cell) as i32, (p.z / cell) as i32);
6485        grid.entry(key).or_default().push(i);
6486    }
6487    for indices in grid.values() {
6488        for &a in indices {
6489            for &b in indices {
6490                if b > a + 2 { return true; }
6491            }
6492        }
6493    }
6494    false
6495}
6496
6497// ============================================================
6498// FBM NOISE PERTURBATION
6499// ============================================================
6500
6501pub fn perturb_spline_fbm(spline: &mut CatmullRomSpline, amplitude: f32, frequency: f32, octaves: u32, seed: u32) {
6502    for (i, cp) in spline.control_points.iter_mut().enumerate() {
6503        let fi = i as f32 * frequency + seed as f32 * 1.618;
6504        let mut disp = Vec3::ZERO;
6505        let mut amp  = amplitude;
6506        let mut freq = 1.0f32;
6507        for _ in 0..octaves {
6508            disp.x += value_noise_1d(fi * freq + 0.0)  * amp;
6509            disp.y += value_noise_1d(fi * freq + 13.7) * amp;
6510            disp.z += value_noise_1d(fi * freq + 27.3) * amp;
6511            amp  *= 0.5;
6512            freq *= 2.0;
6513        }
6514        cp.position += disp;
6515    }
6516    spline.rebuild_arc_length_table();
6517}
6518
6519// ============================================================
6520// SPLINE MORPH TARGETS
6521// ============================================================
6522
6523#[derive(Clone, Debug)]
6524pub struct SplineMorphTarget {
6525    pub name:    String,
6526    pub offsets: Vec<Vec3>,
6527    pub weight:  f32,
6528}
6529
6530impl SplineMorphTarget {
6531    pub fn new(name: &str, base: &CatmullRomSpline) -> Self {
6532        let offsets = vec![Vec3::ZERO; base.control_points.len()];
6533        SplineMorphTarget { name: name.to_string(), offsets, weight: 0.0 }
6534    }
6535
6536    pub fn set_offset(&mut self, idx: usize, offset: Vec3) {
6537        if idx < self.offsets.len() { self.offsets[idx] = offset; }
6538    }
6539}
6540
6541pub fn apply_morph_targets(base: &CatmullRomSpline, morphs: &[SplineMorphTarget]) -> CatmullRomSpline {
6542    let mut result = base.clone();
6543    for m in morphs {
6544        for (i, cp) in result.control_points.iter_mut().enumerate() {
6545            if let Some(&off) = m.offsets.get(i) { cp.position += off * m.weight; }
6546        }
6547    }
6548    result.rebuild_arc_length_table();
6549    result
6550}
6551
6552// ============================================================
6553// SPLINE EVENT TRACK
6554// ============================================================
6555
6556#[derive(Clone, Debug)]
6557pub struct SplineEvent {
6558    pub id:        u64,
6559    pub name:      String,
6560    pub t:         f32,
6561    pub arc_s:     f32,
6562    pub payload:   String,
6563    pub triggered: bool,
6564}
6565
6566pub struct SplineEventTrack {
6567    pub events: Vec<SplineEvent>,
6568    next_id:    u64,
6569}
6570
6571impl SplineEventTrack {
6572    pub fn new() -> Self { SplineEventTrack { events: Vec::new(), next_id: 1 } }
6573
6574    pub fn add_event(&mut self, name: &str, t: f32, arc_s: f32, payload: &str) -> u64 {
6575        let id = self.next_id; self.next_id += 1;
6576        self.events.push(SplineEvent {
6577            id, name: name.to_string(), t, arc_s, payload: payload.to_string(), triggered: false
6578        });
6579        id
6580    }
6581
6582    pub fn reset(&mut self) { for e in &mut self.events { e.triggered = false; } }
6583
6584    pub fn poll(&mut self, prev_s: f32, cur_s: f32) -> Vec<SplineEvent> {
6585        let mut fired = Vec::new();
6586        for e in &mut self.events {
6587            if !e.triggered && e.arc_s >= prev_s && e.arc_s < cur_s {
6588                e.triggered = true;
6589                fired.push(e.clone());
6590            }
6591        }
6592        fired
6593    }
6594
6595    pub fn sort_by_t(&mut self) {
6596        self.events.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
6597    }
6598}
6599
6600// ============================================================
6601// WIND FORCE & COLLISION
6602// ============================================================
6603
6604pub fn apply_wind(dyn_chain: &mut SplineDynamics, wind_dir: Vec3, wind_speed: f32, drag_coeff: f32, dt: f32) {
6605    let wind_vel = wind_dir.normalize_or_zero() * wind_speed;
6606    for p in &mut dyn_chain.particles {
6607        if p.pinned { continue; }
6608        let rel  = wind_vel - p.velocity;
6609        let drag = rel * drag_coeff;
6610        p.velocity += drag * dt;
6611    }
6612}
6613
6614pub fn collide_with_sphere(dyn_chain: &mut SplineDynamics, centre: Vec3, radius: f32) {
6615    for p in &mut dyn_chain.particles {
6616        if p.pinned { continue; }
6617        let d   = p.position - centre;
6618        let len = d.length();
6619        if len < radius {
6620            p.position = centre + d.normalize_or_zero() * radius;
6621            let n  = d.normalize_or_zero();
6622            let vn = p.velocity.dot(n);
6623            if vn < 0.0 { p.velocity -= n * vn; }
6624        }
6625    }
6626}
6627
6628pub fn collide_with_plane_y(dyn_chain: &mut SplineDynamics, y: f32, restitution: f32) {
6629    for p in &mut dyn_chain.particles {
6630        if p.pinned { continue; }
6631        if p.position.y < y {
6632            p.position.y = y;
6633            if p.velocity.y < 0.0 { p.velocity.y = -p.velocity.y * restitution; }
6634        }
6635    }
6636}
6637
6638// ============================================================
6639// CSV SERIALISATION
6640// ============================================================
6641
6642pub fn spline_to_csv(spline: &CatmullRomSpline) -> String {
6643    let mut out = String::new();
6644    out.push_str(&format!("#catmull,alpha={},closed={}\n", spline.alpha, spline.closed));
6645    for cp in &spline.control_points {
6646        out.push_str(&format!("{},{},{},{}\n", cp.position.x, cp.position.y, cp.position.z, cp.weight));
6647    }
6648    out
6649}
6650
6651pub fn spline_from_csv(csv: &str) -> Result<CatmullRomSpline, String> {
6652    let mut alpha  = 0.5f32;
6653    let mut closed = false;
6654    let mut cps    = Vec::new();
6655    for line in csv.lines() {
6656        let line = line.trim();
6657        if line.is_empty() { continue; }
6658        if line.starts_with('#') {
6659            if let Some(a) = line.find("alpha=") {
6660                let rest = &line[a + 6..];
6661                let end  = rest.find(',').unwrap_or(rest.len());
6662                alpha    = rest[..end].parse().unwrap_or(0.5);
6663            }
6664            if line.contains("closed=true") { closed = true; }
6665            continue;
6666        }
6667        let parts: Vec<&str> = line.split(',').collect();
6668        if parts.len() < 3 { return Err(format!("Bad line: {}", line)); }
6669        let x: f32 = parts[0].parse().map_err(|e: std::num::ParseFloatError| e.to_string())?;
6670        let y: f32 = parts[1].parse().map_err(|e: std::num::ParseFloatError| e.to_string())?;
6671        let z: f32 = parts[2].parse().map_err(|e: std::num::ParseFloatError| e.to_string())?;
6672        let w: f32 = parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(1.0);
6673        cps.push(ControlPoint { position: Vec3::new(x, y, z), weight: w, ..ControlPoint::new(Vec3::new(x, y, z)) });
6674    }
6675    let pts: Vec<Vec3> = cps.iter().map(|c| c.position).collect();
6676    Ok(CatmullRomSpline::new(pts, alpha, closed))
6677}
6678
6679// ============================================================
6680// SPLINE EDITOR EXTENDED COMMANDS
6681// ============================================================
6682
6683impl SplineEditor {
6684    pub fn cmd_taubin_smooth(&mut self, id: u64, lambda: f32, mu: f32, iterations: u32) {
6685        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6686            let _ = spline.control_points.iter().map(|c| c.position).collect::<Vec<_>>();
6687            taubin_smooth_catmull(spline, lambda, mu, iterations);
6688        }
6689    }
6690
6691    pub fn cmd_equidistribute(&mut self, id: u64, new_count: usize) {
6692        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6693            let _ = spline.control_points.iter().map(|c| c.position).collect::<Vec<_>>();
6694            equidistribute_catmull(spline, new_count);
6695        }
6696    }
6697
6698    pub fn cmd_snap_grid(&mut self, id: u64, cell_size: f32) {
6699        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6700            let _ = spline.control_points.iter().map(|c| c.position).collect::<Vec<_>>();
6701            snap_to_grid(spline, cell_size);
6702        }
6703    }
6704
6705    pub fn cmd_mirror(&mut self, id: u64, axis: u8) {
6706        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6707            let _ = spline.control_points.iter().map(|c| c.position).collect::<Vec<_>>();
6708            match axis {
6709                0 => mirror_spline_x(spline),
6710                1 => mirror_spline_y(spline),
6711                _ => mirror_spline_z(spline),
6712            }
6713        }
6714    }
6715
6716    pub fn cmd_perturb_fbm(&mut self, id: u64, amplitude: f32, freq: f32, octaves: u32, seed: u32) {
6717        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6718            let _ = spline.control_points.iter().map(|c| c.position).collect::<Vec<_>>();
6719            perturb_spline_fbm(spline, amplitude, freq, octaves, seed);
6720        }
6721    }
6722
6723    pub fn cmd_translate(&mut self, id: u64, delta: Vec3) {
6724        if let Some(spline) = self.catmull_splines.get_mut(&id) { translate_spline(spline, delta); }
6725    }
6726
6727    pub fn cmd_rotate(&mut self, id: u64, rot: Quat) {
6728        if let Some(spline) = self.catmull_splines.get_mut(&id) { rotate_spline(spline, rot); }
6729    }
6730
6731    pub fn cmd_scale(&mut self, id: u64, scale: f32) {
6732        if let Some(spline) = self.catmull_splines.get_mut(&id) { scale_spline_uniform(spline, scale); }
6733    }
6734
6735    pub fn cmd_analyze(&self, id: u64, samples: usize) -> Option<SplineAnalysisReport> {
6736        self.catmull_splines.get(&id).map(|s| SplineAnalysisReport::compute(s, samples))
6737    }
6738
6739    pub fn cmd_fit_bezier(&mut self, id: u64, samples: usize, max_error: f32) -> Option<u64> {
6740        let dense = {
6741            let s = self.catmull_splines.get(&id)?;
6742            sample_uniform_arc_length(s, samples)
6743        };
6744        let bezier = fit_piecewise_cubic_bezier(&dense, max_error);
6745        let new_id = rand_id();
6746        self.bezier_splines.insert(new_id, bezier);
6747        Some(new_id)
6748    }
6749
6750    pub fn cmd_export_csv(&self) -> String {
6751        let mut out = String::new();
6752        for (id, spline) in &self.catmull_splines {
6753            out.push_str(&format!("## spline_id={}\n", id));
6754            out.push_str(&spline_to_csv(spline));
6755        }
6756        out
6757    }
6758
6759    pub fn cmd_import_csv(&mut self, csv: &str) {
6760        let mut current = String::new();
6761        for line in csv.lines() {
6762            if line.starts_with("## spline_id=") {
6763                if !current.is_empty() {
6764                    if let Ok(s) = spline_from_csv(&current) {
6765                        let id = rand_id();
6766                        self.catmull_splines.insert(id, s);
6767                    }
6768                    current.clear();
6769                }
6770            } else {
6771                current.push_str(line);
6772                current.push('\n');
6773            }
6774        }
6775        if !current.is_empty() {
6776            if let Ok(s) = spline_from_csv(&current) {
6777                let id = rand_id();
6778                self.catmull_splines.insert(id, s);
6779            }
6780        }
6781    }
6782
6783    pub fn spline_bounding_box(&self, id: u64) -> Option<(Vec3, Vec3)> {
6784        let s = self.catmull_splines.get(&id)?;
6785        let mut mn = Vec3::splat(f32::MAX);
6786        let mut mx = Vec3::splat(f32::MIN);
6787        for cp in &s.control_points { mn = mn.min(cp.position); mx = mx.max(cp.position); }
6788        if mn == Vec3::splat(f32::MAX) { None } else { Some((mn, mx)) }
6789    }
6790
6791    pub fn clear_all(&mut self) {
6792        self.catmull_splines.clear();
6793        self.bezier_splines.clear();
6794        self.bsplines.clear();
6795        self.nurbs_splines.clear();
6796        self.hermite_splines.clear();
6797        self.generated_meshes.clear();
6798    }
6799
6800    pub fn total_control_points(&self) -> usize {
6801        self.catmull_splines.values().map(|s| s.control_points.len()).sum()
6802    }
6803
6804    pub fn cmd_reverse(&mut self, id: u64) {
6805        if let Some(spline) = self.catmull_splines.get_mut(&id) {
6806            spline.control_points.reverse();
6807            spline.rebuild_arc_length_table();
6808        }
6809    }
6810
6811    pub fn cmd_duplicate(&mut self, id: u64, offset: Vec3) -> Option<u64> {
6812        let mut s = self.catmull_splines.get(&id)?.clone();
6813        translate_spline(&mut s, offset);
6814        let new_id = rand_id();
6815        self.catmull_splines.insert(new_id, s);
6816        Some(new_id)
6817    }
6818
6819    pub fn cmd_weld(&mut self, id_a: u64, id_b: u64, threshold: f32) -> Option<u64> {
6820        let a = self.catmull_splines.get(&id_a)?.clone();
6821        let b = self.catmull_splines.get(&id_b)?.clone();
6822        let end_a   = a.control_points.last()?.position;
6823        let start_b = b.control_points.first()?.position;
6824        if (end_a - start_b).length() > threshold { return None; }
6825        let joined = CatmullRomSpline::join(a, b);
6826        let new_id = rand_id();
6827        self.catmull_splines.insert(new_id, joined);
6828        Some(new_id)
6829    }
6830
6831    pub fn arc_length(&self, id: u64) -> f32 {
6832        self.catmull_splines.get(&id).map(|s| {
6833            let table = build_arc_length_table(512, &|t| s.evaluate(t));
6834            table.last().map(|&(_, l)| l).unwrap_or(0.0)
6835        }).unwrap_or(0.0)
6836    }
6837}
6838
6839// ============================================================
6840// ADVANCED UNIT TESTS
6841// ============================================================
6842
6843#[cfg(test)]
6844mod tests_spline_advanced {
6845    use super::*;
6846
6847    pub(super) fn simple_line(n: usize) -> CatmullRomSpline {
6848        CatmullRomSpline {
6849            control_points: (0..n).map(|i| ControlPoint {
6850                position: Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
6851            }).collect(),
6852            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6853        }
6854    }
6855
6856    #[test]
6857    fn test_laplacian_smooth_middle_moves() {
6858        let mut s = CatmullRomSpline {
6859            control_points: vec![
6860                ControlPoint { position: Vec3::new(0.0, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6861                ControlPoint { position: Vec3::new(1.0, 2.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6862                ControlPoint { position: Vec3::new(2.0, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6863            ],
6864            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6865        };
6866        laplacian_smooth_catmull(&mut s, 0.5, 1);
6867        assert!((s.control_points[1].position.y - 1.0).abs() < 0.01);
6868    }
6869
6870    #[test]
6871    fn test_equidistribute_correct_count() {
6872        let mut s = simple_line(10);
6873        equidistribute_catmull(&mut s, 5);
6874        assert_eq!(s.control_points.len(), 5);
6875    }
6876
6877    #[test]
6878    fn test_fit_cubic_bezier_endpoints() {
6879        let pts = vec![Vec3::new(0.0,0.0,0.0), Vec3::new(0.5,1.0,0.0), Vec3::new(1.0,0.0,0.0)];
6880        let seg = fit_cubic_bezier(&pts);
6881        assert!((seg[0] - Vec3::new(0.0,0.0,0.0)).length() < 1e-5);
6882        assert!((seg[3] - Vec3::new(1.0,0.0,0.0)).length() < 1e-5);
6883    }
6884
6885    #[test]
6886    fn test_spline_csv_round_trip() {
6887        let s = CatmullRomSpline {
6888            control_points: vec![
6889                ControlPoint { position: Vec3::new(1.0,2.0,3.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6890                ControlPoint { position: Vec3::new(4.0,5.0,6.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6891            ],
6892            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6893        };
6894        let csv = spline_to_csv(&s);
6895        let s2  = spline_from_csv(&csv).unwrap();
6896        assert_eq!(s2.control_points.len(), 2);
6897        assert!((s2.control_points[0].position - Vec3::new(1.0,2.0,3.0)).length() < 1e-4);
6898    }
6899
6900    #[test]
6901    fn test_spline_dynamics_gravity_falls() {
6902        let s = CatmullRomSpline {
6903            control_points: vec![
6904                ControlPoint { position: Vec3::new(0.0,10.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6905                ControlPoint { position: Vec3::new(1.0,10.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6906            ],
6907            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6908        };
6909        let mut chain = SplineDynamics::from_catmull(&s, 100.0, 0.1);
6910        chain.particles[0].pinned = true;
6911        chain.step(0.016);
6912        assert!(chain.particles[1].position.y < 10.0);
6913    }
6914
6915    #[test]
6916    fn test_ribbon_mesh_vertex_count() {
6917        let s = simple_line(3);
6918        let ribbon = RibbonMesh::generate(&s, 8, &|_| 0.1);
6919        assert_eq!(ribbon.vertices.len(), (8 + 1) * 2);
6920    }
6921
6922    #[test]
6923    fn test_analysis_report_positive_arc_length() {
6924        let s = simple_line(5);
6925        let rep = SplineAnalysisReport::compute(&s, 64);
6926        assert!(rep.total_arc_length > 0.0);
6927    }
6928
6929    #[test]
6930    fn test_snap_to_grid_rounds() {
6931        let mut s = CatmullRomSpline {
6932            control_points: vec![ControlPoint { position: Vec3::new(0.3,1.7,-0.1), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) }],
6933            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6934        };
6935        snap_to_grid(&mut s, 1.0);
6936        assert!((s.control_points[0].position.x).abs() < 1e-5);
6937        assert!((s.control_points[0].position.y - 2.0).abs() < 1e-5);
6938    }
6939
6940    #[test]
6941    fn test_clothoid_sample_count() {
6942        let pts = sample_clothoid(1.0, 50, false);
6943        assert_eq!(pts.len(), 50);
6944        assert!(pts[0].length() < 0.01);
6945    }
6946
6947    #[test]
6948    fn test_offset_spline_xz_point_count() {
6949        let s   = simple_line(5);
6950        let off = offset_spline_xz(&s, 0.5, 20);
6951        assert_eq!(off.control_points.len(), 20);
6952    }
6953
6954    #[test]
6955    fn test_fabrik_reaches_target() {
6956        let joints = vec![Vec3::ZERO, Vec3::new(1.0,0.0,0.0), Vec3::new(2.0,0.0,0.0)];
6957        let mut chain = SplineIKChain::new(joints);
6958        let target = Vec3::new(1.5, 1.0, 0.0);
6959        chain.solve_fabrik(target, 20, 1e-3);
6960        let end = *chain.joints.last().unwrap();
6961        assert!((end - target).length() < 0.05);
6962    }
6963
6964    #[test]
6965    fn test_spline_lattice_midpoint() {
6966        let rail_a = CatmullRomSpline {
6967            control_points: vec![
6968                ControlPoint { position: Vec3::new(0.0,0.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6969                ControlPoint { position: Vec3::new(1.0,0.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6970            ],
6971            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6972        };
6973        let rail_b = CatmullRomSpline {
6974            control_points: vec![
6975                ControlPoint { position: Vec3::new(0.0,1.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6976                ControlPoint { position: Vec3::new(1.0,1.0,0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO) },
6977            ],
6978            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
6979        };
6980        let lattice = SplineLattice { rail_a, rail_b };
6981        let mid = lattice.evaluate(0.0, 0.5);
6982        assert!((mid.y - 0.5).abs() < 0.01);
6983    }
6984
6985    #[test]
6986    fn test_mirror_spline_x_reverses() {
6987        let mut s = simple_line(3);
6988        let orig_last = s.control_points.last().unwrap().position;
6989        mirror_spline_x(&mut s);
6990        let new_first = s.control_points.first().unwrap().position;
6991        assert!((new_first.x + orig_last.x).abs() < 1e-5);
6992    }
6993
6994    #[test]
6995    fn test_spline_total_variation_positive() {
6996        let s = simple_line(4);
6997        let tv = spline_total_variation(&s);
6998        assert!(tv > 0.0);
6999    }
7000}
7001
7002// ============================================================
7003// SPLINE SEGMENT ANNOTATOR (metadata per segment)
7004// ============================================================
7005
7006#[derive(Clone, Debug, Default)]
7007pub struct SegmentAnnotation {
7008    pub label:       String,
7009    pub speed_limit: f32,
7010    pub terrain_tag: String,
7011    pub danger:      bool,
7012}
7013
7014pub struct SplineAnnotator {
7015    pub annotations: Vec<(f32, f32, SegmentAnnotation)>,  // (t_start, t_end, data)
7016}
7017
7018impl SplineAnnotator {
7019    pub fn new() -> Self { SplineAnnotator { annotations: Vec::new() } }
7020
7021    pub fn add(&mut self, t_start: f32, t_end: f32, ann: SegmentAnnotation) {
7022        self.annotations.push((t_start.min(t_end), t_start.max(t_end), ann));
7023    }
7024
7025    pub fn query(&self, t: f32) -> Vec<&SegmentAnnotation> {
7026        self.annotations.iter()
7027            .filter(|(s, e, _)| t >= *s && t <= *e)
7028            .map(|(_, _, ann)| ann)
7029            .collect()
7030    }
7031
7032    pub fn speed_limit_at(&self, t: f32) -> f32 {
7033        self.query(t).iter()
7034            .map(|a| a.speed_limit)
7035            .fold(f32::MAX, f32::min)
7036    }
7037}
7038
7039// ============================================================
7040// SPLINE BUNDLE (set of parallel splines)
7041// ============================================================
7042
7043pub struct SplineBundle {
7044    pub splines:   Vec<CatmullRomSpline>,
7045    pub offsets:   Vec<f32>,  // lateral offset per lane
7046    pub separator: f32,       // world-space lane width
7047}
7048
7049impl SplineBundle {
7050    /// Build a bundle from a centre-line and evenly-spaced offsets.
7051    pub fn from_centre(centre: &CatmullRomSpline, lane_count: usize, lane_width: f32, samples: usize) -> Self {
7052        let half = (lane_count as f32 - 1.0) * 0.5 * lane_width;
7053        let mut splines = Vec::new();
7054        let mut offsets = Vec::new();
7055        for i in 0..lane_count {
7056            let off = i as f32 * lane_width - half;
7057            offsets.push(off);
7058            splines.push(offset_spline_xz(centre, off, samples));
7059        }
7060        SplineBundle { splines, offsets, separator: lane_width }
7061    }
7062
7063    pub fn lane_count(&self) -> usize { self.splines.len() }
7064
7065    pub fn evaluate(&self, lane: usize, t: f32) -> Option<Vec3> {
7066        self.splines.get(lane).map(|s| s.evaluate(t))
7067    }
7068
7069    pub fn nearest_lane(&self, point: Vec3) -> usize {
7070        let mut best = 0;
7071        let mut best_dist = f32::MAX;
7072        for (i, s) in self.splines.iter().enumerate() {
7073            let (t, _) = s.nearest_point(point);
7074            let d = (s.evaluate(t) - point).length();
7075            if d < best_dist { best_dist = d; best = i; }
7076        }
7077        best
7078    }
7079}
7080
7081// ============================================================
7082// SPLINE PREVIEW WIDGET DATA
7083// ============================================================
7084
7085/// A compact view of a spline for UI display: discretised polyline + tangent arrows.
7086pub struct SplinePreviewData {
7087    pub polyline:   Vec<Vec3>,
7088    pub tangents:   Vec<Vec3>,
7089    pub normals:    Vec<Vec3>,
7090    pub curvatures: Vec<f32>,
7091}
7092
7093impl SplinePreviewData {
7094    pub fn from_spline(spline: &CatmullRomSpline, resolution: usize) -> Self {
7095        let mut polyline   = Vec::with_capacity(resolution + 1);
7096        let mut tangents   = Vec::with_capacity(resolution + 1);
7097        let mut normals    = Vec::with_capacity(resolution + 1);
7098        let mut curvatures = Vec::with_capacity(resolution + 1);
7099        for i in 0..=resolution {
7100            let t     = i as f32 / resolution as f32;
7101            let frame = spline.frenet_frame_at(t);
7102            polyline.push(frame.position);
7103            tangents.push(frame.tangent);
7104            normals.push(frame.normal);
7105            curvatures.push(frame.curvature);
7106        }
7107        SplinePreviewData { polyline, tangents, normals, curvatures }
7108    }
7109
7110    /// Average curvature.
7111    pub fn mean_curvature(&self) -> f32 {
7112        if self.curvatures.is_empty() { return 0.0; }
7113        self.curvatures.iter().sum::<f32>() / self.curvatures.len() as f32
7114    }
7115
7116    /// Axis-aligned bounding box.
7117    pub fn aabb(&self) -> (Vec3, Vec3) {
7118        let mut mn = Vec3::splat(f32::MAX);
7119        let mut mx = Vec3::splat(f32::MIN);
7120        for &p in &self.polyline { mn = mn.min(p); mx = mx.max(p); }
7121        if mn == Vec3::splat(f32::MAX) { (Vec3::ZERO, Vec3::ZERO) } else { (mn, mx) }
7122    }
7123}
7124
7125// ============================================================
7126// SPLINE PAINT TOOL (stroke to spline conversion)
7127// ============================================================
7128
7129/// Convert a sequence of raw stroke samples (mouse / pen) to a smoothed CatmullRom spline.
7130pub struct SplinePaintTool {
7131    pub raw_samples:    Vec<Vec3>,
7132    pub simplify_eps:   f32,
7133    pub smooth_passes:  u32,
7134    pub smooth_lambda:  f32,
7135}
7136
7137impl SplinePaintTool {
7138    pub fn new(simplify_eps: f32, smooth_passes: u32, smooth_lambda: f32) -> Self {
7139        SplinePaintTool { raw_samples: Vec::new(), simplify_eps, smooth_passes, smooth_lambda }
7140    }
7141
7142    pub fn add_sample(&mut self, p: Vec3) {
7143        self.raw_samples.push(p);
7144    }
7145
7146    /// Finalise: simplify + smooth → CatmullRomSpline.
7147    pub fn finish(&mut self) -> CatmullRomSpline {
7148        let simplified = simplify_polyline(&self.raw_samples, self.simplify_eps);
7149        let mut spline = CatmullRomSpline::new(simplified, 0.5, false);
7150        laplacian_smooth_catmull(&mut spline, self.smooth_lambda, self.smooth_passes);
7151        self.raw_samples.clear();
7152        spline
7153    }
7154}
7155
7156// ============================================================
7157// SPLINE COORDINATE FRAME SEQUENCE
7158// ============================================================
7159
7160/// A pre-baked sequence of Frenet frames at uniform arc-length intervals.
7161pub struct BakedFrenetFrames {
7162    pub frames:      Vec<FrenetFrame>,
7163    pub arc_step:    f32,
7164    pub total_length: f32,
7165}
7166
7167impl BakedFrenetFrames {
7168    pub fn bake(spline: &CatmullRomSpline, samples: usize) -> Self {
7169        let table       = build_arc_length_table(samples * 4, &|t| spline.evaluate(t));
7170        let total_length = table.last().map(|&(_, s)| s).unwrap_or(0.0);
7171        let arc_step    = if samples > 1 { total_length / (samples - 1) as f32 } else { 0.0 };
7172        let frames: Vec<FrenetFrame> = (0..samples).map(|i| {
7173            let s = i as f32 * arc_step;
7174            let t = arc_length_to_t(&table, s);
7175            spline.frenet_frame_at(t)
7176        }).collect();
7177        BakedFrenetFrames { frames, arc_step, total_length }
7178    }
7179
7180    /// Sample a frame at arc-length position `s` using linear interpolation.
7181    pub fn sample(&self, s: f32) -> Option<FrenetFrame> {
7182        if self.frames.is_empty() || self.arc_step < 1e-10 { return None; }
7183        let idx = (s / self.arc_step) as usize;
7184        if idx + 1 >= self.frames.len() { return self.frames.last().cloned(); }
7185        let t   = (s / self.arc_step) - idx as f32;
7186        let a   = &self.frames[idx];
7187        let b   = &self.frames[idx + 1];
7188        Some(FrenetFrame {
7189            position:  a.position.lerp(b.position, t),
7190            tangent:   a.tangent.lerp(b.tangent, t).normalize_or_zero(),
7191            normal:    a.normal.lerp(b.normal, t).normalize_or_zero(),
7192            binormal:  a.binormal.lerp(b.binormal, t).normalize_or_zero(),
7193            curvature: a.curvature + (b.curvature - a.curvature) * t,
7194            torsion:   a.torsion   + (b.torsion   - a.torsion)   * t,
7195        })
7196    }
7197}
7198
7199// ============================================================
7200// SPLINE OUTLINE (expanded polygon)
7201// ============================================================
7202
7203/// Generate a 2-D outline (two parallel polylines) from a spline in the XZ plane.
7204pub fn spline_outline_xz(spline: &CatmullRomSpline, half_width: f32, samples: usize) -> (Vec<Vec3>, Vec<Vec3>) {
7205    let table = build_arc_length_table(samples * 4, &|t| spline.evaluate(t));
7206    let total  = table.last().map(|&(_, s)| s).unwrap_or(0.0);
7207    let mut left  = Vec::with_capacity(samples);
7208    let mut right = Vec::with_capacity(samples);
7209    for i in 0..samples {
7210        let s  = total * i as f32 / (samples - 1).max(1) as f32;
7211        let t  = arc_length_to_t(&table, s);
7212        let p  = spline.evaluate(t);
7213        let tn = spline.evaluate_derivative(t);
7214        let n  = Vec3::new(-tn.z, 0.0, tn.x).normalize_or_zero();
7215        left.push(p - n * half_width);
7216        right.push(p + n * half_width);
7217    }
7218    (left, right)
7219}
7220
7221// ============================================================
7222// SPLINE INTERSECTION UTILITIES (standalone)
7223// ============================================================
7224
7225/// Fast segment-segment distance (3-D); returns squared minimum distance.
7226pub fn segment_segment_dist_sq(p0: Vec3, p1: Vec3, q0: Vec3, q1: Vec3) -> f32 {
7227    let d1 = p1 - p0;
7228    let d2 = q1 - q0;
7229    let r  = p0 - q0;
7230    let a  = d1.dot(d1);
7231    let e  = d2.dot(d2);
7232    let f  = d2.dot(r);
7233    let (s, t);
7234    if a < 1e-10 && e < 1e-10 {
7235        s = 0.0; t = 0.0;
7236    } else if a < 1e-10 {
7237        s = 0.0; t = (f / e).clamp(0.0, 1.0);
7238    } else {
7239        let c = d1.dot(r);
7240        if e < 1e-10 {
7241            t = 0.0; s = (-c / a).clamp(0.0, 1.0);
7242        } else {
7243            let b  = d1.dot(d2);
7244            let denom = a * e - b * b;
7245            s = if denom.abs() > 1e-10 { ((b * f - c * e) / denom).clamp(0.0, 1.0) } else { 0.0 };
7246            t = (b * s + f) / e;
7247            let (ss, tt);
7248            if t < 0.0 {
7249                tt = 0.0; ss = (-c / a).clamp(0.0, 1.0);
7250            } else if t > 1.0 {
7251                tt = 1.0; ss = ((b - c) / a).clamp(0.0, 1.0);
7252            } else {
7253                ss = s; tt = t;
7254            }
7255            let _ = (s, t);
7256            let cp1 = p0 + d1 * ss;
7257            let cp2 = q0 + d2 * tt;
7258            return (cp1 - cp2).length_squared();
7259        }
7260    }
7261    let cp1 = p0 + d1 * s;
7262    let cp2 = q0 + d2 * t;
7263    (cp1 - cp2).length_squared()
7264}
7265
7266/// Find approximate intersection parameter pairs between two CatmullRom splines.
7267pub fn spline_spline_intersection_params(
7268    a: &CatmullRomSpline,
7269    b: &CatmullRomSpline,
7270    coarse_steps: usize,
7271    tol: f32,
7272) -> Vec<(f32, f32)> {
7273    let mut candidates = Vec::new();
7274    let step = 1.0 / coarse_steps as f32;
7275    for i in 0..coarse_steps {
7276        for j in 0..coarse_steps {
7277            let ta0 = i as f32 * step;
7278            let ta1 = ta0 + step;
7279            let tb0 = j as f32 * step;
7280            let tb1 = tb0 + step;
7281            let pa0 = a.evaluate(ta0); let pa1 = a.evaluate(ta1);
7282            let pb0 = b.evaluate(tb0); let pb1 = b.evaluate(tb1);
7283            if segment_segment_dist_sq(pa0, pa1, pb0, pb1) < tol * tol {
7284                candidates.push(((ta0 + ta1) * 0.5, (tb0 + tb1) * 0.5));
7285            }
7286        }
7287    }
7288
7289    // Newton-Raphson refinement
7290    let mut results = Vec::new();
7291    for (mut ta, mut tb) in candidates {
7292        for _ in 0..20 {
7293            let fa  = a.evaluate(ta);
7294            let fb  = b.evaluate(tb);
7295            let dfa = a.evaluate_derivative(ta);
7296            let dfb = b.evaluate_derivative(tb);
7297            let res = fa - fb;
7298            // J = [dfa | -dfb], solve 2x2 least-squares in XZ
7299            let j00 =  dfa.x; let j01 = -dfb.x;
7300            let j10 =  dfa.z; let j11 = -dfb.z;
7301            let det = j00 * j11 - j01 * j10;
7302            if det.abs() < 1e-10 { break; }
7303            let dta = ( j11 * res.x - j01 * res.z) / det;
7304            let dtb = (-j10 * res.x + j00 * res.z) / det;
7305            ta -= dta;
7306            tb -= dtb;
7307            ta = ta.clamp(0.0, 1.0);
7308            tb = tb.clamp(0.0, 1.0);
7309            if dta.abs() < 1e-6 && dtb.abs() < 1e-6 { break; }
7310        }
7311        let dist = (a.evaluate(ta) - b.evaluate(tb)).length();
7312        if dist < tol * 2.0 { results.push((ta, tb)); }
7313    }
7314    results
7315}
7316
7317// ============================================================
7318// MORE UNIT TESTS
7319// ============================================================
7320
7321#[cfg(test)]
7322mod tests_spline_extra {
7323    use super::*;
7324
7325    #[test]
7326    fn test_speed_profile_linear_interpolation() {
7327        let p = SpeedProfile { keyframes: vec![(0.0, 0.0), (1.0, 10.0)] };
7328        assert!((p.evaluate(0.5) - 5.0).abs() < 0.01);
7329    }
7330
7331    #[test]
7332    fn test_speed_profile_total_time_positive() {
7333        // A profile that starts at rest never leaves t = 0, so start moving.
7334        let p = SpeedProfile::ease_in_out(5.0, 20.0, 5.0);
7335        let t = p.time_to_t(100.0, 1.0, 0.01);
7336        assert!(t > 0.0);
7337    }
7338
7339    #[test]
7340    fn test_spline_bundle_lane_count() {
7341        let centre = CatmullRomSpline {
7342            control_points: (0..4).map(|i| crate::editor::spline_editor::ControlPoint {
7343                position: glam::Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
7344            }).collect(),
7345            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
7346        };
7347        let bundle = SplineBundle::from_centre(&centre, 3, 1.0, 32);
7348        assert_eq!(bundle.lane_count(), 3);
7349    }
7350
7351    #[test]
7352    fn test_preview_data_aabb() {
7353        let s = super::tests_spline_advanced::simple_line(4);
7354        let preview = SplinePreviewData::from_spline(&s, 32);
7355        let (mn, mx) = preview.aabb();
7356        assert!(mx.x > mn.x || mx.y >= mn.y);
7357    }
7358
7359    #[test]
7360    fn test_outline_xz_point_count() {
7361        let s = super::tests_spline_advanced::simple_line(4);
7362        let (left, right) = spline_outline_xz(&s, 0.5, 16);
7363        assert_eq!(left.len(), 16);
7364        assert_eq!(right.len(), 16);
7365    }
7366
7367    #[test]
7368    fn test_baked_frenet_sample() {
7369        let s = super::tests_spline_advanced::simple_line(5);
7370        let baked = BakedFrenetFrames::bake(&s, 64);
7371        let f = baked.sample(baked.total_length * 0.5);
7372        assert!(f.is_some());
7373    }
7374
7375    #[test]
7376    fn test_segment_segment_dist_sq_parallel() {
7377        let d = segment_segment_dist_sq(
7378            Vec3::new(0.0,0.0,0.0), Vec3::new(1.0,0.0,0.0),
7379            Vec3::new(0.0,1.0,0.0), Vec3::new(1.0,1.0,0.0),
7380        );
7381        assert!((d - 1.0).abs() < 0.01);
7382    }
7383}
7384
7385// ============================================================
7386// SPLINE GRADIENT MATERIAL (colour ramp along arc-length)
7387// ============================================================
7388
7389#[derive(Clone, Debug)]
7390pub struct SplineColorKey {
7391    pub t:     f32,
7392    pub color: Vec4,
7393}
7394
7395pub struct SplineColorRamp {
7396    pub keys: Vec<SplineColorKey>,
7397}
7398
7399impl SplineColorRamp {
7400    pub fn new() -> Self { SplineColorRamp { keys: Vec::new() } }
7401
7402    pub fn add_key(&mut self, t: f32, color: Vec4) {
7403        let pos = self.keys.partition_point(|k| k.t < t);
7404        self.keys.insert(pos, SplineColorKey { t, color });
7405    }
7406
7407    pub fn evaluate(&self, t: f32) -> Vec4 {
7408        if self.keys.is_empty() { return Vec4::ONE; }
7409        let t = t.clamp(0.0, 1.0);
7410        let idx = self.keys.partition_point(|k| k.t <= t);
7411        if idx == 0 { return self.keys[0].color; }
7412        if idx >= self.keys.len() { return self.keys.last().unwrap().color; }
7413        let a  = &self.keys[idx - 1];
7414        let b  = &self.keys[idx];
7415        let f  = if (b.t - a.t).abs() < 1e-10 { 0.0 } else { (t - a.t) / (b.t - a.t) };
7416        a.color.lerp(b.color, f)
7417    }
7418}
7419
7420// ============================================================
7421// SPLINE MEASUREMENT TOOLS
7422// ============================================================
7423
7424/// Distance from an external point to the nearest point on a spline.
7425pub fn point_to_spline_distance(spline: &CatmullRomSpline, point: Vec3, steps: usize) -> f32 {
7426    let (t, _) = spline.nearest_point(point);
7427    let np = spline.evaluate(t);
7428    (point - np).length()
7429}
7430
7431/// Closest pair of points between two splines.
7432pub fn spline_spline_min_distance(a: &CatmullRomSpline, b: &CatmullRomSpline, steps: usize) -> (f32, f32, f32) {
7433    let mut min_dist = f32::MAX;
7434    let mut best_ta  = 0.0f32;
7435    let mut best_tb  = 0.0f32;
7436    for i in 0..=steps {
7437        let ta = i as f32 / steps as f32;
7438        let pa = a.evaluate(ta);
7439        let (tb_best, _) = b.nearest_point(pa);
7440        let pb_best = b.evaluate(tb_best);
7441        let d = (pa - pb_best).length();
7442        if d < min_dist { min_dist = d; best_ta = ta; best_tb = tb_best; }
7443    }
7444    (min_dist, best_ta, best_tb)
7445}
7446
7447// ============================================================
7448// SPLINE SUBDIVISION (uniform T)
7449// ============================================================
7450
7451/// Insert `n` new control points via uniform T subdivision.
7452pub fn subdivide_catmull(spline: &mut CatmullRomSpline, n: u32) {
7453    for _ in 0..n {
7454        let old: Vec<Vec3> = spline.control_points.iter().map(|cp| cp.position).collect();
7455        if old.len() < 2 { break; }
7456        let mut new_pts = Vec::with_capacity(old.len() * 2 - 1);
7457        for i in 0..old.len() - 1 {
7458            new_pts.push(old[i]);
7459            new_pts.push((old[i] + old[i + 1]) * 0.5);
7460        }
7461        new_pts.push(*old.last().unwrap());
7462        spline.control_points = new_pts.into_iter().map(|p| ControlPoint::new(p)).collect();
7463    }
7464    spline.rebuild_arc_length_table();
7465}
7466
7467// ============================================================
7468// SPLINE META-DATA STORE
7469// ============================================================
7470
7471/// Arbitrary key-value metadata attached to a spline.
7472pub struct SplineMetadata {
7473    pub data: HashMap<String, String>,
7474}
7475
7476impl SplineMetadata {
7477    pub fn new() -> Self { SplineMetadata { data: HashMap::new() } }
7478
7479    pub fn set(&mut self, key: &str, value: &str) { self.data.insert(key.to_string(), value.to_string()); }
7480
7481    pub fn get(&self, key: &str) -> Option<&str> { self.data.get(key).map(String::as_str) }
7482
7483    pub fn get_f32(&self, key: &str) -> Option<f32> { self.data.get(key)?.parse().ok() }
7484
7485    pub fn get_bool(&self, key: &str) -> bool {
7486        self.data.get(key).map(|v| v == "true").unwrap_or(false)
7487    }
7488}
7489
7490// ============================================================
7491// SPLINE CAMERA PATH EVALUATOR
7492// ============================================================
7493
7494/// Evaluate a camera's world-space position and look-at target along a camera rail.
7495pub fn camera_path_evaluate(
7496    position_spline: &CatmullRomSpline,
7497    target_spline:   &CatmullRomSpline,
7498    t:               f32,
7499) -> (Vec3, Vec3, Mat4) {
7500    let pos    = position_spline.evaluate(t);
7501    let target = target_spline.evaluate(t);
7502    let forward = (target - pos).normalize_or_zero();
7503    let up      = Vec3::Y;
7504    let right   = forward.cross(up).normalize_or_zero();
7505    let true_up = right.cross(forward).normalize_or_zero();
7506    let mat = Mat4::from_cols(
7507        right.extend(0.0),
7508        true_up.extend(0.0),
7509        (-forward).extend(0.0),
7510        pos.extend(1.0),
7511    );
7512    (pos, target, mat)
7513}
7514
7515// ============================================================
7516// SPLINE REPARAMETERISATION BY CURVATURE
7517// ============================================================
7518
7519/// Reparameterise spline samples so high-curvature regions get more samples.
7520pub fn reparametrise_by_curvature(
7521    spline: &CatmullRomSpline,
7522    n:      usize,
7523    weight: f32,
7524) -> Vec<f32> {
7525    // Build curvature density: density(t) = 1 + weight * kappa(t)
7526    let raw: Vec<(f32, f32)> = (0..=n * 4).map(|i| {
7527        let t = i as f32 / (n * 4) as f32;
7528        let kappa = spline.frenet_frame_at(t).curvature;
7529        (t, 1.0 + weight * kappa)
7530    }).collect();
7531
7532    // Integrate density to get cumulative weight
7533    let mut cum: Vec<f32> = Vec::with_capacity(raw.len());
7534    let mut acc = 0.0f32;
7535    cum.push(0.0);
7536    for i in 1..raw.len() {
7537        let dt = raw[i].0 - raw[i - 1].0;
7538        acc += (raw[i - 1].1 + raw[i].1) * 0.5 * dt;
7539        cum.push(acc);
7540    }
7541    let total = acc;
7542    if total < 1e-10 { return (0..n).map(|i| i as f32 / (n - 1) as f32).collect(); }
7543
7544    // Sample n points uniformly in cumulative weight space
7545    (0..n).map(|i| {
7546        let target = total * i as f32 / (n - 1).max(1) as f32;
7547        let idx    = cum.partition_point(|&c| c < target).min(cum.len() - 1);
7548        if idx == 0 { return raw[0].0; }
7549        let c0 = cum[idx - 1];
7550        let c1 = cum[idx];
7551        let f  = if (c1 - c0).abs() < 1e-10 { 0.0 } else { (target - c0) / (c1 - c0) };
7552        raw[idx - 1].0 + (raw[idx].0 - raw[idx - 1].0) * f
7553    }).collect()
7554}
7555
7556// ============================================================
7557// FINAL UNIT TESTS
7558// ============================================================
7559
7560#[cfg(test)]
7561mod tests_final {
7562    use super::*;
7563
7564    pub(super) fn simple_line(n: usize) -> CatmullRomSpline {
7565        CatmullRomSpline {
7566            control_points: (0..n).map(|i| ControlPoint {
7567                position: Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
7568            }).collect(),
7569            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
7570        }
7571    }
7572
7573    #[test]
7574    fn test_color_ramp_midpoint() {
7575        let mut ramp = SplineColorRamp::new();
7576        ramp.add_key(0.0, Vec4::ZERO);
7577        ramp.add_key(1.0, Vec4::ONE);
7578        let mid = ramp.evaluate(0.5);
7579        assert!((mid.x - 0.5).abs() < 0.01);
7580    }
7581
7582    #[test]
7583    fn test_point_to_spline_distance() {
7584        let s = simple_line(3);
7585        let d = point_to_spline_distance(&s, Vec3::new(1.0, 1.0, 0.0), 64);
7586        assert!((d - 1.0).abs() < 0.05);
7587    }
7588
7589    #[test]
7590    fn test_subdivide_catmull_doubles_count() {
7591        let mut s = simple_line(4);
7592        let n_before = s.control_points.len();
7593        subdivide_catmull(&mut s, 1);
7594        assert_eq!(s.control_points.len(), n_before * 2 - 1);
7595    }
7596
7597    #[test]
7598    fn test_spline_metadata_set_get() {
7599        let mut m = SplineMetadata::new();
7600        m.set("name", "river");
7601        m.set("width", "3.5");
7602        assert_eq!(m.get("name"), Some("river"));
7603        assert!((m.get_f32("width").unwrap() - 3.5).abs() < 1e-5);
7604    }
7605
7606    #[test]
7607    fn test_reparametrise_count() {
7608        let s = simple_line(5);
7609        let ts = reparametrise_by_curvature(&s, 20, 2.0);
7610        assert_eq!(ts.len(), 20);
7611        assert!(*ts.first().unwrap() >= 0.0);
7612        assert!(*ts.last().unwrap() <= 1.0 + 1e-5);
7613    }
7614
7615    #[test]
7616    fn test_camera_path_mat4_finite() {
7617        let ps = simple_line(3);
7618        let ts = simple_line(3); // target slightly offset
7619        let (pos, target, mat) = camera_path_evaluate(&ps, &ts, 0.5);
7620        assert!(pos.is_finite());
7621        assert!(target.is_finite());
7622        for col in mat.to_cols_array() { assert!(col.is_finite()); }
7623    }
7624
7625    #[test]
7626    fn test_spline_spline_min_distance_self_zero() {
7627        let s = simple_line(4);
7628        let (d, _ta, _tb) = spline_spline_min_distance(&s, &s, 32);
7629        assert!(d < 0.01);
7630    }
7631}
7632
7633// ============================================================
7634// SPLINE WAYPOINT TRACKER
7635// ============================================================
7636
7637/// A runtime tracker that advances a "current t" parameter along a spline
7638/// at a given world-space speed, firing waypoint events.
7639pub struct SplineWaypointTracker {
7640    pub t:            f32,
7641    pub speed:        f32,
7642    pub arc_length:   f32,
7643    pub arc_table:    Vec<(f32, f32)>,
7644    pub waypoints:    Vec<(f32, String)>,  // (arc-s, label)
7645    pub fired:        Vec<bool>,
7646    pub loop_mode:    bool,
7647}
7648
7649impl SplineWaypointTracker {
7650    pub fn new(spline: &CatmullRomSpline, speed: f32, loop_mode: bool) -> Self {
7651        let arc_table   = build_arc_length_table(1024, &|t| spline.evaluate(t));
7652        let arc_length  = arc_table.last().map(|&(_, s)| s).unwrap_or(0.0);
7653        SplineWaypointTracker { t: 0.0, speed, arc_length, arc_table, waypoints: Vec::new(), fired: Vec::new(), loop_mode }
7654    }
7655
7656    pub fn add_waypoint(&mut self, arc_s: f32, label: &str) {
7657        self.waypoints.push((arc_s, label.to_string()));
7658        self.fired.push(false);
7659    }
7660
7661    /// Advance by dt seconds; return list of newly-fired waypoint labels.
7662    pub fn advance(&mut self, dt: f32) -> Vec<String> {
7663        if self.arc_length < 1e-6 { return Vec::new(); }
7664        let prev_s = arc_length_to_t(&self.arc_table, 0.0); // placeholder, use arc
7665        let cur_s  = {
7666            let t_cur = self.t;
7667            // Find arc-s corresponding to current t via reverse lookup
7668            let idx = self.arc_table.partition_point(|&(ti, _)| ti <= t_cur);
7669            if idx == 0 { 0.0 } else if idx >= self.arc_table.len() {
7670                self.arc_table.last().unwrap().1
7671            } else {
7672                let (t0, s0) = self.arc_table[idx - 1];
7673                let (t1, s1) = self.arc_table[idx];
7674                let f = if (t1 - t0).abs() < 1e-10 { 0.0 } else { (t_cur - t0) / (t1 - t0) };
7675                s0 + (s1 - s0) * f
7676            }
7677        };
7678        let ds   = self.speed * dt;
7679        let new_s = (cur_s + ds).min(if self.loop_mode { f32::MAX } else { self.arc_length });
7680        let new_s_wrapped = new_s % self.arc_length;
7681        self.t    = arc_length_to_t(&self.arc_table, new_s_wrapped);
7682
7683        let mut fired = Vec::new();
7684        for (i, &(wp_s, ref label)) in self.waypoints.iter().enumerate() {
7685            if !self.fired[i] && cur_s < wp_s && new_s >= wp_s {
7686                self.fired[i] = true;
7687                fired.push(label.clone());
7688            }
7689        }
7690        let _ = prev_s;
7691        fired
7692    }
7693
7694    pub fn reset(&mut self) { self.t = 0.0; for f in &mut self.fired { *f = false; } }
7695
7696    pub fn position_on(&self, spline: &CatmullRomSpline) -> Vec3 { spline.evaluate(self.t) }
7697}
7698
7699// ============================================================
7700// SPLINE UTILITY: CATMULL-ROM SECOND DERIVATIVE
7701// ============================================================
7702
7703/// Compute the second derivative of a Catmull-Rom segment via finite differences of tangents.
7704pub fn catmull_second_derivative(spline: &CatmullRomSpline, t: f32, dt: f32) -> Vec3 {
7705    let t0 = (t - dt).max(0.0);
7706    let t1 = (t + dt).min(1.0);
7707    let tang0 = spline.evaluate_derivative(t0);
7708    let tang1 = spline.evaluate_derivative(t1);
7709    (tang1 - tang0) / (t1 - t0).max(1e-10)
7710}
7711
7712// ============================================================
7713// SPLINE JERK (THIRD DERIVATIVE)
7714// ============================================================
7715
7716/// Approximate jerk (3rd derivative) via finite differences of second derivative.
7717pub fn catmull_jerk(spline: &CatmullRomSpline, t: f32, dt: f32) -> Vec3 {
7718    let t0 = (t - dt).max(0.0);
7719    let t1 = (t + dt).min(1.0);
7720    let d2_0 = catmull_second_derivative(spline, t0, dt);
7721    let d2_1 = catmull_second_derivative(spline, t1, dt);
7722    (d2_1 - d2_0) / (t1 - t0).max(1e-10)
7723}
7724
7725// ============================================================
7726// SPLINE SIGNED CURVATURE (2D XZ)
7727// ============================================================
7728
7729/// Signed curvature in the XZ plane: positive = left turn.
7730pub fn signed_curvature_xz(spline: &CatmullRomSpline, t: f32) -> f32 {
7731    let d1 = spline.evaluate_derivative(t);
7732    let d2 = catmull_second_derivative(spline, t, 1e-4);
7733    let cross = d1.x * d2.z - d1.z * d2.x;
7734    let denom = (d1.x * d1.x + d1.z * d1.z).powf(1.5);
7735    if denom < 1e-10 { 0.0 } else { cross / denom }
7736}
7737
7738// ============================================================
7739// SPLINE HEADING ANGLE
7740// ============================================================
7741
7742/// Return the heading angle (yaw) in radians of the spline tangent at t.
7743pub fn spline_heading_yaw(spline: &CatmullRomSpline, t: f32) -> f32 {
7744    let tang = spline.evaluate_derivative(t);
7745    tang.z.atan2(tang.x)
7746}
7747
7748// ============================================================
7749// FINAL EXTRA TESTS
7750// ============================================================
7751
7752#[cfg(test)]
7753mod tests_waypoints {
7754    use super::*;
7755
7756    pub(super) fn simple_line(n: usize) -> CatmullRomSpline {
7757        CatmullRomSpline {
7758            control_points: (0..n).map(|i| ControlPoint {
7759                position: Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
7760            }).collect(),
7761            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
7762        }
7763    }
7764
7765    #[test]
7766    fn test_waypoint_tracker_advances() {
7767        let s = simple_line(5);
7768        let mut tracker = SplineWaypointTracker::new(&s, 1.0, false);
7769        tracker.advance(0.5);
7770        assert!(tracker.t >= 0.0 && tracker.t <= 1.0);
7771    }
7772
7773    #[test]
7774    fn test_signed_curvature_straight_line_zero() {
7775        let s = simple_line(4);
7776        let kappa = signed_curvature_xz(&s, 0.5);
7777        // A straight line has zero curvature
7778        assert!(kappa.abs() < 0.1);
7779    }
7780
7781    #[test]
7782    fn test_heading_yaw_positive_x() {
7783        let s = simple_line(3);
7784        let yaw = spline_heading_yaw(&s, 0.5);
7785        // Tangent is along +X, so atan2(0, 1) ~ 0
7786        assert!(yaw.abs() < 0.2);
7787    }
7788
7789    #[test]
7790    fn test_subdivide_idempotent_positions() {
7791        let mut s = simple_line(3);
7792        subdivide_catmull(&mut s, 2);
7793        // All y and z should remain 0
7794        for cp in &s.control_points {
7795            assert!(cp.position.y.abs() < 1e-5);
7796            assert!(cp.position.z.abs() < 1e-5);
7797        }
7798    }
7799}
7800
7801// ============================================================
7802// SPLINE CURVATURE COMB VISUALISATION
7803// ============================================================
7804
7805/// Compute a curvature comb: for each sample point, a line from the point in
7806/// the normal direction scaled by curvature * scale.
7807pub struct CurvatureComb {
7808    pub base_points:  Vec<Vec3>,
7809    pub comb_tips:    Vec<Vec3>,
7810    pub curvatures:   Vec<f32>,
7811}
7812
7813impl CurvatureComb {
7814    pub fn compute(spline: &CatmullRomSpline, samples: usize, scale: f32) -> Self {
7815        let mut base_points = Vec::with_capacity(samples + 1);
7816        let mut comb_tips   = Vec::with_capacity(samples + 1);
7817        let mut curvatures  = Vec::with_capacity(samples + 1);
7818        for i in 0..=samples {
7819            let t     = i as f32 / samples as f32;
7820            let frame = spline.frenet_frame_at(t);
7821            let tip   = frame.position + frame.normal * (frame.curvature * scale);
7822            base_points.push(frame.position);
7823            comb_tips.push(tip);
7824            curvatures.push(frame.curvature);
7825        }
7826        CurvatureComb { base_points, comb_tips, curvatures }
7827    }
7828
7829    /// Maximum curvature comb height.
7830    pub fn max_height(&self) -> f32 {
7831        self.curvatures.iter().cloned().fold(0.0f32, f32::max)
7832    }
7833}
7834
7835// ============================================================
7836// SPLINE ARC-LENGTH INTEGRAL HELPERS
7837// ============================================================
7838
7839/// Integrate a scalar field f(t) weighted by arc-length ds/dt.
7840pub fn integrate_along_spline(
7841    spline:   &CatmullRomSpline,
7842    field:    &dyn Fn(Vec3) -> f32,
7843    steps:    usize,
7844) -> f32 {
7845    let mut acc = 0.0f32;
7846    let dt      = 1.0 / steps as f32;
7847    for i in 0..steps {
7848        let t0 = i as f32 * dt;
7849        let t1 = t0 + dt;
7850        let p0 = spline.evaluate(t0);
7851        let p1 = spline.evaluate(t1);
7852        let ds = (p1 - p0).length();
7853        let f0 = field(p0);
7854        let f1 = field(p1);
7855        acc += (f0 + f1) * 0.5 * ds;
7856    }
7857    acc
7858}
7859
7860/// Average of a scalar field over arc-length.
7861pub fn average_along_spline(
7862    spline: &CatmullRomSpline,
7863    field:  &dyn Fn(Vec3) -> f32,
7864    steps:  usize,
7865) -> f32 {
7866    let integral   = integrate_along_spline(spline, field, steps);
7867    let arc_length = integrate_along_spline(spline, &|_| 1.0, steps);
7868    if arc_length < 1e-10 { 0.0 } else { integral / arc_length }
7869}
7870
7871// ============================================================
7872// SPLINE WINDING NUMBER (2-D XZ)
7873// ============================================================
7874
7875/// Compute approximate winding number of a closed spline around a query point in XZ.
7876pub fn winding_number_xz(spline: &CatmullRomSpline, query: Vec2, samples: usize) -> f32 {
7877    if !spline.closed || samples < 2 { return 0.0; }
7878    let mut winding = 0.0f32;
7879    for i in 0..samples {
7880        let t0 = i as f32 / samples as f32;
7881        let t1 = (i + 1) as f32 / samples as f32;
7882        let p0 = spline.evaluate(t0);
7883        let p1 = spline.evaluate(t1);
7884        let a  = Vec2::new(p0.x - query.x, p0.z - query.y);
7885        let b  = Vec2::new(p1.x - query.x, p1.z - query.y);
7886        // Angle increment using atan2 of cross/dot
7887        let cross = a.x * b.y - a.y * b.x;
7888        let dot   = a.x * b.x + a.y * b.y;
7889        winding += cross.atan2(dot);
7890    }
7891    winding / (2.0 * std::f32::consts::PI)
7892}
7893
7894#[cfg(test)]
7895mod tests_comb {
7896    use super::*;
7897    pub(super) fn simple_line(n: usize) -> CatmullRomSpline {
7898        CatmullRomSpline {
7899            control_points: (0..n).map(|i| ControlPoint {
7900                position: Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
7901            }).collect(),
7902            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
7903        }
7904    }
7905
7906    #[test]
7907    fn test_curvature_comb_sample_count() {
7908        let s    = simple_line(4);
7909        let comb = CurvatureComb::compute(&s, 32, 1.0);
7910        assert_eq!(comb.base_points.len(), 33);
7911    }
7912
7913    #[test]
7914    fn test_integrate_along_constant_one() {
7915        let s   = simple_line(3);
7916        let val = integrate_along_spline(&s, &|_| 1.0, 128);
7917        // Arc length of a line 0..2 is approximately 2
7918        assert!((val - 2.0).abs() < 0.1);
7919    }
7920}
7921
7922// ============================================================
7923// SPLINE UTILITY: PERPENDICULAR DISTANCE FROM LINE
7924// ============================================================
7925
7926/// Signed perpendicular distance from `point` to the line through `a` and `b` in XZ.
7927pub fn xz_line_perpendicular_distance(a: Vec3, b: Vec3, point: Vec3) -> f32 {
7928    let ab = b - a;
7929    let ap = point - a;
7930    let ab_len = ab.length();
7931    if ab_len < 1e-10 { return (point - a).length(); }
7932    let ab_hat = ab / ab_len;
7933    let perp = ap - ab_hat * ap.dot(ab_hat);
7934    // signed via cross in XZ
7935    let sign = (ab_hat.x * perp.z - ab_hat.z * perp.x).signum();
7936    perp.length() * sign
7937}
7938
7939/// Compute the deviation of each control point from the chord (first to last).
7940pub fn chord_deviation(spline: &CatmullRomSpline) -> Vec<f32> {
7941    let n = spline.control_points.len();
7942    if n < 2 { return vec![0.0; n]; }
7943    let a = spline.control_points[0].position;
7944    let b = spline.control_points[n - 1].position;
7945    spline.control_points.iter().map(|cp| xz_line_perpendicular_distance(a, b, cp.position)).collect()
7946}
7947
7948/// Maximum absolute chord deviation.
7949pub fn max_chord_deviation(spline: &CatmullRomSpline) -> f32 {
7950    chord_deviation(spline).into_iter().map(|d| d.abs()).fold(0.0f32, f32::max)
7951}
7952
7953#[cfg(test)]
7954mod tests_spline_geometry {
7955    use super::*;
7956    pub(super) fn simple_line(n: usize) -> CatmullRomSpline {
7957        CatmullRomSpline {
7958            control_points: (0..n).map(|i| ControlPoint {
7959                position: Vec3::new(i as f32, 0.0, 0.0), weight: 1.0, tension: 0.0, ..ControlPoint::new(Vec3::ZERO)
7960            }).collect(),
7961            closed: false, alpha: 0.5, arc_length_table: Vec::new(), total_length: 0.0,
7962        }
7963    }
7964
7965    #[test]
7966    fn test_xz_perpendicular_distance_on_line() {
7967        let a = Vec3::new(0.0, 0.0, 0.0);
7968        let b = Vec3::new(4.0, 0.0, 0.0);
7969        let p = Vec3::new(2.0, 0.0, 3.0);
7970        let d = xz_line_perpendicular_distance(a, b, p);
7971        assert!((d.abs() - 3.0).abs() < 0.01);
7972    }
7973
7974    #[test]
7975    fn test_chord_deviation_straight_line_zero() {
7976        let s = simple_line(5);
7977        let max = max_chord_deviation(&s);
7978        assert!(max < 1e-4);
7979    }
7980}
7981
7982// ============================================================
7983// SPLINE UTILITIES: CENTRIPETAL PARAMETER ESTIMATE
7984// ============================================================
7985
7986/// Estimate a good alpha for centripetal Catmull-Rom given control points.
7987/// Returns the mean chord-length ratio exponent that minimises parameterisation error.
7988pub fn estimate_alpha(pts: &[Vec3]) -> f32 {
7989    if pts.len() < 3 { return 0.5; }
7990    let mut sum_ratio = 0.0f32;
7991    let n = pts.len() - 2;
7992    for i in 0..n {
7993        let d0 = (pts[i+1] - pts[i]).length().max(1e-10);
7994        let d1 = (pts[i+2] - pts[i+1]).length().max(1e-10);
7995        sum_ratio += (d0 / d1).ln().abs();
7996    }
7997    let mean_ratio = sum_ratio / n as f32;
7998    // alpha = 0 (uniform), 0.5 (centripetal), 1.0 (chordal)
7999    (0.5 * (1.0 + mean_ratio * 0.5)).clamp(0.0, 1.0)
8000}
8001
8002#[cfg(test)]
8003mod tests_alpha_estimate {
8004    use super::*;
8005
8006    #[test]
8007    fn test_estimate_alpha_uniform_spacing_half() {
8008        let pts: Vec<Vec3> = (0..5).map(|i| Vec3::new(i as f32, 0.0, 0.0)).collect();
8009        let a = estimate_alpha(&pts);
8010        // Uniformly spaced → ratio = 1 → ln(1)=0 → alpha = 0.5
8011        assert!((a - 0.5).abs() < 0.01);
8012    }
8013}