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