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
6const 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; const PARALLEL_TRANSPORT_STEPS: usize = 256;
19
20fn 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 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
97fn 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
134fn 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
153fn 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#[derive(Clone, Debug)]
172pub struct FrenetFrame {
173 pub position: Vec3,
174 pub tangent: Vec3, pub normal: Vec3, pub binormal: Vec3, pub curvature: f32, pub torsion: f32, }
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 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 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#[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 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#[derive(Clone, Debug)]
274pub struct ControlPoint {
275 pub position: Vec3,
276 pub tangent_in: Vec3,
277 pub tangent_out: Vec3,
278 pub weight: f32, pub knot_value: f32, pub id: u64,
281 pub tension: f32, }
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#[derive(Clone, Debug, PartialEq)]
317pub enum SplineType {
318 CatmullRom,
319 CubicBezier,
320 BSpline { degree: usize },
321 Nurbs { degree: usize },
322 Hermite,
323}
324
325#[derive(Clone, Debug)]
330pub struct CatmullRomSpline {
331 pub control_points: Vec<ControlPoint>,
332 pub closed: bool,
333 pub alpha: f32, 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 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 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 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 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 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 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 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#[derive(Clone, Debug)]
581pub struct CubicBezierSpline {
582 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 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 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 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 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) }
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 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 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#[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 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 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 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 pub fn insert_knot(&mut self, t_new: f32) {
908 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 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#[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 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; let mut pts = Vec::new();
1083 let mut wts = Vec::new();
1084 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#[derive(Clone, Debug)]
1110pub struct HermiteSpline {
1111 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 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
1245fn 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); 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
1270pub 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 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
1315pub 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 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 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 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#[derive(Clone, Debug)]
1388pub struct RailTrack {
1389 pub id: u64,
1390 pub spline: CatmullRomSpline,
1391 pub gauge: f32, pub max_speed: f32, pub super_elevation_max: f32, pub cant_deficiency: f32, 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 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 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; cant.min(self.super_elevation_max * 1000.0)
1428 }
1429
1430 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#[derive(Clone, Debug)]
1485pub struct SpeedProfile {
1486 pub keyframes: Vec<(f32, f32)>, }
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 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 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, pub roll_correction: bool,
1540 pub fov_profile: SpeedProfile, 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 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#[derive(Clone, Debug)]
1591pub struct CrossSection {
1592 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 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 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 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 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 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#[derive(Clone, Debug)]
1832pub struct SplineNode {
1833 pub id: u64,
1834 pub position: Vec3,
1835 pub connected_splines: Vec<u64>, }
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, 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: 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 pub fn dijkstra(&self, start: u64, goal: u64) -> Option<Vec<u64>> {
1900 use std::collections::BinaryHeap;
1901 use std::cmp::Reverse;
1902
1903 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 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 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#[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>, 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 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 let spline_clone = spline.clone();
2086 agent.update(dt, &spline_clone);
2087 }
2088 }
2089 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#[derive(Clone, Debug)]
2120pub struct SplineConstrainedObject {
2121 pub id: u64,
2122 pub spline_id: u64,
2123 pub t: f32,
2124 pub speed: f32, pub mass: f32,
2126 pub gravity: Vec3,
2127 pub friction: f32, pub normal_force: f32, }
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 let g_tangent = self.gravity.dot(frame.tangent);
2149 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 let friction_force = -self.speed.signum() * self.friction * self.normal_force;
2155 let net_tangential = self.mass * g_tangent + friction_force;
2157 let tangential_accel = net_tangential / self.mass;
2158 self.speed += tangential_accel * dt;
2159 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#[derive(Clone, Debug)]
2184pub struct ChainLink {
2185 pub t: f32,
2186 pub angle_twist: f32, 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, }
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#[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)>, }
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), });
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), });
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), });
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), (4,5),(5,6),(6,7),(7,4), (0,4),(1,5),(2,6),(3,7), ];
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#[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#[derive(Clone, Debug, PartialEq)]
2486pub enum SelectionTarget {
2487 SplineId(u64),
2488 ControlPointIndex(u64, usize), 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>, pub selected_cp: Vec<(u64, usize)>, 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#[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)>, 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 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#[derive(Debug)]
2622pub struct SplineEditor {
2623 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 pub rail_tracks: HashMap<u64, RailTrack>,
2634 pub camera_rails: HashMap<u64, CameraRail>,
2635
2636 pub path_network: PathNetwork,
2638 pub traffic_system: Option<TrafficSystem>,
2639
2640 pub constrained_objects: Vec<SplineConstrainedObject>,
2642 pub chains: Vec<SplineChain>,
2643
2644 pub selection: SelectionState,
2646 pub undo_history: UndoHistory,
2647 pub debug_viz: SplineDebugViz,
2648
2649 pub default_alpha: f32, 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 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 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 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 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 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 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 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 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 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 §ion,
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 §ion,
2959 min_steps,
2960 max_steps,
2961 total_length,
2962 );
2963 self.generated_meshes.insert(spline_id, mesh);
2964 true
2965 }
2966
2967 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 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 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 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 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 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 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 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 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 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 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 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 _ => { }
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 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 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 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
3286pub fn resample_polyline(pts: &[Vec3], n_out: usize) -> Vec<Vec3> {
3292 if pts.len() < 2 || n_out < 2 { return pts.to_vec(); }
3293 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
3319pub 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; 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
3341pub 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
3356pub fn douglas_peucker(pts: &[Vec3], epsilon: f32) -> Vec<Vec3> {
3358 if pts.len() < 3 { return pts.to_vec(); }
3359 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(); left.extend(right);
3384 left
3385 } else {
3386 vec![pts[0], *pts.last().unwrap()]
3387 }
3388}
3389
3390pub 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 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
3412pub 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
3422pub 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
3436pub 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
3455pub 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#[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, pub show_weights: bool,
3494 pub edit_mode: SplineEditMode,
3495 pub snap_angle: f32, 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 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
3559pub 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
3577pub 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
3596pub 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 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 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 self.fit_cubic_with_tangents(pts, ¶ms, 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 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
3692pub 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
3708pub 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, §ion, ring_count, total_length)
3729}
3730
3731pub 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 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 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
3788pub 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 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 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 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
3814pub 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 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#[derive(Clone, Debug)]
3858pub struct SpeedCurve {
3859 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 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 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#[derive(Clone, Debug)]
3932pub struct SplineSignal {
3933 pub t: f32, 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 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#[derive(Clone, Debug)]
3997pub struct SplineLodLevel {
3998 pub max_camera_distance: f32,
3999 pub resolution: usize, 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
4031pub fn example_build_roller_coaster() -> SplineEditor {
4036 let mut editor = SplineEditor::new();
4037
4038 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), ];
4050 let spline_id = editor.create_catmull_spline(loop_pts, "RollerCoaster");
4051 editor.toggle_closed_spline(spline_id);
4052
4053 editor.create_rail_track(spline_id, DEFAULT_RAIL_GAUGE);
4055
4056 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 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
4083pub 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
4101pub 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
4118pub 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
4134pub 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 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#[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
4204pub fn generate_helix(
4209 center: Vec3,
4210 radius: f32,
4211 pitch: f32, 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, q: u32, 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
4246fn verify_arc_length_integration() -> bool {
4251 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 }
4263
4264pub 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
4287pub 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
4312impl 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
4357pub 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 let params = chord_length_params(pts);
4376 let mut cps = Vec::with_capacity(n);
4377 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 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 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
4414pub fn surface_of_revolution(
4419 profile_pts: &[Vec2], 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 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#[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
4507pub 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 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
4536pub 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#[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), ];
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 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 let d = s.eval_segment_derivative(0, 1.0);
4663 assert!(d.length() > EPSILON, "Derivative at boundary should be non-zero");
4665 }
4666}
4667
4668#[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#[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#[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#[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#[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#[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#[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
5071pub 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#[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#[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#[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#[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#[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#[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
5482pub 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
5510pub 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
5543impl 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#[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
5595pub fn simplify_polyline(pts: &[Vec3], epsilon: f32) -> Vec<Vec3> {
5600 douglas_peucker(pts, epsilon)
5601}
5602
5603impl SplineEditor {
5608 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 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 pub fn select_all(&mut self) {
5642 for &id in self.catmull_splines.keys() {
5643 self.selection.selected.insert(id);
5644 }
5645 }
5646
5647 pub fn deselect_all(&mut self) {
5649 self.selection.clear();
5650 }
5651
5652 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 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
5673pub 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
5691pub 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
5699pub 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
5705pub 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
5721pub 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
5780pub 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
5807pub 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
5827pub 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
5903pub 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
5974pub 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
6012pub 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#[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#[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
6139fn 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
6173pub 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
6184pub 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#[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
6220pub 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; 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#[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
6294pub 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
6359pub 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#[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
6505pub 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#[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#[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
6608pub 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
6646pub 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
6687impl 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(¤t) {
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(¤t) {
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#[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#[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)>, }
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
7047pub struct SplineBundle {
7052 pub splines: Vec<CatmullRomSpline>,
7053 pub offsets: Vec<f32>, pub separator: f32, }
7056
7057impl SplineBundle {
7058 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
7089pub 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 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 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
7133pub 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 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
7164pub 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 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
7207pub 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
7229pub 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
7274pub 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 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 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#[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 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(¢re, 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#[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
7428pub 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
7439pub 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
7455pub 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
7475pub 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
7498pub 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
7523pub fn reparametrise_by_curvature(
7529 spline: &CatmullRomSpline,
7530 n: usize,
7531 weight: f32,
7532) -> Vec<f32> {
7533 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 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 (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#[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); 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
7641pub 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)>, 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 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); let cur_s = {
7674 let t_cur = self.t;
7675 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
7707pub 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
7720pub 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
7733pub 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
7746pub 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#[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 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 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 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
7809pub 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 pub fn max_height(&self) -> f32 {
7839 self.curvatures.iter().cloned().fold(0.0f32, f32::max)
7840 }
7841}
7842
7843pub 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
7868pub 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
7879pub 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 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 assert!((val - 2.0).abs() < 0.1);
7927 }
7928}
7929
7930pub 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 let sign = (ab_hat.x * perp.z - ab_hat.z * perp.x).signum();
7944 perp.length() * sign
7945}
7946
7947pub 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
7956pub 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
7990pub 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 (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 assert!((a - 0.5).abs() < 0.01);
8020 }
8021}