1use crate::Vec3;
8
9#[derive(Debug, Clone)]
11pub struct SplineControlPoint {
12 pub position: Vec3,
13 pub twist: f32,
15 pub gap_after: bool,
18}
19
20#[derive(Debug, Clone)]
22pub struct SplineSample {
23 pub position: Vec3,
24 pub forward: Vec3,
25 pub right: Vec3,
26 pub up: Vec3,
27 pub twist: f32,
29 pub t: f32,
31}
32
33pub fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
35 let t2 = t * t;
36 let t3 = t2 * t;
37 Vec3::new(
38 0.5 * ((2.0 * p1.x)
39 + (-p0.x + p2.x) * t
40 + (2.0 * p0.x - 5.0 * p1.x + 4.0 * p2.x - p3.x) * t2
41 + (-p0.x + 3.0 * p1.x - 3.0 * p2.x + p3.x) * t3),
42 0.5 * ((2.0 * p1.y)
43 + (-p0.y + p2.y) * t
44 + (2.0 * p0.y - 5.0 * p1.y + 4.0 * p2.y - p3.y) * t2
45 + (-p0.y + 3.0 * p1.y - 3.0 * p2.y + p3.y) * t3),
46 0.5 * ((2.0 * p1.z)
47 + (-p0.z + p2.z) * t
48 + (2.0 * p0.z - 5.0 * p1.z + 4.0 * p2.z - p3.z) * t2
49 + (-p0.z + 3.0 * p1.z - 3.0 * p2.z + p3.z) * t3),
50 )
51}
52
53pub fn catmull_rom_scalar(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
55 let t2 = t * t;
56 let t3 = t2 * t;
57 0.5 * ((2.0 * p1)
58 + (-p0 + p2) * t
59 + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
60 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
61}
62
63pub fn rotate_around_axis(v: Vec3, axis: Vec3, angle: f32) -> Vec3 {
65 let cos_a = angle.cos();
66 let sin_a = angle.sin();
67 let dot = v.dot(&axis);
68 let cross = axis.cross(&v);
69 Vec3::new(
70 v.x * cos_a + cross.x * sin_a + axis.x * dot * (1.0 - cos_a),
71 v.y * cos_a + cross.y * sin_a + axis.y * dot * (1.0 - cos_a),
72 v.z * cos_a + cross.z * sin_a + axis.z * dot * (1.0 - cos_a),
73 )
74}
75
76pub fn sample_closed_spline(points: &[SplineControlPoint], spacing: f32) -> Vec<SplineSample> {
78 let n = points.len();
79 if n < 3 {
80 return Vec::new();
81 }
82
83 let world_up = Vec3::UP;
84
85 let segments_per_cp = 20;
87 let mut total_length = 0.0_f32;
88 let mut prev_pos = points[0].position;
89 for seg in 0..n {
90 let p0 = points[(seg + n - 1) % n].position;
91 let p1 = points[seg].position;
92 let p2 = points[(seg + 1) % n].position;
93 let p3 = points[(seg + 2) % n].position;
94 for j in 1..=segments_per_cp {
95 let t = j as f32 / segments_per_cp as f32;
96 let pos = catmull_rom(p0, p1, p2, p3, t);
97 total_length += (pos - prev_pos).length();
98 prev_pos = pos;
99 }
100 }
101
102 let num_samples = (total_length / spacing).ceil() as usize;
103 let num_samples = num_samples.max(20);
104 let mut samples = Vec::with_capacity(num_samples);
105
106 for i in 0..num_samples {
107 let global_t = i as f32 / num_samples as f32;
108 let scaled = global_t * n as f32;
109 let seg = scaled.floor() as usize % n;
110 let local_t = scaled - scaled.floor();
111
112 let p0 = points[(seg + n - 1) % n].position;
113 let p1 = points[seg].position;
114 let p2 = points[(seg + 1) % n].position;
115 let p3 = points[(seg + 2) % n].position;
116
117 let position = catmull_rom(p0, p1, p2, p3, local_t);
118
119 let dt = 0.001;
121 let next_t = local_t + dt;
122 let pos_next = if next_t <= 1.0 {
123 catmull_rom(p0, p1, p2, p3, next_t)
124 } else {
125 let next_seg = (seg + 1) % n;
126 let np0 = points[(next_seg + n - 1) % n].position;
127 let np1 = points[next_seg].position;
128 let np2 = points[(next_seg + 1) % n].position;
129 let np3 = points[(next_seg + 2) % n].position;
130 catmull_rom(np0, np1, np2, np3, next_t - 1.0)
131 };
132
133 let forward = (pos_next - position).normalized();
134
135 let tw0 = points[(seg + n - 1) % n].twist;
137 let tw1 = points[seg].twist;
138 let tw2 = points[(seg + 1) % n].twist;
139 let tw3 = points[(seg + 2) % n].twist;
140 let twist = catmull_rom_scalar(tw0, tw1, tw2, tw3, local_t);
141 let twist_rad = twist.to_radians();
142
143 let right_flat = forward.cross(&world_up).normalized();
145 let up_flat = right_flat.cross(&forward).normalized();
146 let right = rotate_around_axis(right_flat, forward, twist_rad);
147 let up = rotate_around_axis(up_flat, forward, twist_rad);
148
149 samples.push(SplineSample {
150 position,
151 forward,
152 right,
153 up,
154 twist,
155 t: global_t,
156 });
157 }
158
159 samples
160}
161
162pub fn sample_open_spline(points: &[SplineControlPoint], spacing: f32) -> Vec<SplineSample> {
167 let n = points.len();
168 if n < 2 {
169 return Vec::new();
170 }
171
172 let phantom_start = SplineControlPoint {
174 position: points[0].position * 2.0 - points[1].position,
175 twist: points[0].twist,
176 gap_after: false,
177 };
178 let phantom_end = SplineControlPoint {
179 position: points[n - 1].position * 2.0 - points[n - 2].position,
180 twist: points[n - 1].twist,
181 gap_after: false,
182 };
183
184 let mut extended = Vec::with_capacity(n + 2);
185 extended.push(phantom_start);
186 extended.extend_from_slice(points);
187 extended.push(phantom_end);
188
189 let num_segs = n - 1;
192 let world_up = Vec3::UP;
193
194 let segments_per_cp = 20;
196 let mut total_length = 0.0_f32;
197 let mut prev_pos = extended[1].position; for seg in 0..num_segs {
199 let p0 = extended[seg].position;
200 let p1 = extended[seg + 1].position;
201 let p2 = extended[seg + 2].position;
202 let p3 = extended[seg + 3].position;
203 for j in 1..=segments_per_cp {
204 let t = j as f32 / segments_per_cp as f32;
205 let pos = catmull_rom(p0, p1, p2, p3, t);
206 total_length += (pos - prev_pos).length();
207 prev_pos = pos;
208 }
209 }
210
211 let num_samples = (total_length / spacing).ceil() as usize;
212 let num_samples = num_samples.max(10);
213 let total_pts = num_samples + 1;
215 let mut samples = Vec::with_capacity(total_pts);
216
217 for i in 0..total_pts {
218 let global_t = i as f32 / num_samples as f32; let scaled = global_t * num_segs as f32;
220 let seg = (scaled.floor() as usize).min(num_segs - 1);
221 let local_t = scaled - seg as f32;
222
223 let p0 = extended[seg].position;
224 let p1 = extended[seg + 1].position;
225 let p2 = extended[seg + 2].position;
226 let p3 = extended[seg + 3].position;
227
228 let position = catmull_rom(p0, p1, p2, p3, local_t);
229
230 let dt = 0.001;
232 let next_t = local_t + dt;
233 let pos_next = if next_t <= 1.0 {
234 catmull_rom(p0, p1, p2, p3, next_t)
235 } else if seg + 1 < num_segs {
236 let np0 = extended[seg + 1].position;
237 let np1 = extended[seg + 2].position;
238 let np2 = extended[seg + 3].position;
239 let np3 = extended[seg + 4].position;
240 catmull_rom(np0, np1, np2, np3, next_t - 1.0)
241 } else {
242 catmull_rom(p0, p1, p2, p3, 1.0)
243 };
244
245 let forward = (pos_next - position).normalized();
246
247 let tw0 = extended[seg].twist;
249 let tw1 = extended[seg + 1].twist;
250 let tw2 = extended[seg + 2].twist;
251 let tw3 = extended[seg + 3].twist;
252 let twist = catmull_rom_scalar(tw0, tw1, tw2, tw3, local_t);
253 let twist_rad = twist.to_radians();
254
255 let right_flat = forward.cross(&world_up).normalized();
256 let up_flat = right_flat.cross(&forward).normalized();
257 let right = rotate_around_axis(right_flat, forward, twist_rad);
258 let up = rotate_around_axis(up_flat, forward, twist_rad);
259
260 samples.push(SplineSample {
261 position,
262 forward,
263 right,
264 up,
265 twist,
266 t: global_t,
267 });
268 }
269
270 samples
271}
272
273pub fn compute_gap_ranges(points: &[SplineControlPoint], closed: bool) -> Vec<(f32, f32)> {
282 let n = points.len();
283 if n < 2 {
284 return Vec::new();
285 }
286
287 let num_segs = if closed { n } else { n - 1 };
288 let mut ranges: Vec<(f32, f32)> = Vec::new();
289
290 let mut i = 0;
291 while i < n {
292 if points[i].gap_after {
293 let start_t = i as f32 / num_segs as f32;
294 let mut end_idx = i + 1;
296 while end_idx < n && points[end_idx].gap_after {
297 end_idx += 1;
298 }
299 let end_t = (end_idx as f32 / num_segs as f32).min(1.0);
302 ranges.push((start_t, end_t));
303 i = end_idx;
304 } else {
305 i += 1;
306 }
307 }
308
309 if closed && ranges.len() >= 2 {
312 let last = ranges.len() - 1;
313 if (ranges[last].1 - 1.0).abs() < 1e-6 && ranges[0].0.abs() < 1e-6 {
314 let merged_start = ranges[last].0;
315 let merged_end = ranges[0].1;
316 ranges[0] = (merged_start, merged_end); ranges.pop();
318 }
319 }
320
321 ranges
322}
323
324pub fn is_in_gap(t: f32, gaps: &[(f32, f32)]) -> bool {
328 gap_at(t, gaps).is_some()
329}
330
331pub fn gap_at(t: f32, gaps: &[(f32, f32)]) -> Option<(f32, f32)> {
335 let t_wrapped = ((t % 1.0) + 1.0) % 1.0;
336 for &(start, end) in gaps {
337 if start <= end {
338 if t_wrapped >= start && t_wrapped < end {
340 return Some((start, end));
341 }
342 } else {
343 if t_wrapped >= start || t_wrapped < end {
345 return Some((start, end));
346 }
347 }
348 }
349 None
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn make_square_loop() -> Vec<SplineControlPoint> {
357 vec![
358 SplineControlPoint {
359 position: Vec3::new(0.0, 0.0, 0.0),
360 twist: 0.0,
361 gap_after: false,
362 },
363 SplineControlPoint {
364 position: Vec3::new(10.0, 0.0, 0.0),
365 twist: 0.0,
366 gap_after: false,
367 },
368 SplineControlPoint {
369 position: Vec3::new(10.0, 0.0, 10.0),
370 twist: 0.0,
371 gap_after: false,
372 },
373 SplineControlPoint {
374 position: Vec3::new(0.0, 0.0, 10.0),
375 twist: 0.0,
376 gap_after: false,
377 },
378 ]
379 }
380
381 #[test]
382 fn closed_spline_produces_samples() {
383 let pts = make_square_loop();
384 let samples = sample_closed_spline(&pts, 1.0);
385 assert!(samples.len() > 10);
386 assert!(samples.first().unwrap().t < 0.01);
388 assert!(samples.last().unwrap().t < 1.0);
389 }
390
391 #[test]
392 fn open_spline_produces_samples() {
393 let pts = vec![
394 SplineControlPoint {
395 position: Vec3::new(0.0, 0.0, 0.0),
396 twist: 0.0,
397 gap_after: false,
398 },
399 SplineControlPoint {
400 position: Vec3::new(5.0, 0.0, 0.0),
401 twist: 0.0,
402 gap_after: false,
403 },
404 SplineControlPoint {
405 position: Vec3::new(10.0, 0.0, 0.0),
406 twist: 0.0,
407 gap_after: false,
408 },
409 ];
410 let samples = sample_open_spline(&pts, 1.0);
411 assert!(samples.len() >= 10);
412 let first = samples.first().unwrap();
414 let last = samples.last().unwrap();
415 assert!((first.position - pts[0].position).length() < 0.1);
416 assert!((last.position - pts[2].position).length() < 0.5);
417 }
418
419 #[test]
420 fn too_few_points_returns_empty() {
421 let pts = vec![
422 SplineControlPoint {
423 position: Vec3::new(0.0, 0.0, 0.0),
424 twist: 0.0,
425 gap_after: false,
426 },
427 SplineControlPoint {
428 position: Vec3::new(1.0, 0.0, 0.0),
429 twist: 0.0,
430 gap_after: false,
431 },
432 ];
433 assert!(sample_closed_spline(&pts, 1.0).is_empty());
434 }
435
436 #[test]
437 fn gap_ranges_single_gap_closed() {
438 let pts = vec![
439 SplineControlPoint {
440 position: Vec3::new(0.0, 0.0, 0.0),
441 twist: 0.0,
442 gap_after: false,
443 },
444 SplineControlPoint {
445 position: Vec3::new(10.0, 0.0, 0.0),
446 twist: 0.0,
447 gap_after: true,
448 },
449 SplineControlPoint {
450 position: Vec3::new(10.0, 0.0, 10.0),
451 twist: 0.0,
452 gap_after: false,
453 },
454 SplineControlPoint {
455 position: Vec3::new(0.0, 0.0, 10.0),
456 twist: 0.0,
457 gap_after: false,
458 },
459 ];
460 let ranges = compute_gap_ranges(&pts, true);
461 assert_eq!(ranges.len(), 1);
462 assert!((ranges[0].0 - 0.25).abs() < 0.01); assert!((ranges[0].1 - 0.50).abs() < 0.01); }
465
466 #[test]
467 fn gap_ranges_consecutive_merge() {
468 let pts = vec![
469 SplineControlPoint {
470 position: Vec3::new(0.0, 0.0, 0.0),
471 twist: 0.0,
472 gap_after: false,
473 },
474 SplineControlPoint {
475 position: Vec3::new(10.0, 0.0, 0.0),
476 twist: 0.0,
477 gap_after: true,
478 },
479 SplineControlPoint {
480 position: Vec3::new(10.0, 0.0, 10.0),
481 twist: 0.0,
482 gap_after: true,
483 },
484 SplineControlPoint {
485 position: Vec3::new(0.0, 0.0, 10.0),
486 twist: 0.0,
487 gap_after: false,
488 },
489 ];
490 let ranges = compute_gap_ranges(&pts, true);
491 assert_eq!(ranges.len(), 1);
492 assert!((ranges[0].0 - 0.25).abs() < 0.01); assert!((ranges[0].1 - 0.75).abs() < 0.01); }
495
496 #[test]
497 fn gap_ranges_open_spline() {
498 let pts = vec![
499 SplineControlPoint {
500 position: Vec3::new(0.0, 0.0, 0.0),
501 twist: 0.0,
502 gap_after: true,
503 },
504 SplineControlPoint {
505 position: Vec3::new(5.0, 0.0, 0.0),
506 twist: 0.0,
507 gap_after: false,
508 },
509 SplineControlPoint {
510 position: Vec3::new(10.0, 0.0, 0.0),
511 twist: 0.0,
512 gap_after: false,
513 },
514 ];
515 let ranges = compute_gap_ranges(&pts, false);
516 assert_eq!(ranges.len(), 1);
517 assert!(ranges[0].0.abs() < 0.01); assert!((ranges[0].1 - 0.50).abs() < 0.01); }
520
521 #[test]
522 fn is_in_gap_basic() {
523 let gaps = vec![(0.25, 0.50)];
524 assert!(!is_in_gap(0.1, &gaps));
525 assert!(is_in_gap(0.3, &gaps));
526 assert!(!is_in_gap(0.5, &gaps)); assert!(!is_in_gap(0.7, &gaps));
528 }
529
530 #[test]
531 fn gap_at_returns_range() {
532 let gaps = vec![(0.25, 0.50)];
533 assert!(gap_at(0.1, &gaps).is_none());
534 let g = gap_at(0.3, &gaps).unwrap();
535 assert!((g.0 - 0.25).abs() < 0.01);
536 assert!((g.1 - 0.50).abs() < 0.01);
537 }
538
539 #[test]
540 fn gap_wraparound_closed() {
541 let pts = vec![
543 SplineControlPoint {
544 position: Vec3::new(0.0, 0.0, 0.0),
545 twist: 0.0,
546 gap_after: false,
547 },
548 SplineControlPoint {
549 position: Vec3::new(10.0, 0.0, 0.0),
550 twist: 0.0,
551 gap_after: false,
552 },
553 SplineControlPoint {
554 position: Vec3::new(10.0, 0.0, 10.0),
555 twist: 0.0,
556 gap_after: false,
557 },
558 SplineControlPoint {
559 position: Vec3::new(0.0, 0.0, 10.0),
560 twist: 0.0,
561 gap_after: true,
562 },
563 ];
564 let ranges = compute_gap_ranges(&pts, true);
565 assert_eq!(ranges.len(), 1);
566 assert!((ranges[0].0 - 0.75).abs() < 0.01);
568 assert!((ranges[0].1 - 1.0).abs() < 0.01);
569 assert!(is_in_gap(0.8, &ranges));
570 assert!(!is_in_gap(0.1, &ranges));
571 }
572}