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