1use crate::collision::ContactPoint;
27use crate::components::ColliderShape;
28use crate::gjk::Gjk;
29use gizmo_math::{Quat, Vec3};
30
31pub struct NarrowPhase;
36
37impl NarrowPhase {
38 pub fn sphere_sphere(pos_a: Vec3, r_a: f32, pos_b: Vec3, r_b: f32) -> Option<ContactPoint> {
42 let d = pos_b - pos_a;
43 let d2 = d.length_squared();
44 let rsum = r_a + r_b;
45
46 if d2 >= rsum * rsum || d2 < 1e-10 {
48 return None;
49 }
50
51 let dist = d2.sqrt();
52 let normal = d / dist; Some(mk_contact(pos_a + normal * r_a, normal, rsum - dist))
54 }
55
56 pub fn sphere_plane(
61 sph_pos: Vec3,
62 r: f32,
63 plane_n: Vec3,
64 plane_d: f32,
65 ) -> Option<ContactPoint> {
66 let signed_dist = sph_pos.dot(plane_n) - plane_d;
68 if signed_dist >= r {
69 return None; }
71 let point = sph_pos - plane_n * signed_dist;
73 Some(mk_contact(point, -plane_n, r - signed_dist))
75 }
76
77 pub fn box_plane(
81 bpos: Vec3,
82 brot: Quat,
83 half: Vec3,
84 plane_n: Vec3,
85 plane_d: f32,
86 ) -> Vec<ContactPoint> {
87 box_corners(bpos, brot, half)
88 .iter()
89 .filter_map(|&corner| {
90 let signed_dist = corner.dot(plane_n) - plane_d;
91 if signed_dist < 0.0 {
92 Some(mk_contact(
94 corner - plane_n * signed_dist,
95 -plane_n,
96 -signed_dist,
97 ))
98 } else {
99 None
100 }
101 })
102 .collect()
103 }
104
105 pub fn shape_plane(
108 shape: &ColliderShape,
109 pos: Vec3,
110 rot: Quat,
111 plane_n: Vec3,
112 plane_d: f32,
113 ) -> Option<ContactPoint> {
114 let deepest = Gjk::support_point(shape, pos, rot, -plane_n);
117 let signed_dist = deepest.dot(plane_n) - plane_d;
118 if signed_dist < 0.0 {
119 Some(mk_contact(
120 deepest - plane_n * signed_dist,
121 -plane_n,
122 -signed_dist,
123 ))
124 } else {
125 None
126 }
127 }
128
129 pub fn box_box(
136 pos_a: Vec3,
137 rot_a: Quat,
138 ha: Vec3,
139 pos_b: Vec3,
140 rot_b: Quat,
141 hb: Vec3,
142 ) -> Vec<ContactPoint> {
143 let ax = [
145 rot_a.mul_vec3(Vec3::X),
146 rot_a.mul_vec3(Vec3::Y),
147 rot_a.mul_vec3(Vec3::Z),
148 ];
149 let bx = [
150 rot_b.mul_vec3(Vec3::X),
151 rot_b.mul_vec3(Vec3::Y),
152 rot_b.mul_vec3(Vec3::Z),
153 ];
154 let ha_ = [ha.x, ha.y, ha.z];
155 let hb_ = [hb.x, hb.y, hb.z];
156 let t = pos_b - pos_a; let mut axes = [Vec3::ZERO; 15];
163 let mut n_axes = 0usize;
164
165 for &a in &ax {
166 axes[n_axes] = a;
167 n_axes += 1;
168 }
169 for &b in &bx {
170 axes[n_axes] = b;
171 n_axes += 1;
172 }
173
174 for &a in &ax {
175 for &b in &bx {
176 let c = a.cross(b);
177 let len_sq = c.length_squared();
178 if len_sq > 1e-6 {
179 axes[n_axes] = c * len_sq.sqrt().recip();
181 n_axes += 1;
182 }
183 }
184 }
185
186 let mut min_pen = f32::MAX;
188 let mut best_axis = Vec3::Y;
189 let mut flip = false;
190
191 for &axis in &axes[..n_axes] {
192 let pen = sat_penetration(&axis, &ax, &ha_, &bx, &hb_, t);
193 if pen < 0.0 {
194 return vec![]; }
196 if pen < min_pen {
197 min_pen = pen;
198 best_axis = axis;
199 flip = t.dot(axis) < 0.0;
201 }
202 }
203
204 let normal = if flip { -best_axis } else { best_axis };
205
206 let (ref_pos, ref_rot, ref_h, inc_pos, inc_rot, inc_h, ref_is_a) =
211 if is_face_axis(normal, &ax, 0.707) {
212 (pos_a, rot_a, ha, pos_b, rot_b, hb, true)
213 } else if is_face_axis(normal, &bx, 0.707) {
214 (pos_b, rot_b, hb, pos_a, rot_a, ha, false)
215 } else {
216 let dot_a = ax
218 .iter()
219 .map(|a| a.dot(normal).abs())
220 .fold(0.0f32, f32::max);
221 let dot_b = bx
222 .iter()
223 .map(|b| b.dot(normal).abs())
224 .fold(0.0f32, f32::max);
225 if dot_a >= dot_b {
226 (pos_a, rot_a, ha, pos_b, rot_b, hb, true)
227 } else {
228 (pos_b, rot_b, hb, pos_a, rot_a, ha, false)
229 }
230 };
231
232 let clip_normal = if ref_is_a { normal } else { -normal };
240 let mut contacts = clip_box_box(
241 clip_normal, min_pen, ref_pos, ref_rot, ref_h, inc_pos, inc_rot, inc_h,
242 );
243 if !ref_is_a {
244 for c in &mut contacts {
245 c.normal = -c.normal; }
247 }
248
249 if contacts.is_empty() {
254 contacts = clip_box_box(
255 -clip_normal, min_pen, inc_pos, inc_rot, inc_h, ref_pos, ref_rot, ref_h,
256 );
257 if ref_is_a {
259 for c in &mut contacts {
260 c.normal = -c.normal;
261 }
262 }
263 }
264
265 if contacts.is_empty() {
268 let shape_a = ColliderShape::Box(crate::components::BoxShape { half_extents: ha });
269 let shape_b = ColliderShape::Box(crate::components::BoxShape { half_extents: hb });
270 if let Some(c) = Gjk::get_contact(&shape_a, pos_a, rot_a, &shape_b, pos_b, rot_b) {
271 contacts.push(c);
272 }
273 }
274
275 contacts
276 }
277
278 pub fn test_collision(
287 shape_a: &ColliderShape,
288 pos_a: Vec3,
289 rot_a: Quat,
290 shape_b: &ColliderShape,
291 pos_b: Vec3,
292 rot_b: Quat,
293 ) -> Option<ContactPoint> {
294 let contacts = Self::test_collision_manifold(shape_a, pos_a, rot_a, shape_b, pos_b, rot_b);
295 contacts
296 .into_iter()
297 .max_by(|a, b| a.penetration.total_cmp(&b.penetration))
298 }
299
300 pub fn test_collision_manifold(
305 shape_a: &ColliderShape,
306 pos_a: Vec3,
307 rot_a: Quat,
308 shape_b: &ColliderShape,
309 pos_b: Vec3,
310 rot_b: Quat,
311 ) -> Vec<ContactPoint> {
312 if let ColliderShape::Compound(parts) = shape_a {
314 return parts
315 .iter()
316 .flat_map(|(local_t, sub)| {
317 let wp = pos_a + rot_a.mul_vec3(local_t.position);
318 let wr = rot_a * local_t.rotation;
319 Self::test_collision_manifold(sub, wp, wr, shape_b, pos_b, rot_b)
320 })
321 .collect();
322 }
323 if let ColliderShape::Compound(parts) = shape_b {
324 return parts
325 .iter()
326 .flat_map(|(local_t, sub)| {
327 let wp = pos_b + rot_b.mul_vec3(local_t.position);
328 let wr = rot_b * local_t.rotation;
329 Self::test_collision_manifold(shape_a, pos_a, rot_a, sub, wp, wr)
330 })
331 .collect();
332 }
333
334 let mut contacts: Vec<ContactPoint> = match (shape_a, shape_b) {
336 (ColliderShape::Sphere(sa), ColliderShape::Sphere(sb)) => {
338 Self::sphere_sphere(pos_a, sa.radius, pos_b, sb.radius)
339 .into_iter()
340 .collect()
341 }
342
343 (ColliderShape::Sphere(s), ColliderShape::Plane(p)) => {
345 Self::sphere_plane(pos_a, s.radius, p.normal, p.distance)
346 .into_iter()
347 .collect()
348 }
349
350 (ColliderShape::Plane(p), ColliderShape::Sphere(s)) => {
352 Self::sphere_plane(pos_b, s.radius, p.normal, p.distance)
353 .map(|mut c| {
354 c.normal = -c.normal;
355 c
356 })
357 .into_iter()
358 .collect()
359 }
360
361 (ColliderShape::Box(b), ColliderShape::Plane(p)) => {
363 Self::box_plane(pos_a, rot_a, b.half_extents, p.normal, p.distance)
364 }
365
366 (ColliderShape::Plane(p), ColliderShape::Box(b)) => {
368 let mut cs = Self::box_plane(pos_b, rot_b, b.half_extents, p.normal, p.distance);
369 for c in &mut cs {
370 c.normal = -c.normal;
371 }
372 cs
373 }
374
375 (ColliderShape::Box(ba), ColliderShape::Box(bb)) => {
377 Self::box_box(pos_a, rot_a, ba.half_extents, pos_b, rot_b, bb.half_extents)
378 }
379
380 (_, ColliderShape::Plane(p)) => {
382 Self::shape_plane(shape_a, pos_a, rot_a, p.normal, p.distance)
383 .into_iter()
384 .collect()
385 }
386
387 (ColliderShape::Plane(p), _) => {
389 Self::shape_plane(shape_b, pos_b, rot_b, p.normal, p.distance)
390 .map(|mut c| {
391 c.normal = -c.normal;
392 c
393 })
394 .into_iter()
395 .collect()
396 }
397
398 _ => Gjk::get_contact(shape_a, pos_a, rot_a, shape_b, pos_b, rot_b)
400 .into_iter()
401 .collect(),
402 };
403
404 for c in &mut contacts {
406 c.local_point_a = c.point - pos_a;
407 c.local_point_b = c.point - pos_b;
408 }
409
410 contacts
411 }
412}
413
414#[inline]
423fn sat_penetration(
424 axis: &Vec3,
425 ax: &[Vec3; 3],
426 ha: &[f32; 3],
427 bx: &[Vec3; 3],
428 hb: &[f32; 3],
429 t: Vec3,
430) -> f32 {
431 let proj_a: f32 = ax
432 .iter()
433 .zip(ha)
434 .map(|(e, &h)| e.dot(*axis).abs() * h)
435 .sum();
436 let proj_b: f32 = bx
437 .iter()
438 .zip(hb)
439 .map(|(e, &h)| e.dot(*axis).abs() * h)
440 .sum();
441 let dist = t.dot(*axis).abs();
442 proj_a + proj_b - dist
443}
444
445#[inline]
450fn is_face_axis(normal: Vec3, axes: &[Vec3; 3], threshold: f32) -> bool {
451 axes.iter().any(|a| a.dot(normal).abs() > threshold)
452}
453
454fn box_corners(pos: Vec3, rot: Quat, h: Vec3) -> [Vec3; 8] {
460 const SIGNS: [(f32, f32, f32); 8] = [
461 (1., 1., 1.),
462 (-1., 1., 1.),
463 (1., -1., 1.),
464 (-1., -1., 1.),
465 (1., 1., -1.),
466 (-1., 1., -1.),
467 (1., -1., -1.),
468 (-1., -1., -1.),
469 ];
470 SIGNS.map(|(sx, sy, sz)| pos + rot.mul_vec3(Vec3::new(sx * h.x, sy * h.y, sz * h.z)))
471}
472
473#[inline]
475fn mk_contact(point: Vec3, normal: Vec3, penetration: f32) -> ContactPoint {
476 ContactPoint {
477 point,
478 normal,
479 penetration,
480 ..Default::default()
481 }
482}
483
484fn select_4_contacts(contacts: Vec<ContactPoint>) -> Vec<ContactPoint> {
491 if contacts.len() <= 4 {
492 return contacts;
493 }
494
495 let n = contacts.len();
496
497 let i0 = (0..n)
499 .max_by(|&a, &b| contacts[a].penetration.total_cmp(&contacts[b].penetration))
500 .unwrap();
501
502 let mut chosen = vec![i0];
503
504 for _ in 0..3 {
506 if chosen.len() == n {
507 break;
508 }
509 let next = (0..n).filter(|i| !chosen.contains(i)).max_by(|&a, &b| {
510 let da = chosen
511 .iter()
512 .map(|&c| (contacts[c].point - contacts[a].point).length_squared())
513 .fold(f32::INFINITY, f32::min);
514 let db = chosen
515 .iter()
516 .map(|&c| (contacts[c].point - contacts[b].point).length_squared())
517 .fold(f32::INFINITY, f32::min);
518 da.total_cmp(&db)
519 });
520 if let Some(idx) = next {
521 chosen.push(idx);
522 }
523 }
524
525 chosen.iter().map(|&i| contacts[i]).collect()
526}
527
528fn clip_box_box(
537 normal: Vec3,
538 _min_pen: f32,
539 ref_pos: Vec3,
540 ref_rot: Quat,
541 ref_h: Vec3,
542 inc_pos: Vec3,
543 inc_rot: Quat,
544 inc_h: Vec3,
545) -> Vec<ContactPoint> {
546 let ref_axes = [
547 ref_rot.mul_vec3(Vec3::X),
548 ref_rot.mul_vec3(Vec3::Y),
549 ref_rot.mul_vec3(Vec3::Z),
550 ];
551 let ref_h_arr = [ref_h.x, ref_h.y, ref_h.z];
552
553 let (face_idx, _) = ref_axes
555 .iter()
556 .enumerate()
557 .map(|(i, a)| (i, a.dot(normal).abs()))
558 .fold(
559 (0, 0.0f32),
560 |(bi, bv), (i, v)| if v > bv { (i, v) } else { (bi, bv) },
561 );
562
563 let ref_face_d = ref_pos.dot(normal)
571 + ref_axes[0].dot(normal).abs() * ref_h_arr[0]
572 + ref_axes[1].dot(normal).abs() * ref_h_arr[1]
573 + ref_axes[2].dot(normal).abs() * ref_h_arr[2];
574
575 let t0 = ref_axes[(face_idx + 1) % 3];
577 let t1 = ref_axes[(face_idx + 2) % 3];
578 let e0 = ref_h_arr[(face_idx + 1) % 3];
579 let e1 = ref_h_arr[(face_idx + 2) % 3];
580
581 const SLAB_TOLERANCE: f32 = 1e-3;
583
584 let contacts: Vec<ContactPoint> = box_corners(inc_pos, inc_rot, inc_h)
585 .iter()
586 .filter_map(|&corner| {
587 let signed_depth = ref_face_d - corner.dot(normal);
589 if signed_depth <= 0.0 {
590 return None;
591 } let local = corner - ref_pos;
595 if local.dot(t0).abs() > e0 + SLAB_TOLERANCE {
596 return None;
597 }
598 if local.dot(t1).abs() > e1 + SLAB_TOLERANCE {
599 return None;
600 }
601
602 let depth = signed_depth.max(0.0);
606
607 Some(mk_contact(corner, normal, depth))
608 })
609 .collect();
610
611 select_4_contacts(contacts)
612}
613
614#[cfg(test)]
619mod tests {
620 use super::*;
621 use crate::components::BoxShape;
622
623 fn box_shape(half: f32) -> ColliderShape {
624 ColliderShape::Box(BoxShape {
625 half_extents: Vec3::splat(half),
626 })
627 }
628
629 #[test]
637 fn box_box_ref_b_penetration_not_inflated() {
638 let diag = Vec3::new(1.0, 1.0, 1.0).normalize();
642 let rot_a = Quat::from_axis_angle(
643 Vec3::new(0.0, 1.0, -1.0).normalize(),
644 diag.dot(Vec3::X).acos(),
645 );
646 let pos_a = Vec3::ZERO;
647 let ha = Vec3::splat(1.0);
648 let pos_b = Vec3::new(2.5, 0.0, 0.0);
650 let rot_b = Quat::IDENTITY;
651 let hb = Vec3::splat(1.0);
652
653 let contacts = NarrowPhase::box_box(pos_a, rot_a, ha, pos_b, rot_b, hb);
654 assert!(!contacts.is_empty(), "overlapping boxes must produce contacts");
655
656 let n = contacts[0].normal;
657 assert!(
658 n.x > 0.99,
659 "expected the +X contact normal that forces the ref=B path, got {n:?}"
660 );
661
662 let extent = |rot: Quat, h: Vec3| {
664 let a = [rot.mul_vec3(Vec3::X), rot.mul_vec3(Vec3::Y), rot.mul_vec3(Vec3::Z)];
665 a[0].dot(n).abs() * h.x + a[1].dot(n).abs() * h.y + a[2].dot(n).abs() * h.z
666 };
667 let overlap = (pos_a.dot(n) + extent(rot_a, ha)) - (pos_b.dot(n) - extent(rot_b, hb));
668 assert!(overlap > 0.0, "boxes must actually overlap along the normal");
669
670 let max_pen = contacts
671 .iter()
672 .map(|c| c.penetration)
673 .fold(0.0_f32, f32::max);
674 assert!(
675 max_pen <= overlap + 1e-3,
676 "penetration {max_pen} exceeds the SAT overlap {overlap} along the normal \
677 → inflated depth (the ref=B unflipped-normal bug)"
678 );
679 }
680
681 #[test]
684 fn sphere_sphere_overlap_produces_contact() {
685 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(1.5, 0., 0.), 1.0);
686 assert!(c.is_some(), "overlapping spheres must collide");
687 let c = c.unwrap();
688 assert!(c.penetration > 0.0, "penetration must be positive");
689 assert!(
690 (c.normal.x - 1.0).abs() < 0.01,
691 "normal must point A→B (+X)"
692 );
693 }
694
695 #[test]
696 fn sphere_sphere_separated_returns_none() {
697 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(3.0, 0., 0.), 1.0);
698 assert!(c.is_none(), "separated spheres must not collide");
699 }
700
701 #[test]
702 fn sphere_sphere_touching_returns_none() {
703 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(2.0, 0., 0.), 1.0);
705 assert!(
706 c.is_none(),
707 "just-touching spheres should not produce contact"
708 );
709 }
710
711 #[test]
714 fn sphere_plane_below_produces_contact() {
715 let c = NarrowPhase::sphere_plane(Vec3::new(0., 0.5, 0.), 1.0, Vec3::Y, 0.0);
718 assert!(c.is_some());
719 let c = c.unwrap();
720 assert!(c.penetration > 0.0);
721 assert!((c.normal.y + 1.0).abs() < 0.01, "normal should be -Y");
723 }
724
725 #[test]
726 fn sphere_plane_above_returns_none() {
727 let c = NarrowPhase::sphere_plane(Vec3::new(0., 2.0, 0.), 1.0, Vec3::Y, 0.0);
728 assert!(c.is_none());
729 }
730
731 #[test]
734 fn box_plane_four_contacts_when_flat_on_ground() {
735 let contacts = NarrowPhase::box_plane(
738 Vec3::new(0., 0.5, 0.),
739 Quat::IDENTITY,
740 Vec3::splat(1.0),
741 Vec3::Y,
742 0.0,
743 );
744 assert_eq!(contacts.len(), 4, "flat box should have 4 contacts");
745 for c in &contacts {
746 assert!(c.penetration > 0.0, "each contact must penetrate");
747 assert!(
748 (c.normal.y + 1.0).abs() < 0.01,
749 "normal must be -Y (box→plane)"
750 );
751 }
752 }
753
754 #[test]
755 fn box_plane_no_contact_when_above() {
756 let contacts = NarrowPhase::box_plane(
757 Vec3::new(0., 2.0, 0.),
758 Quat::IDENTITY,
759 Vec3::splat(1.0),
760 Vec3::Y,
761 0.0,
762 );
763 assert!(contacts.is_empty());
764 }
765
766 #[test]
769 fn box_box_overlap_produces_contacts() {
770 let contacts = NarrowPhase::box_box(
771 Vec3::ZERO,
772 Quat::IDENTITY,
773 Vec3::splat(1.0),
774 Vec3::new(1.5, 0., 0.),
775 Quat::IDENTITY,
776 Vec3::splat(1.0),
777 );
778 assert!(!contacts.is_empty(), "overlapping boxes must have contacts");
779 for c in &contacts {
780 assert!(c.penetration >= 0.0);
781 }
782 }
783
784 #[test]
785 fn box_box_separated_returns_empty() {
786 let contacts = NarrowPhase::box_box(
787 Vec3::ZERO,
788 Quat::IDENTITY,
789 Vec3::splat(1.0),
790 Vec3::new(5.0, 0., 0.),
791 Quat::IDENTITY,
792 Vec3::splat(1.0),
793 );
794 assert!(
795 contacts.is_empty(),
796 "separated boxes must not produce contacts"
797 );
798 }
799
800 #[test]
801 fn box_box_rotated_45_produces_contacts() {
802 let rot45 = Quat::from_rotation_y(std::f32::consts::FRAC_PI_4);
803 let contacts = NarrowPhase::box_box(
804 Vec3::ZERO,
805 Quat::IDENTITY,
806 Vec3::splat(0.8),
807 Vec3::new(1.0, 0., 0.),
808 rot45,
809 Vec3::splat(0.8),
810 );
811 assert!(
812 !contacts.is_empty(),
813 "rotated overlapping boxes must collide"
814 );
815 }
816
817 #[test]
818 fn box_box_face_contact_normal_is_axis_aligned() {
819 let contacts = NarrowPhase::box_box(
821 Vec3::ZERO,
822 Quat::IDENTITY,
823 Vec3::splat(1.0),
824 Vec3::new(1.5, 0., 0.),
825 Quat::IDENTITY,
826 Vec3::splat(1.0),
827 );
828 assert!(!contacts.is_empty());
829 for c in &contacts {
830 assert!(
831 c.normal.x.abs() > 0.9,
832 "face contact normal should be X-aligned, got {:?}",
833 c.normal
834 );
835 }
836 }
837
838 #[test]
839 fn box_box_contact_count_at_most_4() {
840 let contacts = NarrowPhase::box_box(
841 Vec3::ZERO,
842 Quat::IDENTITY,
843 Vec3::splat(1.0),
844 Vec3::new(1.5, 0., 0.),
845 Quat::IDENTITY,
846 Vec3::splat(1.0),
847 );
848 assert!(
849 contacts.len() <= 4,
850 "manifold must not exceed 4 contact points"
851 );
852 }
853
854 #[test]
855 fn box_box_rotated_penetration_along_normal_equals_mtv() {
856 let rot = Quat::from_rotation_y(std::f32::consts::FRAC_PI_6); let contacts = NarrowPhase::box_box(
863 Vec3::ZERO,
864 rot,
865 Vec3::splat(1.0),
866 Vec3::new(1.2, 0.0, 0.0),
867 Quat::IDENTITY,
868 Vec3::splat(1.0),
869 );
870 assert!(!contacts.is_empty(), "rotated overlapping boxes must collide");
871 let expected_mtv = 1.166_f32;
872 let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
873 for c in &contacts {
874 lo = lo.min(c.penetration);
875 hi = hi.max(c.penetration);
876 assert!(
877 (c.penetration - expected_mtv).abs() < 0.02,
878 "penetration {} must equal the true MTV {} (measured along the contact normal)",
879 c.penetration,
880 expected_mtv,
881 );
882 }
883 assert!(
885 hi - lo < 0.02,
886 "manifold depths must be uniform across a flat contact, spread was {}",
887 hi - lo
888 );
889 }
890
891 #[test]
894 fn dispatcher_box_box_finds_contact() {
895 let ba = box_shape(1.0);
896 let bb = box_shape(1.0);
897 let c = NarrowPhase::test_collision(
898 &ba,
899 Vec3::ZERO,
900 Quat::IDENTITY,
901 &bb,
902 Vec3::new(1.5, 0., 0.),
903 Quat::IDENTITY,
904 );
905 assert!(c.is_some(), "dispatcher must detect box-box overlap");
906 }
907
908 #[test]
909 fn dispatcher_manifold_populates_local_points() {
910 let ba = box_shape(1.0);
911 let bb = box_shape(1.0);
912 let contacts = NarrowPhase::test_collision_manifold(
913 &ba,
914 Vec3::ZERO,
915 Quat::IDENTITY,
916 &bb,
917 Vec3::new(1.5, 0., 0.),
918 Quat::IDENTITY,
919 );
920 assert!(!contacts.is_empty());
921 for c in &contacts {
922 let _ = c.local_point_a; let _ = c.local_point_b;
927 }
928 }
929
930 #[test]
931 fn test_collision_returns_deepest_of_manifold() {
932 let ba = box_shape(1.0);
933 let bb = box_shape(1.0);
934
935 let manifold = NarrowPhase::test_collision_manifold(
936 &ba,
937 Vec3::ZERO,
938 Quat::IDENTITY,
939 &bb,
940 Vec3::new(1.5, 0., 0.),
941 Quat::IDENTITY,
942 );
943 let single = NarrowPhase::test_collision(
944 &ba,
945 Vec3::ZERO,
946 Quat::IDENTITY,
947 &bb,
948 Vec3::new(1.5, 0., 0.),
949 Quat::IDENTITY,
950 );
951
952 if let (Some(s), Some(deepest)) = (
953 single,
954 manifold
955 .iter()
956 .max_by(|a, b| a.penetration.total_cmp(&b.penetration)),
957 ) {
958 assert!(
959 (s.penetration - deepest.penetration).abs() < 1e-5,
960 "test_collision must return the deepest manifold contact"
961 );
962 }
963 }
964
965 #[test]
968 fn sphere_sphere_normal_points_a_to_b() {
969 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(1.5, 0., 0.), 1.0).unwrap();
970 assert!(
972 c.normal.dot(Vec3::new(1.5, 0., 0.)) > 0.0,
973 "normal must point from A toward B"
974 );
975 }
976
977 #[test]
978 fn box_box_normal_points_a_to_b() {
979 let contacts = NarrowPhase::box_box(
980 Vec3::ZERO,
981 Quat::IDENTITY,
982 Vec3::splat(1.0),
983 Vec3::new(1.5, 0., 0.),
984 Quat::IDENTITY,
985 Vec3::splat(1.0),
986 );
987 let d = Vec3::new(1.5, 0., 0.); for c in &contacts {
989 assert!(
990 c.normal.dot(d) > 0.0,
991 "box-box normal must point from A toward B, got {:?}",
992 c.normal
993 );
994 }
995 }
996}