1#[inline]
19fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
20 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
21}
22
23#[inline]
24fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27
28#[inline]
29fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
30 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
31}
32
33#[inline]
34fn scale(v: [f64; 3], s: f64) -> [f64; 3] {
35 [v[0] * s, v[1] * s, v[2] * s]
36}
37
38#[inline]
39fn len_sq(v: [f64; 3]) -> f64 {
40 dot(v, v)
41}
42
43#[inline]
44fn len(v: [f64; 3]) -> f64 {
45 len_sq(v).sqrt()
46}
47
48#[inline]
49fn normalize(v: [f64; 3]) -> [f64; 3] {
50 let l = len(v);
51 if l < 1e-300 {
52 [0.0, 0.0, 0.0]
53 } else {
54 scale(v, 1.0 / l)
55 }
56}
57
58#[inline]
59fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
60 [
61 a[1] * b[2] - a[2] * b[1],
62 a[2] * b[0] - a[0] * b[2],
63 a[0] * b[1] - a[1] * b[0],
64 ]
65}
66
67#[derive(Debug, Clone, PartialEq)]
73pub struct SweepResult {
74 pub toi: f64,
76 pub normal: [f64; 3],
78 pub point_a: [f64; 3],
80 pub point_b: [f64; 3],
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct RayHit {
87 pub t: f64,
89 pub point: [f64; 3],
91 pub normal: [f64; 3],
93}
94
95pub fn swept_sphere_sphere(
107 c0_a: [f64; 3],
108 c1_a: [f64; 3],
109 r_a: f64,
110 c0_b: [f64; 3],
111 c1_b: [f64; 3],
112 r_b: f64,
113) -> Option<SweepResult> {
114 let r_sum = r_a + r_b;
115
116 let dp = sub(c0_b, c0_a);
118 let dv = sub(sub(c1_b, c0_b), sub(c1_a, c0_a));
120
121 let a = len_sq(dv);
124 let b = dot(dp, dv);
125 let c = len_sq(dp) - r_sum * r_sum;
126
127 if c <= 0.0 {
129 let normal = if len_sq(dp) > 1e-20 {
130 normalize(scale(dp, -1.0))
131 } else {
132 [0.0, 1.0, 0.0]
133 };
134 let contact = add(c0_a, scale(normal, -r_a));
135 return Some(SweepResult {
136 toi: 0.0,
137 normal,
138 point_a: contact,
139 point_b: contact,
140 });
141 }
142
143 if a < 1e-300 {
144 return None;
146 }
147
148 let disc = b * b - a * c;
149 if disc < 0.0 {
150 return None; }
152
153 let sqrt_disc = disc.sqrt();
154 let t = (-b - sqrt_disc) / a;
155 let t = if (0.0..=1.0).contains(&t) {
156 t
157 } else {
158 let t2 = (-b + sqrt_disc) / a;
159 if (0.0..=1.0).contains(&t2) {
160 t2
161 } else {
162 return None;
163 }
164 };
165
166 let pos_a = add(c0_a, scale(sub(c1_a, c0_a), t));
168 let pos_b = add(c0_b, scale(sub(c1_b, c0_b), t));
169 let d = sub(pos_b, pos_a);
170 let dist = len(d);
171 let normal = if dist > 1e-10 {
172 scale(d, 1.0 / dist)
173 } else {
174 [0.0, 1.0, 0.0]
175 };
176
177 Some(SweepResult {
178 toi: t,
179 normal,
180 point_a: add(pos_a, scale(normal, r_a)),
181 point_b: add(pos_b, scale(normal, -r_b)),
182 })
183}
184
185pub fn swept_sphere_aabb(
197 c0: [f64; 3],
198 c1: [f64; 3],
199 r: f64,
200 aabb_min: [f64; 3],
201 aabb_max: [f64; 3],
202) -> Option<SweepResult> {
203 let exp_min = [aabb_min[0] - r, aabb_min[1] - r, aabb_min[2] - r];
206 let exp_max = [aabb_max[0] + r, aabb_max[1] + r, aabb_max[2] + r];
207
208 let dir = sub(c1, c0);
210 let result = ray_aabb_slab(c0, dir, exp_min, exp_max, 0.0, 1.0)?;
211
212 let hit_center = add(c0, scale(dir, result.t));
214
215 let closest_on_aabb = closest_point_on_aabb(hit_center, aabb_min, aabb_max);
217 let diff = sub(hit_center, closest_on_aabb);
218 let dist = len(diff);
219 let normal = if dist > 1e-10 {
220 normalize(diff)
221 } else {
222 result.normal
223 };
224
225 Some(SweepResult {
226 toi: result.t,
227 normal,
228 point_a: add(hit_center, scale(normal, -r)),
229 point_b: closest_on_aabb,
230 })
231}
232
233pub fn swept_aabb_aabb(
246 min_a: [f64; 3],
247 max_a: [f64; 3],
248 vel: [f64; 3],
249 min_b: [f64; 3],
250 max_b: [f64; 3],
251) -> Option<SweepResult> {
252 let half_a = [
254 (max_a[0] - min_a[0]) * 0.5,
255 (max_a[1] - min_a[1]) * 0.5,
256 (max_a[2] - min_a[2]) * 0.5,
257 ];
258 let center_a = [
260 (min_a[0] + max_a[0]) * 0.5,
261 (min_a[1] + max_a[1]) * 0.5,
262 (min_a[2] + max_a[2]) * 0.5,
263 ];
264
265 let exp_min = [
267 min_b[0] - half_a[0],
268 min_b[1] - half_a[1],
269 min_b[2] - half_a[2],
270 ];
271 let exp_max = [
272 max_b[0] + half_a[0],
273 max_b[1] + half_a[1],
274 max_b[2] + half_a[2],
275 ];
276
277 let result = ray_aabb_slab(center_a, vel, exp_min, exp_max, 0.0, 1.0)?;
279
280 let hit_center_a = add(center_a, scale(vel, result.t));
282
283 let contact_b = closest_point_on_aabb(hit_center_a, min_b, max_b);
285
286 Some(SweepResult {
287 toi: result.t,
288 normal: result.normal,
289 point_a: add(
290 hit_center_a,
291 scale(result.normal, -half_a[0].max(half_a[1]).max(half_a[2])),
292 ),
293 point_b: contact_b,
294 })
295}
296
297pub fn ray_sphere(
307 origin: [f64; 3],
308 dir: [f64; 3],
309 center: [f64; 3],
310 radius: f64,
311 t_min: f64,
312 t_max: f64,
313) -> Option<RayHit> {
314 let oc = sub(origin, center);
315 let a = len_sq(dir);
316 let b = dot(oc, dir);
317 let c = len_sq(oc) - radius * radius;
318
319 let disc = b * b - a * c;
320 if disc < 0.0 {
321 return None;
322 }
323
324 let sqrt_disc = disc.sqrt();
325 let t = {
326 let t1 = (-b - sqrt_disc) / a;
327 if t1 >= t_min && t1 <= t_max {
328 t1
329 } else {
330 let t2 = (-b + sqrt_disc) / a;
331 if t2 >= t_min && t2 <= t_max {
332 t2
333 } else {
334 return None;
335 }
336 }
337 };
338
339 let point = add(origin, scale(dir, t));
340 let normal = normalize(sub(point, center));
341
342 Some(RayHit { t, point, normal })
343}
344
345pub fn ray_aabb(
353 origin: [f64; 3],
354 dir: [f64; 3],
355 aabb_min: [f64; 3],
356 aabb_max: [f64; 3],
357 t_min: f64,
358 t_max: f64,
359) -> Option<RayHit> {
360 ray_aabb_slab(origin, dir, aabb_min, aabb_max, t_min, t_max)
361}
362
363fn ray_aabb_slab(
365 origin: [f64; 3],
366 dir: [f64; 3],
367 aabb_min: [f64; 3],
368 aabb_max: [f64; 3],
369 t_min: f64,
370 t_max: f64,
371) -> Option<RayHit> {
372 let mut t_near = t_min;
373 let mut t_far = t_max;
374 let mut hit_axis = 0usize;
375 let mut hit_sign = 1.0_f64;
376
377 for i in 0..3 {
378 if dir[i].abs() < 1e-300 {
379 if origin[i] < aabb_min[i] || origin[i] > aabb_max[i] {
381 return None; }
383 } else {
384 let inv = 1.0 / dir[i];
385 let mut t1 = (aabb_min[i] - origin[i]) * inv;
386 let mut t2 = (aabb_max[i] - origin[i]) * inv;
387 let sign = if t1 <= t2 { -1.0_f64 } else { 1.0_f64 };
388 if t1 > t2 {
389 std::mem::swap(&mut t1, &mut t2);
390 }
391 if t1 > t_near {
392 t_near = t1;
393 hit_axis = i;
394 hit_sign = sign;
395 }
396 if t2 < t_far {
397 t_far = t2;
398 }
399 if t_near > t_far {
400 return None;
401 }
402 }
403 }
404
405 if t_near > t_far || t_near > t_max || t_far < t_min {
406 return None;
407 }
408
409 let t = if t_near >= t_min { t_near } else { t_far };
410 if t < t_min || t > t_max {
411 return None;
412 }
413
414 let point = add(origin, scale(dir, t));
415 let mut normal = [0.0f64; 3];
416 normal[hit_axis] = hit_sign;
417
418 Some(RayHit { t, point, normal })
419}
420
421pub fn ray_triangle(
430 origin: [f64; 3],
431 dir: [f64; 3],
432 v0: [f64; 3],
433 v1: [f64; 3],
434 v2: [f64; 3],
435 t_min: f64,
436 t_max: f64,
437) -> Option<RayHit> {
438 let e1 = sub(v1, v0);
439 let e2 = sub(v2, v0);
440
441 let h = cross(dir, e2);
442 let det = dot(e1, h);
443
444 if det.abs() < 1e-10 {
446 return None;
447 }
448
449 let inv_det = 1.0 / det;
450 let s = sub(origin, v0);
451 let u = dot(s, h) * inv_det;
452 if !(0.0..=1.0).contains(&u) {
453 return None;
454 }
455
456 let q = cross(s, e1);
457 let v = dot(dir, q) * inv_det;
458 if v < 0.0 || u + v > 1.0 {
459 return None;
460 }
461
462 let t = dot(e2, q) * inv_det;
463 if t < t_min || t > t_max {
464 return None;
465 }
466
467 let point = add(origin, scale(dir, t));
468 let normal = normalize(cross(e1, e2));
469
470 Some(RayHit { t, point, normal })
471}
472
473fn closest_point_on_aabb(pt: [f64; 3], aabb_min: [f64; 3], aabb_max: [f64; 3]) -> [f64; 3] {
479 [
480 pt[0].clamp(aabb_min[0], aabb_max[0]),
481 pt[1].clamp(aabb_min[1], aabb_max[1]),
482 pt[2].clamp(aabb_min[2], aabb_max[2]),
483 ]
484}
485
486pub fn point_aabb_dist_sq(pt: [f64; 3], aabb_min: [f64; 3], aabb_max: [f64; 3]) -> f64 {
488 let mut dist_sq = 0.0_f64;
489 for i in 0..3 {
490 if pt[i] < aabb_min[i] {
491 let d = aabb_min[i] - pt[i];
492 dist_sq += d * d;
493 } else if pt[i] > aabb_max[i] {
494 let d = pt[i] - aabb_max[i];
495 dist_sq += d * d;
496 }
497 }
498 dist_sq
499}
500
501pub fn sphere_aabb_overlap(
503 center: [f64; 3],
504 radius: f64,
505 aabb_min: [f64; 3],
506 aabb_max: [f64; 3],
507) -> bool {
508 point_aabb_dist_sq(center, aabb_min, aabb_max) <= radius * radius
509}
510
511pub fn aabb_aabb_overlap(
513 min_a: [f64; 3],
514 max_a: [f64; 3],
515 min_b: [f64; 3],
516 max_b: [f64; 3],
517) -> bool {
518 min_a[0] <= max_b[0]
519 && max_a[0] >= min_b[0]
520 && min_a[1] <= max_b[1]
521 && max_a[1] >= min_b[1]
522 && min_a[2] <= max_b[2]
523 && max_a[2] >= min_b[2]
524}
525
526pub fn swept_capsule_plane(
536 c0: [f64; 3],
537 c1: [f64; 3],
538 radius: f64,
539 half_height: f64,
540 plane_normal: [f64; 3],
541 plane_d: f64,
542) -> Option<SweepResult> {
543 let n = normalize(plane_normal);
544 let effective_r = radius + half_height;
549 let dist0 = dot(n, c0) - plane_d - effective_r;
550
551 if dist0 <= 0.0 {
552 return Some(SweepResult {
553 toi: 0.0,
554 normal: n,
555 point_a: c0,
556 point_b: add(c0, scale(n, -dist0)),
557 });
558 }
559
560 let disp = sub(c1, c0);
561 let closing = -dot(n, disp);
562
563 if closing <= 1e-12 {
564 return None;
565 }
566
567 let t = dist0 / closing;
568 if t > 1.0 {
569 return None;
570 }
571
572 let hit_pos = add(c0, scale(disp, t));
573 Some(SweepResult {
574 toi: t,
575 normal: n,
576 point_a: add(hit_pos, scale(n, -effective_r)),
577 point_b: add(hit_pos, scale(n, -effective_r)),
578 })
579}
580
581pub fn refine_sphere_sphere_toi(
591 c0_a: [f64; 3],
592 vel_a: [f64; 3],
593 r_a: f64,
594 c0_b: [f64; 3],
595 vel_b: [f64; 3],
596 r_b: f64,
597 t_lo: f64,
598 t_hi: f64,
599 tolerance: f64,
600 max_iter: usize,
601) -> f64 {
602 let r_sum = r_a + r_b;
603 let mut lo = t_lo;
604 let mut hi = t_hi;
605
606 for _ in 0..max_iter {
607 if hi - lo < tolerance {
608 break;
609 }
610 let mid = (lo + hi) * 0.5;
611 let pa = add(c0_a, scale(vel_a, mid));
612 let pb = add(c0_b, scale(vel_b, mid));
613 let d = len(sub(pa, pb));
614 if d <= r_sum {
615 hi = mid;
616 } else {
617 lo = mid;
618 }
619 }
620 (lo + hi) * 0.5
621}
622
623#[derive(Debug, Clone)]
629pub struct SweptSphere {
630 pub pos0: [f64; 3],
632 pub pos1: [f64; 3],
634 pub radius: f64,
636 pub id: u32,
638}
639
640impl SweptSphere {
641 pub fn new(pos0: [f64; 3], pos1: [f64; 3], radius: f64, id: u32) -> Self {
643 Self {
644 pos0,
645 pos1,
646 radius,
647 id,
648 }
649 }
650
651 pub fn sweep_aabb(&self) -> ([f64; 3], [f64; 3]) {
653 let mut mn = [0.0f64; 3];
654 let mut mx = [0.0f64; 3];
655 for i in 0..3 {
656 mn[i] = self.pos0[i].min(self.pos1[i]) - self.radius;
657 mx[i] = self.pos0[i].max(self.pos1[i]) + self.radius;
658 }
659 (mn, mx)
660 }
661
662 pub fn sweep_aabb_overlaps(&self, other: &SweptSphere) -> bool {
664 let (amn, amx) = self.sweep_aabb();
665 let (bmn, bmx) = other.sweep_aabb();
666 amn[0] <= bmx[0]
667 && amx[0] >= bmn[0]
668 && amn[1] <= bmx[1]
669 && amx[1] >= bmn[1]
670 && amn[2] <= bmx[2]
671 && amx[2] >= bmn[2]
672 }
673}
674
675pub fn sweep_broadphase(spheres: &[SweptSphere]) -> Vec<(u32, u32)> {
679 let mut pairs = Vec::new();
680 for i in 0..spheres.len() {
681 for j in (i + 1)..spheres.len() {
682 if spheres[i].sweep_aabb_overlaps(&spheres[j]) {
683 let (a, b) = if spheres[i].id < spheres[j].id {
684 (spheres[i].id, spheres[j].id)
685 } else {
686 (spheres[j].id, spheres[i].id)
687 };
688 pairs.push((a, b));
689 }
690 }
691 }
692 pairs
693}
694
695pub fn swept_sphere_sphere_interval(
706 c0_a: [f64; 3],
707 vel_a: [f64; 3],
708 r_a: f64,
709 c0_b: [f64; 3],
710 vel_b: [f64; 3],
711 r_b: f64,
712 t_start: f64,
713 t_end: f64,
714) -> Option<SweepResult> {
715 let a0 = add(c0_a, scale(vel_a, t_start));
717 let a1 = add(c0_a, scale(vel_a, t_end));
718 let b0 = add(c0_b, scale(vel_b, t_start));
719 let b1 = add(c0_b, scale(vel_b, t_end));
720 swept_sphere_sphere(a0, a1, r_a, b0, b1, r_b)
721}
722
723pub struct CompoundSweptShape {
729 pub spheres: Vec<([f64; 3], f64)>, }
732
733impl CompoundSweptShape {
734 pub fn new(spheres: Vec<([f64; 3], f64)>) -> Self {
736 Self { spheres }
737 }
738
739 pub fn sweep_vs_compound(
744 &self,
745 pos0: [f64; 3],
746 pos1: [f64; 3],
747 other: &CompoundSweptShape,
748 other_pos0: [f64; 3],
749 other_pos1: [f64; 3],
750 ) -> Option<SweepResult> {
751 let mut best: Option<SweepResult> = None;
752
753 for (off_a, r_a) in &self.spheres {
754 let a0 = add(pos0, *off_a);
755 let a1 = add(pos1, *off_a);
756 for (off_b, r_b) in &other.spheres {
757 let b0 = add(other_pos0, *off_b);
758 let b1 = add(other_pos1, *off_b);
759 if let Some(result) = swept_sphere_sphere(a0, a1, *r_a, b0, b1, *r_b)
760 && best.as_ref().is_none_or(|br| result.toi < br.toi)
761 {
762 best = Some(result);
763 }
764 }
765 }
766 best
767 }
768}
769
770pub fn swept_capsule_aabb(
780 axis_start_0: [f64; 3],
781 axis_start_1: [f64; 3],
782 axis_end_0: [f64; 3],
783 axis_end_1: [f64; 3],
784 radius: f64,
785 aabb_min: [f64; 3],
786 aabb_max: [f64; 3],
787) -> Option<SweepResult> {
788 let mid_start = scale(add(axis_start_0, axis_start_1), 0.5);
790 let mid_end = scale(add(axis_end_0, axis_end_1), 0.5);
791 let half_len = len(scale(sub(axis_start_1, axis_start_0), 0.5));
793 let effective_radius = radius + half_len;
794 let exp_min = [
796 aabb_min[0] - effective_radius,
797 aabb_min[1] - effective_radius,
798 aabb_min[2] - effective_radius,
799 ];
800 let exp_max = [
801 aabb_max[0] + effective_radius,
802 aabb_max[1] + effective_radius,
803 aabb_max[2] + effective_radius,
804 ];
805 let dir = sub(mid_end, mid_start);
806 let hit = ray_aabb_slab(mid_start, dir, exp_min, exp_max, 0.0, 1.0)?;
807 let hit_center = add(mid_start, scale(dir, hit.t));
808 let closest_on_aabb = closest_point_on_aabb(hit_center, aabb_min, aabb_max);
809 let diff = sub(hit_center, closest_on_aabb);
810 let d = len(diff);
811 let normal = if d > 1e-10 {
812 normalize(diff)
813 } else {
814 hit.normal
815 };
816 Some(SweepResult {
817 toi: hit.t,
818 normal,
819 point_a: add(hit_center, scale(normal, -effective_radius)),
820 point_b: closest_on_aabb,
821 })
822}
823
824pub fn linear_ccd_sphere_sphere(
836 pos_a: [f64; 3],
837 vel_a: [f64; 3],
838 r_a: f64,
839 pos_b: [f64; 3],
840 vel_b: [f64; 3],
841 r_b: f64,
842 dt: f64,
843) -> Option<SweepResult> {
844 let c0_a = pos_a;
845 let c1_a = add(pos_a, scale(vel_a, dt));
846 let c0_b = pos_b;
847 let c1_b = add(pos_b, scale(vel_b, dt));
848 let result = swept_sphere_sphere(c0_a, c1_a, r_a, c0_b, c1_b, r_b)?;
849 Some(SweepResult {
851 toi: result.toi * dt,
852 ..result
853 })
854}
855
856pub fn bisect_toi<F>(
866 mut t_lo: f64,
867 mut t_hi: f64,
868 tolerance: f64,
869 max_iter: usize,
870 dist_fn: F,
871) -> f64
872where
873 F: Fn(f64) -> f64,
874{
875 for _ in 0..max_iter {
876 if t_hi - t_lo < tolerance {
877 break;
878 }
879 let mid = (t_lo + t_hi) * 0.5;
880 if dist_fn(mid) >= 0.0 {
881 t_lo = mid;
882 } else {
883 t_hi = mid;
884 }
885 }
886 (t_lo + t_hi) * 0.5
887}
888
889#[derive(Debug, Clone)]
895pub struct RotatingBodyState {
896 pub position: [f64; 3],
898 pub linear_vel: [f64; 3],
900 pub angular_speed: f64,
902 pub bound_radius: f64,
904}
905
906impl RotatingBodyState {
907 pub fn new(
909 position: [f64; 3],
910 linear_vel: [f64; 3],
911 angular_speed: f64,
912 bound_radius: f64,
913 ) -> Self {
914 Self {
915 position,
916 linear_vel,
917 angular_speed,
918 bound_radius,
919 }
920 }
921
922 pub fn motion_bound(&self, dt: f64) -> f64 {
926 let linear_speed = len(self.linear_vel);
927 (linear_speed + self.angular_speed * self.bound_radius) * dt
928 }
929
930 pub fn advance(&self, dt: f64) -> [f64; 3] {
932 add(self.position, scale(self.linear_vel, dt))
933 }
934}
935
936pub fn conservative_advancement_rotating(
943 a: &RotatingBodyState,
944 b: &RotatingBodyState,
945 separation: f64,
946 t_start: f64,
947 t_end: f64,
948 tolerance: f64,
949 max_iter: usize,
950) -> Option<f64> {
951 let total_dt = t_end - t_start;
952 if total_dt <= 0.0 {
953 return None;
954 }
955 let sum_r = a.bound_radius + b.bound_radius;
956 let init_dist = len(sub(b.position, a.position)) - sum_r;
957 if init_dist <= 0.0 {
958 return Some(t_start);
959 }
960 if separation < 0.0 {
961 return Some(t_start);
962 }
963
964 let mut t = t_start;
965 let mut remaining_sep = separation.max(init_dist);
966
967 for _ in 0..max_iter {
968 if remaining_sep <= tolerance {
969 return Some(t);
970 }
971 let remaining_dt = t_end - t;
972 if remaining_dt <= 0.0 {
973 break;
974 }
975 let motion_a = a.motion_bound(remaining_dt);
976 let motion_b = b.motion_bound(remaining_dt);
977 let total_motion = motion_a + motion_b;
978 if total_motion < 1e-14 {
979 break;
980 }
981 let dt_step = (remaining_sep / total_motion).min(remaining_dt);
982 t += dt_step;
983 let pos_a_t = a.advance(t - t_start);
985 let pos_b_t = b.advance(t - t_start);
986 let new_dist = len(sub(pos_b_t, pos_a_t)) - sum_r;
987 if new_dist <= tolerance {
988 return Some(t);
989 }
990 remaining_sep = new_dist;
991 }
992 None
993}
994
995#[derive(Debug, Clone)]
1001pub struct TriangleMeshSweepResult {
1002 pub toi: f64,
1004 pub tri_index: usize,
1006 pub normal: [f64; 3],
1008 pub point_a: [f64; 3],
1010 pub point_b: [f64; 3],
1012}
1013
1014pub fn swept_sphere_triangle_mesh(
1018 c0: [f64; 3],
1019 c1: [f64; 3],
1020 radius: f64,
1021 triangles: &[[[f64; 3]; 3]],
1022) -> Option<TriangleMeshSweepResult> {
1023 let mut best: Option<TriangleMeshSweepResult> = None;
1024
1025 for (i, tri) in triangles.iter().enumerate() {
1026 let v0 = tri[0];
1027 let v1 = tri[1];
1028 let v2 = tri[2];
1029
1030 let e1 = sub(v1, v0);
1032 let e2 = sub(v2, v0);
1033 let raw_normal = cross(e1, e2);
1034 if len(raw_normal) < 1e-12 {
1035 continue;
1036 }
1037 let tri_normal_raw = normalize(raw_normal);
1038
1039 let approach_normal = if dot(tri_normal_raw, sub(c0, v0)) >= 0.0 {
1042 tri_normal_raw
1043 } else {
1044 scale(tri_normal_raw, -1.0)
1045 };
1046
1047 let plane_d = dot(approach_normal, v0) + radius;
1048
1049 let d0 = dot(approach_normal, c0) - plane_d;
1051 let d1 = dot(approach_normal, c1) - plane_d;
1052
1053 if d0 < 0.0 || (d0 - d1).abs() < 1e-14 {
1054 continue;
1055 }
1056
1057 let t = d0 / (d0 - d1);
1058 if !(0.0..=1.0).contains(&t) {
1059 continue;
1060 }
1061
1062 let dir = sub(c1, c0);
1064 let center_t = add(c0, scale(dir, t));
1065
1066 let proj = sub(
1068 center_t,
1069 scale(approach_normal, dot(approach_normal, sub(center_t, v0))),
1070 );
1071 let closest_on_tri = closest_point_on_tri(v0, v1, v2, proj);
1073 let diff = sub(center_t, closest_on_tri);
1074 let d = len(diff);
1075 if d > radius + 1e-6 {
1076 continue;
1077 }
1078
1079 let normal = if d > 1e-10 {
1080 normalize(diff)
1081 } else {
1082 approach_normal
1083 };
1084
1085 let result = TriangleMeshSweepResult {
1086 toi: t,
1087 tri_index: i,
1088 normal,
1089 point_a: add(center_t, scale(normal, -radius)),
1090 point_b: closest_on_tri,
1091 };
1092
1093 if best.as_ref().is_none_or(|br| t < br.toi) {
1094 best = Some(result);
1095 }
1096 }
1097
1098 best
1099}
1100
1101fn closest_point_on_tri(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], p: [f64; 3]) -> [f64; 3] {
1103 let ab = sub(v1, v0);
1104 let ac = sub(v2, v0);
1105 let ap = sub(p, v0);
1106 let d1 = dot(ab, ap);
1107 let d2 = dot(ac, ap);
1108 if d1 <= 0.0 && d2 <= 0.0 {
1109 return v0;
1110 }
1111 let bp = sub(p, v1);
1112 let d3 = dot(ab, bp);
1113 let d4 = dot(ac, bp);
1114 if d3 >= 0.0 && d4 <= d3 {
1115 return v1;
1116 }
1117 let vc = d1 * d4 - d3 * d2;
1118 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
1119 let v = d1 / (d1 - d3);
1120 return add(v0, scale(ab, v));
1121 }
1122 let cp = sub(p, v2);
1123 let d5 = dot(ab, cp);
1124 let d6 = dot(ac, cp);
1125 if d6 >= 0.0 && d5 <= d6 {
1126 return v2;
1127 }
1128 let vb = d5 * d2 - d1 * d6;
1129 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
1130 let w = d2 / (d2 - d6);
1131 return add(v0, scale(ac, w));
1132 }
1133 let va = d3 * d6 - d5 * d4;
1134 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
1135 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
1136 return add(v1, scale(sub(v2, v1), w));
1137 }
1138 let denom = 1.0 / (va + vb + vc);
1139 let v = vb * denom;
1140 let w = vc * denom;
1141 add(add(v0, scale(ab, v)), scale(ac, w))
1142}
1143
1144pub fn swept_sphere_capsule(
1154 c0_s: [f64; 3],
1155 c1_s: [f64; 3],
1156 r_s: f64,
1157 cap0: [f64; 3],
1158 cap1: [f64; 3],
1159 r_c: f64,
1160) -> Option<SweepResult> {
1161 let mid_sphere = scale(add(c0_s, c1_s), 0.5);
1163 let axis = sub(cap1, cap0);
1164 let axis_len_sq = dot(axis, axis);
1165 let t = if axis_len_sq < 1e-20 {
1166 0.0
1167 } else {
1168 (dot(sub(mid_sphere, cap0), axis) / axis_len_sq).clamp(0.0, 1.0)
1169 };
1170 let cap_closest = add(cap0, scale(axis, t));
1171 swept_sphere_sphere(c0_s, c1_s, r_s, cap_closest, cap_closest, r_c)
1173}
1174
1175#[cfg(test)]
1180mod tests {
1181 use super::*;
1182
1183 #[test]
1186 fn test_swept_sphere_sphere_head_on() {
1187 let result = swept_sphere_sphere(
1196 [-1.5, 0.0, 0.0],
1197 [-0.5, 0.0, 0.0],
1198 0.5,
1199 [1.5, 0.0, 0.0],
1200 [0.5, 0.0, 0.0],
1201 0.5,
1202 );
1203 assert!(result.is_some(), "head-on spheres should hit");
1204 let hit = result.unwrap();
1205 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1206 }
1207
1208 #[test]
1209 fn test_swept_sphere_sphere_miss() {
1210 let result = swept_sphere_sphere(
1212 [0.0, 0.0, 0.0],
1213 [1.0, 0.0, 0.0],
1214 0.5,
1215 [0.0, 5.0, 0.0],
1216 [1.0, 5.0, 0.0],
1217 0.5,
1218 );
1219 assert!(result.is_none(), "parallel spheres should not collide");
1220 }
1221
1222 #[test]
1223 fn test_swept_sphere_sphere_already_overlapping() {
1224 let result = swept_sphere_sphere(
1226 [0.0, 0.0, 0.0],
1227 [1.0, 0.0, 0.0],
1228 0.5,
1229 [0.5, 0.0, 0.0],
1230 [1.5, 0.0, 0.0],
1231 0.5,
1232 );
1233 assert!(result.is_some());
1234 assert_eq!(result.unwrap().toi, 0.0);
1235 }
1236
1237 #[test]
1238 fn test_swept_sphere_sphere_moving_apart() {
1239 let result = swept_sphere_sphere(
1241 [0.0, 0.0, 0.0],
1242 [-1.0, 0.0, 0.0],
1243 0.5,
1244 [3.0, 0.0, 0.0],
1245 [4.0, 0.0, 0.0],
1246 0.5,
1247 );
1248 assert!(result.is_none(), "diverging spheres should not collide");
1249 }
1250
1251 #[test]
1254 fn test_swept_sphere_aabb_hit() {
1255 let result = swept_sphere_aabb(
1259 [0.0, 5.0, 0.0],
1260 [0.0, -1.0, 0.0],
1261 0.5,
1262 [-1.0, -1.0, -1.0],
1263 [1.0, 1.0, 1.0],
1264 );
1265 assert!(result.is_some(), "sphere moving toward AABB should hit");
1266 let hit = result.unwrap();
1267 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1268 }
1269
1270 #[test]
1271 fn test_swept_sphere_aabb_miss() {
1272 let result = swept_sphere_aabb(
1274 [10.0, 0.0, 0.0],
1275 [20.0, 0.0, 0.0],
1276 0.5,
1277 [-1.0, -1.0, -1.0],
1278 [1.0, 1.0, 1.0],
1279 );
1280 assert!(result.is_none(), "sphere moving away should not hit");
1281 }
1282
1283 #[test]
1286 fn test_swept_aabb_aabb_hit() {
1287 let result = swept_aabb_aabb(
1289 [-3.0, -0.5, -0.5],
1290 [-2.0, 0.5, 0.5], [2.5, 0.0, 0.0], [0.0, -0.5, -0.5],
1293 [1.0, 0.5, 0.5], );
1295 assert!(result.is_some(), "sliding AABB should hit static AABB");
1296 let hit = result.unwrap();
1297 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1298 }
1299
1300 #[test]
1301 fn test_swept_aabb_aabb_miss() {
1302 let result = swept_aabb_aabb(
1304 [-3.0, -0.5, -0.5],
1305 [-2.0, 0.5, 0.5],
1306 [-5.0, 0.0, 0.0],
1307 [2.0, -0.5, -0.5],
1308 [3.0, 0.5, 0.5],
1309 );
1310 assert!(result.is_none(), "AABB moving away should not hit");
1311 }
1312
1313 #[test]
1316 fn test_ray_sphere_hit() {
1317 let hit = ray_sphere(
1319 [-5.0, 0.0, 0.0],
1320 [1.0, 0.0, 0.0],
1321 [0.0, 0.0, 0.0],
1322 1.0,
1323 0.0,
1324 100.0,
1325 );
1326 assert!(hit.is_some(), "ray should hit sphere");
1327 let h = hit.unwrap();
1328 assert!((h.t - 4.0).abs() < 1e-9, "expected t=4.0, got {}", h.t);
1329 }
1330
1331 #[test]
1332 fn test_ray_sphere_miss() {
1333 let hit = ray_sphere(
1335 [-5.0, 2.0, 0.0],
1336 [1.0, 0.0, 0.0],
1337 [0.0, 0.0, 0.0],
1338 1.0,
1339 0.0,
1340 100.0,
1341 );
1342 assert!(hit.is_none(), "ray should miss sphere");
1343 }
1344
1345 #[test]
1346 fn test_ray_sphere_behind() {
1347 let hit = ray_sphere(
1349 [5.0, 0.0, 0.0],
1350 [1.0, 0.0, 0.0],
1351 [0.0, 0.0, 0.0],
1352 1.0,
1353 0.0,
1354 100.0,
1355 );
1356 assert!(hit.is_none(), "ray going away from sphere should miss");
1357 }
1358
1359 #[test]
1362 fn test_ray_aabb_hit() {
1363 let hit = ray_aabb(
1364 [-5.0, 0.0, 0.0],
1365 [1.0, 0.0, 0.0],
1366 [-1.0, -1.0, -1.0],
1367 [1.0, 1.0, 1.0],
1368 0.0,
1369 100.0,
1370 );
1371 assert!(hit.is_some(), "ray should hit AABB");
1372 let h = hit.unwrap();
1373 assert!((h.t - 4.0).abs() < 1e-9, "expected t=4.0, got {}", h.t);
1374 }
1375
1376 #[test]
1377 fn test_ray_aabb_miss() {
1378 let hit = ray_aabb(
1379 [-5.0, 3.0, 0.0],
1380 [1.0, 0.0, 0.0],
1381 [-1.0, -1.0, -1.0],
1382 [1.0, 1.0, 1.0],
1383 0.0,
1384 100.0,
1385 );
1386 assert!(hit.is_none(), "ray should miss AABB");
1387 }
1388
1389 #[test]
1392 fn test_ray_triangle_hit() {
1393 let hit = ray_triangle(
1395 [0.0, 5.0, 0.0],
1396 [0.0, -1.0, 0.0],
1397 [-1.0, 0.0, -1.0],
1398 [1.0, 0.0, -1.0],
1399 [0.0, 0.0, 1.0],
1400 0.0,
1401 100.0,
1402 );
1403 assert!(hit.is_some(), "ray should hit triangle");
1404 let h = hit.unwrap();
1405 assert!((h.t - 5.0).abs() < 1e-9, "expected t=5.0, got {}", h.t);
1406 }
1407
1408 #[test]
1409 fn test_ray_triangle_miss() {
1410 let hit = ray_triangle(
1412 [5.0, 5.0, 5.0],
1413 [0.0, -1.0, 0.0],
1414 [-1.0, 0.0, -1.0],
1415 [1.0, 0.0, -1.0],
1416 [0.0, 0.0, 1.0],
1417 0.0,
1418 100.0,
1419 );
1420 assert!(hit.is_none(), "ray should miss triangle");
1421 }
1422
1423 #[test]
1424 fn test_ray_triangle_normal_direction() {
1425 let hit = ray_triangle(
1427 [0.0, 5.0, 0.0],
1428 [0.0, -1.0, 0.0],
1429 [-1.0, 0.0, -1.0],
1430 [1.0, 0.0, -1.0],
1431 [0.0, 0.0, 1.0],
1432 0.0,
1433 100.0,
1434 );
1435 let h = hit.unwrap();
1436 let nl = len(h.normal);
1439 assert!(
1440 (nl - 1.0).abs() < 1e-9,
1441 "normal should be unit length, got {}",
1442 nl
1443 );
1444 }
1445
1446 #[test]
1449 fn test_point_aabb_dist_sq_inside() {
1450 let d = point_aabb_dist_sq([0.0, 0.0, 0.0], [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
1451 assert_eq!(d, 0.0, "point inside AABB has distance 0");
1452 }
1453
1454 #[test]
1455 fn test_point_aabb_dist_sq_outside() {
1456 let d = point_aabb_dist_sq([2.0, 0.0, 0.0], [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
1457 assert!(
1458 (d - 1.0).abs() < 1e-9,
1459 "point 1 unit outside AABB, expected dsq=1, got {}",
1460 d
1461 );
1462 }
1463
1464 #[test]
1467 fn test_sphere_aabb_overlap_touching() {
1468 assert!(sphere_aabb_overlap(
1470 [2.0, 0.0, 0.0],
1471 1.0,
1472 [-1.0, -1.0, -1.0],
1473 [1.0, 1.0, 1.0]
1474 ));
1475 }
1476
1477 #[test]
1478 fn test_sphere_aabb_overlap_separated() {
1479 assert!(!sphere_aabb_overlap(
1480 [3.0, 0.0, 0.0],
1481 1.0,
1482 [-1.0, -1.0, -1.0],
1483 [1.0, 1.0, 1.0]
1484 ));
1485 }
1486
1487 #[test]
1490 fn test_aabb_aabb_overlap_yes() {
1491 assert!(aabb_aabb_overlap(
1492 [-1.0, -1.0, -1.0],
1493 [1.0, 1.0, 1.0],
1494 [0.0, 0.0, 0.0],
1495 [2.0, 2.0, 2.0],
1496 ));
1497 }
1498
1499 #[test]
1500 fn test_aabb_aabb_overlap_no() {
1501 assert!(!aabb_aabb_overlap(
1502 [-1.0, -1.0, -1.0],
1503 [0.0, 0.0, 0.0],
1504 [1.0, 1.0, 1.0],
1505 [2.0, 2.0, 2.0],
1506 ));
1507 }
1508
1509 #[test]
1512 fn test_swept_capsule_plane_hit() {
1513 let result = swept_capsule_plane(
1515 [0.0, 5.0, 0.0],
1516 [0.0, -1.0, 0.0],
1517 0.5,
1518 1.0,
1519 [0.0, 1.0, 0.0],
1520 0.0,
1521 );
1522 assert!(result.is_some(), "capsule should hit floor plane");
1523 let hit = result.unwrap();
1524 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1525 }
1526
1527 #[test]
1528 fn test_swept_capsule_plane_moving_away() {
1529 let result = swept_capsule_plane(
1530 [0.0, 5.0, 0.0],
1531 [0.0, 10.0, 0.0],
1532 0.5,
1533 1.0,
1534 [0.0, 1.0, 0.0],
1535 0.0,
1536 );
1537 assert!(result.is_none(), "moving away from plane should not hit");
1538 }
1539
1540 #[test]
1541 fn test_swept_capsule_plane_already_touching() {
1542 let result = swept_capsule_plane(
1544 [0.0, 1.0, 0.0],
1545 [0.0, 0.0, 0.0],
1546 0.5,
1547 0.5,
1548 [0.0, 1.0, 0.0],
1549 0.0,
1550 );
1551 assert!(result.is_some());
1552 assert_eq!(result.unwrap().toi, 0.0);
1553 }
1554
1555 #[test]
1558 fn test_refine_toi_close_result() {
1559 let toi = refine_sphere_sphere_toi(
1562 [-1.0, 0.0, 0.0],
1563 [1.0, 0.0, 0.0],
1564 0.5,
1565 [1.0, 0.0, 0.0],
1566 [-1.0, 0.0, 0.0],
1567 0.5,
1568 0.0,
1569 1.0,
1570 1e-8,
1571 64,
1572 );
1573 assert!((toi - 0.5).abs() < 0.01, "refined toi={toi}, expected ≈0.5");
1574 }
1575
1576 #[test]
1579 fn test_swept_sphere_aabb_correct() {
1580 let s = SweptSphere::new([0.0; 3], [2.0, 0.0, 0.0], 1.0, 0);
1581 let (mn, mx) = s.sweep_aabb();
1582 assert!((mn[0] - (-1.0)).abs() < 1e-12);
1583 assert!((mx[0] - 3.0).abs() < 1e-12);
1584 }
1585
1586 #[test]
1587 fn test_swept_sphere_aabb_overlap() {
1588 let a = SweptSphere::new([0.0; 3], [1.0, 0.0, 0.0], 0.5, 0);
1589 let b = SweptSphere::new([0.5, 0.0, 0.0], [1.5, 0.0, 0.0], 0.5, 1);
1590 assert!(a.sweep_aabb_overlaps(&b));
1591 }
1592
1593 #[test]
1594 fn test_swept_sphere_aabb_no_overlap() {
1595 let a = SweptSphere::new([0.0; 3], [0.0; 3], 0.1, 0);
1596 let b = SweptSphere::new([10.0, 0.0, 0.0], [10.0, 0.0, 0.0], 0.1, 1);
1597 assert!(!a.sweep_aabb_overlaps(&b));
1598 }
1599
1600 #[test]
1603 fn test_sweep_broadphase_finds_pair() {
1604 let spheres = vec![
1605 SweptSphere::new([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.5, 10),
1606 SweptSphere::new([0.5, 0.0, 0.0], [2.0, 0.0, 0.0], 0.5, 20),
1607 SweptSphere::new([50.0, 0.0, 0.0], [51.0, 0.0, 0.0], 0.5, 30),
1608 ];
1609 let pairs = sweep_broadphase(&spheres);
1610 assert_eq!(pairs.len(), 1, "only pair (10,20) should overlap");
1611 assert_eq!(pairs[0], (10, 20));
1612 }
1613
1614 #[test]
1615 fn test_sweep_broadphase_no_pairs() {
1616 let spheres = vec![
1617 SweptSphere::new([0.0; 3], [0.0; 3], 0.1, 1),
1618 SweptSphere::new([100.0, 0.0, 0.0], [100.0, 0.0, 0.0], 0.1, 2),
1619 ];
1620 let pairs = sweep_broadphase(&spheres);
1621 assert!(pairs.is_empty());
1622 }
1623
1624 #[test]
1627 fn test_interval_sweep_finds_hit() {
1628 let result = swept_sphere_sphere_interval(
1631 [-5.0, 0.0, 0.0],
1632 [1.0, 0.0, 0.0],
1633 0.5,
1634 [5.0, 0.0, 0.0],
1635 [-1.0, 0.0, 0.0],
1636 0.5,
1637 4.0,
1638 6.0,
1639 );
1640 assert!(result.is_some(), "should hit within [4,6]");
1641 }
1642
1643 #[test]
1644 fn test_interval_sweep_no_hit_outside() {
1645 let result = swept_sphere_sphere_interval(
1649 [-5.0, 0.0, 0.0],
1650 [-1.0, 0.0, 0.0],
1651 0.5, [5.0, 0.0, 0.0],
1653 [1.0, 0.0, 0.0],
1654 0.5,
1655 0.0,
1656 5.0,
1657 );
1658 assert!(result.is_none(), "diverging spheres should not collide");
1659 }
1660
1661 #[test]
1664 fn test_compound_sweep_finds_earliest_hit() {
1665 let comp_a = CompoundSweptShape::new(vec![([0.0; 3], 0.5)]);
1669 let comp_b = CompoundSweptShape::new(vec![([0.0; 3], 0.5)]);
1670 let result = comp_a.sweep_vs_compound(
1671 [-5.0, 0.0, 0.0],
1672 [-1.0, 0.0, 0.0], &comp_b,
1674 [0.0, 0.0, 0.0],
1675 [0.0, 0.0, 0.0], );
1677 assert!(result.is_some(), "compound sweep should detect hit");
1679 }
1680
1681 #[test]
1682 fn test_compound_sweep_no_hit_separated() {
1683 let comp_a = CompoundSweptShape::new(vec![([0.0; 3], 0.5)]);
1684 let comp_b = CompoundSweptShape::new(vec![([0.0; 3], 0.5)]);
1685 let result = comp_a.sweep_vs_compound(
1687 [0.0; 3],
1688 [-10.0, 0.0, 0.0],
1689 &comp_b,
1690 [50.0, 0.0, 0.0],
1691 [50.0, 0.0, 0.0],
1692 );
1693 assert!(result.is_none());
1694 }
1695
1696 #[test]
1699 fn test_swept_capsule_aabb_hit() {
1700 let result = swept_capsule_aabb(
1702 [0.0, 5.0, -0.3],
1703 [0.0, 5.0, 0.3], [0.0, 0.0, -0.3],
1705 [0.0, 0.0, 0.3], 0.2,
1707 [-1.0, -1.0, -1.0],
1708 [1.0, 1.0, 1.0],
1709 );
1710 assert!(result.is_some(), "descending capsule should hit AABB");
1711 let hit = result.unwrap();
1712 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1713 }
1714
1715 #[test]
1716 fn test_swept_capsule_aabb_miss() {
1717 let result = swept_capsule_aabb(
1719 [10.0, 0.0, -0.3],
1720 [10.0, 0.0, 0.3],
1721 [20.0, 0.0, -0.3],
1722 [20.0, 0.0, 0.3],
1723 0.2,
1724 [-1.0, -1.0, -1.0],
1725 [1.0, 1.0, 1.0],
1726 );
1727 assert!(result.is_none(), "capsule moving away should not hit AABB");
1728 }
1729
1730 #[test]
1733 fn test_linear_ccd_sphere_sphere_hit() {
1734 let result = linear_ccd_sphere_sphere(
1737 [-1.0, 0.0, 0.0],
1738 [2.0, 0.0, 0.0],
1739 0.5,
1740 [1.0, 0.0, 0.0],
1741 [-2.0, 0.0, 0.0],
1742 0.5,
1743 1.0,
1744 );
1745 assert!(result.is_some(), "converging spheres should hit");
1746 let hit = result.unwrap();
1747 assert!(
1749 hit.toi >= 0.0 && hit.toi <= 1.0,
1750 "toi in [0,1], got {}",
1751 hit.toi
1752 );
1753 }
1754
1755 #[test]
1756 fn test_linear_ccd_sphere_sphere_no_hit() {
1757 let result = linear_ccd_sphere_sphere(
1758 [-10.0, 0.0, 0.0],
1759 [-1.0, 0.0, 0.0],
1760 0.3,
1761 [10.0, 0.0, 0.0],
1762 [1.0, 0.0, 0.0],
1763 0.3,
1764 1.0,
1765 );
1766 assert!(result.is_none(), "spheres moving away should not hit");
1767 }
1768
1769 #[test]
1772 fn test_bisect_toi_sphere_contact() {
1773 let toi = bisect_toi(0.0, 1.0, 1e-8, 64, |t| {
1776 let pa = add([-1.0, 0.0, 0.0], scale([1.0, 0.0, 0.0], t));
1777 let pb = add([1.0, 0.0, 0.0], scale([-1.0, 0.0, 0.0], t));
1778 len(sub(pa, pb)) - 1.0 });
1780 assert!(
1781 (toi - 0.5).abs() < 1e-6,
1782 "bisect_toi should find t≈0.5, got {toi}"
1783 );
1784 }
1785
1786 #[test]
1787 fn test_bisect_toi_already_contact() {
1788 let toi = bisect_toi(0.0, 1.0, 1e-8, 64, |_t| -1.0);
1790 assert!(toi < 0.5, "Should converge to low end when always negative");
1791 }
1792
1793 #[test]
1796 fn test_rotating_body_motion_bound() {
1797 let body = RotatingBodyState::new([0.0; 3], [1.0, 0.0, 0.0], 2.0, 0.5);
1798 let mb = body.motion_bound(1.0);
1801 assert!(
1802 (mb - 2.0).abs() < 1e-10,
1803 "motion_bound should be 2.0, got {mb}"
1804 );
1805 }
1806
1807 #[test]
1808 fn test_rotating_body_advance() {
1809 let body = RotatingBodyState::new([1.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.0, 1.0);
1810 let pos = body.advance(0.5);
1811 assert!(
1812 (pos[0] - 2.0).abs() < 1e-10,
1813 "advanced x should be 2.0, got {}",
1814 pos[0]
1815 );
1816 }
1817
1818 #[test]
1819 fn test_rotating_body_motion_bound_zero_dt() {
1820 let body = RotatingBodyState::new([0.0; 3], [10.0, 0.0, 0.0], 5.0, 1.0);
1821 let mb = body.motion_bound(0.0);
1822 assert_eq!(mb, 0.0, "motion_bound(0) should be 0");
1823 }
1824
1825 #[test]
1828 fn test_conservative_advancement_no_collision_moving_apart() {
1829 let a = RotatingBodyState::new([-10.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 0.0, 0.5);
1830 let b = RotatingBodyState::new([10.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.0, 0.5);
1831 let result = conservative_advancement_rotating(&a, &b, 18.0, 0.0, 1.0, 1e-4, 100);
1832 assert!(result.is_none(), "diverging bodies should not collide");
1833 }
1834
1835 #[test]
1836 fn test_conservative_advancement_already_overlapping() {
1837 let a = RotatingBodyState::new([0.0; 3], [0.0; 3], 0.0, 1.0);
1838 let b = RotatingBodyState::new([0.5, 0.0, 0.0], [0.0; 3], 0.0, 1.0);
1839 let result = conservative_advancement_rotating(&a, &b, 0.0, 0.0, 1.0, 1e-4, 100);
1840 assert!(result.is_some(), "overlapping at start should return a toi");
1842 assert_eq!(result.unwrap(), 0.0, "overlapping at start → toi = t_start");
1843 }
1844
1845 #[test]
1848 fn test_swept_sphere_triangle_mesh_hit() {
1849 let triangles = vec![[[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [0.0, 0.0, 1.0]]];
1851 let result = swept_sphere_triangle_mesh([0.0, 3.0, 0.0], [0.0, -1.0, 0.0], 0.5, &triangles);
1852 assert!(
1853 result.is_some(),
1854 "sphere sweeping toward triangle should hit"
1855 );
1856 let hit = result.unwrap();
1857 assert!(hit.toi >= 0.0 && hit.toi <= 1.0, "toi={}", hit.toi);
1858 assert_eq!(hit.tri_index, 0, "should hit first triangle");
1859 }
1860
1861 #[test]
1862 fn test_swept_sphere_triangle_mesh_miss() {
1863 let triangles = vec![[[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [0.0, 0.0, 1.0]]];
1865 let result =
1866 swept_sphere_triangle_mesh([10.0, 3.0, 0.0], [10.0, -1.0, 0.0], 0.5, &triangles);
1867 assert!(result.is_none(), "sphere far from triangle should miss");
1868 }
1869
1870 #[test]
1871 fn test_swept_sphere_triangle_mesh_multiple_triangles() {
1872 let triangles = vec![
1874 [[-1.0, -5.0, -1.0], [1.0, -5.0, -1.0], [0.0, -5.0, 1.0]], [[-1.0, 1.0, -1.0], [1.0, 1.0, -1.0], [0.0, 1.0, 1.0]], ];
1877 let result = swept_sphere_triangle_mesh([0.0, 5.0, 0.0], [0.0, -5.0, 0.0], 0.3, &triangles);
1879 assert!(result.is_some(), "should hit a triangle");
1880 }
1881
1882 #[test]
1885 fn test_swept_sphere_capsule_hit() {
1886 let result = swept_sphere_capsule(
1889 [-3.0, 0.0, 0.0],
1890 [0.5, 0.0, 0.0],
1891 0.3,
1892 [0.0, -1.0, 0.0],
1893 [0.0, 1.0, 0.0],
1894 0.5,
1895 );
1896 assert!(result.is_some(), "sphere sweeping past capsule should hit");
1897 }
1898
1899 #[test]
1900 fn test_swept_sphere_capsule_miss() {
1901 let result = swept_sphere_capsule(
1903 [5.0, 0.0, 0.0],
1904 [10.0, 0.0, 0.0],
1905 0.3,
1906 [0.0, -1.0, 0.0],
1907 [0.0, 1.0, 0.0],
1908 0.5,
1909 );
1910 assert!(
1911 result.is_none(),
1912 "sphere moving away should not hit capsule"
1913 );
1914 }
1915
1916 #[test]
1917 fn test_swept_sphere_capsule_already_overlapping() {
1918 let result = swept_sphere_capsule(
1920 [0.3, 0.0, 0.0],
1921 [0.5, 0.0, 0.0],
1922 0.3,
1923 [0.0, -1.0, 0.0],
1924 [0.0, 1.0, 0.0],
1925 0.5,
1926 );
1927 assert!(result.is_some(), "overlapping should produce result");
1928 assert_eq!(result.unwrap().toi, 0.0, "overlap at start → toi=0");
1929 }
1930
1931 #[test]
1934 fn test_ray_sphere_grazing() {
1935 let hit = ray_sphere(
1937 [-5.0, 1.0, 0.0],
1938 [1.0, 0.0, 0.0],
1939 [0.0, 0.0, 0.0],
1940 1.0,
1941 0.0,
1942 100.0,
1943 );
1944 assert!(hit.is_some(), "grazing ray should hit sphere at tangent");
1945 let h = hit.unwrap();
1946 assert!(
1947 (h.point[1] - 1.0).abs() < 1e-6,
1948 "hit y should be ≈1 (tangent)"
1949 );
1950 }
1951
1952 #[test]
1953 fn test_ray_aabb_inside_origin() {
1954 let hit = ray_aabb(
1956 [0.0, 0.0, 0.0],
1957 [1.0, 0.0, 0.0],
1958 [-1.0, -1.0, -1.0],
1959 [1.0, 1.0, 1.0],
1960 0.0,
1961 100.0,
1962 );
1963 assert!(
1964 hit.is_some(),
1965 "ray from inside AABB should hit the far wall"
1966 );
1967 }
1968
1969 #[test]
1970 fn test_point_aabb_dist_sq_corner() {
1971 let d = point_aabb_dist_sq([2.0, 2.0, 2.0], [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
1973 assert!((d - 3.0).abs() < 1e-10, "Expected dsq=3.0, got {d}");
1974 }
1975
1976 #[test]
1977 fn test_aabb_aabb_overlap_touching_face() {
1978 assert!(
1980 aabb_aabb_overlap(
1981 [0.0, 0.0, 0.0],
1982 [1.0, 1.0, 1.0],
1983 [1.0, 0.0, 0.0],
1984 [2.0, 1.0, 1.0],
1985 ),
1986 "face-touching AABBs should overlap"
1987 );
1988 }
1989}