1use oxiphysics_core::math::{Real, Vec3};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct CollisionPair {
17 pub a: usize,
19 pub b: usize,
21}
22
23impl CollisionPair {
24 pub fn new(a: usize, b: usize) -> Self {
26 Self { a, b }
27 }
28
29 pub fn canonical(a: usize, b: usize) -> Self {
31 if a <= b {
32 Self { a, b }
33 } else {
34 Self { a: b, b: a }
35 }
36 }
37
38 pub fn contains(&self, idx: usize) -> bool {
40 self.a == idx || self.b == idx
41 }
42
43 pub fn other(&self, idx: usize) -> usize {
45 if self.a == idx {
46 self.b
47 } else if self.b == idx {
48 self.a
49 } else {
50 panic!(
51 "CollisionPair::other: index {idx} is not in pair ({}, {})",
52 self.a, self.b
53 )
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum FeatureId {
66 Vertex(u32),
68 Edge(u32),
70 Face(u32),
72 Unknown,
74}
75
76impl FeatureId {
77 pub fn is_vertex(self) -> bool {
79 matches!(self, FeatureId::Vertex(_))
80 }
81
82 pub fn is_edge(self) -> bool {
84 matches!(self, FeatureId::Edge(_))
85 }
86
87 pub fn is_face(self) -> bool {
89 matches!(self, FeatureId::Face(_))
90 }
91
92 pub fn index(self) -> Option<u32> {
94 match self {
95 FeatureId::Vertex(i) | FeatureId::Edge(i) | FeatureId::Face(i) => Some(i),
96 FeatureId::Unknown => None,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
105pub struct Contact {
106 pub point_a: Vec3,
108 pub point_b: Vec3,
110 pub normal: Vec3,
112 pub depth: Real,
114}
115
116impl Contact {
117 pub fn new(point_a: Vec3, point_b: Vec3, normal: Vec3, depth: Real) -> Self {
119 Self {
120 point_a,
121 point_b,
122 normal,
123 depth,
124 }
125 }
126
127 pub fn point(&self) -> Vec3 {
129 (self.point_a + self.point_b) * 0.5
130 }
131}
132
133#[derive(Debug, Clone)]
138pub struct RichContact {
139 pub point_a: Vec3,
141 pub point_b: Vec3,
143 pub normal: Vec3,
145 pub depth: Real,
147 pub feature_a: FeatureId,
149 pub feature_b: FeatureId,
151 pub impulse_n: Real,
153 pub impulse_t1: Real,
155 pub impulse_t2: Real,
157}
158
159impl RichContact {
160 pub fn new(point_a: Vec3, point_b: Vec3, normal: Vec3, depth: Real) -> Self {
162 Self {
163 point_a,
164 point_b,
165 normal,
166 depth,
167 feature_a: FeatureId::Unknown,
168 feature_b: FeatureId::Unknown,
169 impulse_n: 0.0,
170 impulse_t1: 0.0,
171 impulse_t2: 0.0,
172 }
173 }
174
175 pub fn from_contact(c: &Contact) -> Self {
177 Self::new(c.point_a, c.point_b, c.normal, c.depth)
178 }
179
180 pub fn to_contact(&self) -> Contact {
182 Contact {
183 point_a: self.point_a,
184 point_b: self.point_b,
185 normal: self.normal,
186 depth: self.depth,
187 }
188 }
189
190 pub fn point(&self) -> Vec3 {
192 (self.point_a + self.point_b) * 0.5
193 }
194
195 pub fn tangent_basis(&self) -> (Vec3, Vec3) {
199 let n = self.normal;
200 let ref_vec = if n.x.abs() < 0.9 {
202 Vec3::new(1.0, 0.0, 0.0)
203 } else {
204 Vec3::new(0.0, 1.0, 0.0)
205 };
206 let t1 = n.cross(&ref_vec).normalize();
207 let t2 = n.cross(&t1);
208 (t1, t2)
209 }
210
211 pub fn is_penetrating(&self) -> bool {
213 self.depth > 0.0
214 }
215}
216
217pub const MAX_CONTACTS: usize = 4;
221
222#[derive(Debug, Clone)]
224pub struct ContactManifold {
225 pub pair: CollisionPair,
227 pub contacts: Vec<Contact>,
229 pub age: u32,
231}
232
233impl ContactManifold {
234 pub fn new(pair: CollisionPair) -> Self {
236 Self {
237 pair,
238 contacts: Vec::new(),
239 age: 0,
240 }
241 }
242
243 pub fn add_contact(&mut self, contact: Contact) {
248 if self.contacts.len() < MAX_CONTACTS {
249 self.contacts.push(contact);
250 } else {
251 if let Some(min_idx) = self
253 .contacts
254 .iter()
255 .enumerate()
256 .min_by(|(_, a), (_, b)| {
257 a.depth
258 .partial_cmp(&b.depth)
259 .unwrap_or(std::cmp::Ordering::Equal)
260 })
261 .map(|(i, _)| i)
262 && contact.depth > self.contacts[min_idx].depth
263 {
264 self.contacts[min_idx] = contact;
265 }
266 }
267 }
268
269 pub fn is_empty(&self) -> bool {
271 self.contacts.is_empty()
272 }
273
274 pub fn len(&self) -> usize {
276 self.contacts.len()
277 }
278
279 pub fn max_depth(&self) -> Real {
281 self.contacts
282 .iter()
283 .map(|c| c.depth)
284 .fold(f64::NEG_INFINITY, f64::max)
285 }
286
287 pub fn average_normal(&self) -> Vec3 {
289 if self.contacts.is_empty() {
290 return Vec3::zeros();
291 }
292 let sum: Vec3 = self
293 .contacts
294 .iter()
295 .map(|c| c.normal)
296 .fold(Vec3::zeros(), |a, b| a + b);
297 let n = sum / (self.contacts.len() as Real);
298 let norm = n.norm();
299 if norm > 1e-12 {
300 n / norm
301 } else {
302 Vec3::zeros()
303 }
304 }
305
306 pub fn prune_shallow(&mut self, threshold: Real) {
308 self.contacts.retain(|c| c.depth >= threshold);
309 }
310
311 pub fn tick(&mut self) {
313 self.age = self.age.saturating_add(1);
314 }
315}
316
317#[derive(Debug, Clone)]
322pub struct RichContactManifold {
323 pub pair: CollisionPair,
325 pub contacts: Vec<RichContact>,
327 pub age: u32,
329}
330
331impl RichContactManifold {
332 pub fn new(pair: CollisionPair) -> Self {
334 Self {
335 pair,
336 contacts: Vec::new(),
337 age: 0,
338 }
339 }
340
341 pub fn add_contact(&mut self, contact: RichContact) {
343 if self.contacts.len() < MAX_CONTACTS {
344 self.contacts.push(contact);
345 } else {
346 if let Some(min_idx) = self
347 .contacts
348 .iter()
349 .enumerate()
350 .min_by(|(_, a), (_, b)| {
351 a.depth
352 .partial_cmp(&b.depth)
353 .unwrap_or(std::cmp::Ordering::Equal)
354 })
355 .map(|(i, _)| i)
356 && contact.depth > self.contacts[min_idx].depth
357 {
358 self.contacts[min_idx] = contact;
359 }
360 }
361 }
362
363 pub fn warm_start_from(&mut self, old: &RichContactManifold) {
365 for c in &mut self.contacts {
366 if let Some(prev) = old
367 .contacts
368 .iter()
369 .find(|p| p.feature_a == c.feature_a && p.feature_b == c.feature_b)
370 {
371 c.impulse_n = prev.impulse_n;
372 c.impulse_t1 = prev.impulse_t1;
373 c.impulse_t2 = prev.impulse_t2;
374 }
375 }
376 }
377
378 pub fn is_empty(&self) -> bool {
380 self.contacts.is_empty()
381 }
382
383 pub fn len(&self) -> usize {
385 self.contacts.len()
386 }
387
388 pub fn max_depth(&self) -> Real {
390 self.contacts
391 .iter()
392 .map(|c| c.depth)
393 .fold(f64::NEG_INFINITY, f64::max)
394 }
395
396 pub fn tick(&mut self) {
398 self.age = self.age.saturating_add(1);
399 }
400}
401
402#[derive(Debug, Clone, Copy)]
406pub struct PhysicsMaterial {
407 pub restitution: Real,
409 pub friction_static: Real,
411 pub friction_dynamic: Real,
413}
414
415impl Default for PhysicsMaterial {
416 fn default() -> Self {
417 Self {
418 restitution: 0.3,
419 friction_static: 0.6,
420 friction_dynamic: 0.4,
421 }
422 }
423}
424
425impl PhysicsMaterial {
426 pub fn new(restitution: Real, friction_static: Real, friction_dynamic: Real) -> Self {
428 Self {
429 restitution,
430 friction_static,
431 friction_dynamic,
432 }
433 }
434
435 pub fn combine(a: &Self, b: &Self) -> Self {
438 Self {
439 restitution: a.restitution.min(b.restitution),
440 friction_static: (a.friction_static * b.friction_static).sqrt(),
441 friction_dynamic: (a.friction_dynamic * b.friction_dynamic).sqrt(),
442 }
443 }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
453pub struct CollisionFilter {
454 pub category: u32,
456 pub mask: u32,
458}
459
460impl Default for CollisionFilter {
461 fn default() -> Self {
462 Self {
463 category: 0xFFFF_FFFF,
464 mask: 0xFFFF_FFFF,
465 }
466 }
467}
468
469impl CollisionFilter {
470 pub fn new(category: u32, mask: u32) -> Self {
472 Self { category, mask }
473 }
474
475 pub fn should_collide(&self, other: &CollisionFilter) -> bool {
477 (self.mask & other.category) != 0 && (other.mask & self.category) != 0
478 }
479
480 pub fn all() -> Self {
482 Self::default()
483 }
484
485 pub fn none() -> Self {
487 Self {
488 category: 0,
489 mask: 0,
490 }
491 }
492}
493
494#[derive(Debug, Clone)]
498pub enum CollisionEvent {
499 ContactStarted(CollisionPair),
501 ContactEnded(CollisionPair),
503 TriggerEntered {
505 sensor: usize,
507 body: usize,
509 },
510 TriggerExited {
512 sensor: usize,
514 body: usize,
516 },
517}
518
519impl CollisionEvent {
520 pub fn pair(&self) -> (usize, usize) {
522 match self {
523 CollisionEvent::ContactStarted(p) | CollisionEvent::ContactEnded(p) => (p.a, p.b),
524 CollisionEvent::TriggerEntered { sensor, body }
525 | CollisionEvent::TriggerExited { sensor, body } => (*sensor, *body),
526 }
527 }
528
529 pub fn is_enter(&self) -> bool {
531 matches!(
532 self,
533 CollisionEvent::ContactStarted(_) | CollisionEvent::TriggerEntered { .. }
534 )
535 }
536
537 pub fn is_exit(&self) -> bool {
539 !self.is_enter()
540 }
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
551pub struct IslandId(pub u32);
552
553impl IslandId {
554 pub const NONE: Self = IslandId(u32::MAX);
556
557 pub fn is_none(self) -> bool {
559 self == Self::NONE
560 }
561
562 pub fn is_some(self) -> bool {
564 self != Self::NONE
565 }
566
567 pub fn index(self) -> usize {
569 self.0 as usize
570 }
571}
572
573impl Default for IslandId {
574 fn default() -> Self {
575 Self::NONE
576 }
577}
578
579impl std::fmt::Display for IslandId {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 if self.is_none() {
582 write!(f, "IslandId::NONE")
583 } else {
584 write!(f, "IslandId({})", self.0)
585 }
586 }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
593pub enum RigidBodyKind {
594 Dynamic,
596 Static,
598 Kinematic,
600 Sensor,
602}
603
604impl RigidBodyKind {
605 pub fn is_movable(self) -> bool {
607 matches!(self, RigidBodyKind::Dynamic | RigidBodyKind::Kinematic)
608 }
609
610 pub fn generates_contacts(self) -> bool {
612 !matches!(self, RigidBodyKind::Sensor)
613 }
614
615 pub fn is_island_member(self) -> bool {
617 matches!(self, RigidBodyKind::Dynamic)
618 }
619}
620
621#[derive(Debug, Clone)]
628pub struct BodyGroup {
629 pub island: IslandId,
631 pub bodies: Vec<usize>,
633 pub all_sleeping: bool,
635}
636
637impl BodyGroup {
638 pub fn new(island: IslandId, bodies: Vec<usize>) -> Self {
640 Self {
641 island,
642 bodies,
643 all_sleeping: false,
644 }
645 }
646
647 pub fn len(&self) -> usize {
649 self.bodies.len()
650 }
651
652 pub fn is_empty(&self) -> bool {
654 self.bodies.is_empty()
655 }
656
657 pub fn contains(&self, body_idx: usize) -> bool {
659 self.bodies.contains(&body_idx)
660 }
661
662 pub fn set_sleeping(&mut self, sleeping: bool) {
664 self.all_sleeping = sleeping;
665 }
666}
667
668#[derive(Debug, Clone)]
674pub struct ContactConstraint {
675 pub pair: CollisionPair,
677 pub normal: Vec3,
679 pub depth: Real,
681 pub point: Vec3,
683 pub impulse_n: Real,
685 pub impulse_t1: Real,
687 pub impulse_t2: Real,
689 pub restitution: Real,
691 pub friction: Real,
693}
694
695impl ContactConstraint {
696 pub fn new(pair: CollisionPair, normal: Vec3, depth: Real, point: Vec3) -> Self {
698 Self {
699 pair,
700 normal,
701 depth,
702 point,
703 impulse_n: 0.0,
704 impulse_t1: 0.0,
705 impulse_t2: 0.0,
706 restitution: 0.3,
707 friction: 0.5,
708 }
709 }
710
711 pub fn tangent_basis(&self) -> (Vec3, Vec3) {
713 let n = self.normal;
714 let ref_vec = if n.x.abs() < 0.9 {
715 Vec3::new(1.0, 0.0, 0.0)
716 } else {
717 Vec3::new(0.0, 1.0, 0.0)
718 };
719 let t1 = n.cross(&ref_vec).normalize();
720 let t2 = n.cross(&t1);
721 (t1, t2)
722 }
723
724 pub fn is_penetrating(&self) -> bool {
726 self.depth > 0.0
727 }
728
729 pub fn clear_impulses(&mut self) {
731 self.impulse_n = 0.0;
732 self.impulse_t1 = 0.0;
733 self.impulse_t2 = 0.0;
734 }
735}
736
737#[derive(Debug, Clone)]
743pub struct ContactVelocityConstraint {
744 pub pair: CollisionPair,
746 pub normal: Vec3,
748 pub velocity_bias: Real,
750 pub effective_mass_n: Real,
752 pub effective_mass_t1: Real,
754 pub effective_mass_t2: Real,
756 pub friction: Real,
758 pub impulse_n: Real,
760 pub impulse_t1: Real,
762 pub impulse_t2: Real,
764}
765
766impl ContactVelocityConstraint {
767 pub fn new(pair: CollisionPair, normal: Vec3) -> Self {
769 Self {
770 pair,
771 normal,
772 velocity_bias: 0.0,
773 effective_mass_n: 1.0,
774 effective_mass_t1: 1.0,
775 effective_mass_t2: 1.0,
776 friction: 0.5,
777 impulse_n: 0.0,
778 impulse_t1: 0.0,
779 impulse_t2: 0.0,
780 }
781 }
782
783 pub fn has_impulse(&self) -> bool {
785 self.impulse_n.abs() > 1e-12
786 || self.impulse_t1.abs() > 1e-12
787 || self.impulse_t2.abs() > 1e-12
788 }
789
790 pub fn impulse_magnitude(&self) -> Real {
792 (self.impulse_n * self.impulse_n
793 + self.impulse_t1 * self.impulse_t1
794 + self.impulse_t2 * self.impulse_t2)
795 .sqrt()
796 }
797
798 pub fn warm_start_from(&mut self, prev: &ContactVelocityConstraint) {
800 self.impulse_n = prev.impulse_n;
801 self.impulse_t1 = prev.impulse_t1;
802 self.impulse_t2 = prev.impulse_t2;
803 }
804}
805
806#[derive(Debug, Default)]
810pub struct ContactBatch {
811 pub constraints: Vec<ContactVelocityConstraint>,
813 pub island: IslandId,
815}
816
817impl ContactBatch {
818 pub fn new(island: IslandId) -> Self {
820 Self {
821 constraints: Vec::new(),
822 island,
823 }
824 }
825
826 pub fn push(&mut self, c: ContactVelocityConstraint) {
828 self.constraints.push(c);
829 }
830
831 pub fn len(&self) -> usize {
833 self.constraints.len()
834 }
835
836 pub fn is_empty(&self) -> bool {
838 self.constraints.is_empty()
839 }
840
841 pub fn total_normal_impulse(&self) -> Real {
843 self.constraints.iter().map(|c| c.impulse_n).sum()
844 }
845
846 pub fn clear_impulses(&mut self) {
848 for c in &mut self.constraints {
849 c.impulse_n = 0.0;
850 c.impulse_t1 = 0.0;
851 c.impulse_t2 = 0.0;
852 }
853 }
854}
855
856#[derive(Debug, Clone)]
863pub struct CollisionMatrix {
864 pub(super) rows: [u32; 32],
865}
866
867impl Default for CollisionMatrix {
868 fn default() -> Self {
869 Self::all_collide()
870 }
871}
872
873impl CollisionMatrix {
874 pub fn all_collide() -> Self {
876 Self {
877 rows: [u32::MAX; 32],
878 }
879 }
880
881 pub fn none_collide() -> Self {
883 Self { rows: [0u32; 32] }
884 }
885
886 pub fn enable(&mut self, a: u8, b: u8) {
888 let a = (a & 31) as usize;
889 let b = (b & 31) as usize;
890 self.rows[a] |= 1 << b;
891 self.rows[b] |= 1 << a;
892 }
893
894 pub fn disable(&mut self, a: u8, b: u8) {
896 let a = (a & 31) as usize;
897 let b = (b & 31) as usize;
898 self.rows[a] &= !(1 << b);
899 self.rows[b] &= !(1 << a);
900 }
901
902 pub fn should_collide(&self, a: u8, b: u8) -> bool {
904 let a = (a & 31) as usize;
905 let b = (b & 31) as usize;
906 (self.rows[a] >> b) & 1 == 1
907 }
908}
909
910#[derive(Debug, Default, Clone, Copy)]
914pub struct ContactStats {
915 pub broadphase_pairs: u32,
917 pub narrowphase_pairs: u32,
919 pub contacts_generated: u32,
921 pub contacts_expired: u32,
923 pub islands: u32,
925}
926
927impl ContactStats {
928 pub fn new() -> Self {
930 Self::default()
931 }
932
933 pub fn narrowphase_ratio(&self) -> f64 {
937 if self.broadphase_pairs == 0 {
938 return 0.0;
939 }
940 self.narrowphase_pairs as f64 / self.broadphase_pairs as f64
941 }
942
943 pub fn reset(&mut self) {
945 *self = Self::default();
946 }
947}
948
949#[cfg(test)]
952mod tests {
953 use super::*;
954
955 #[test]
956 fn test_collision_pair_basic() {
957 let pair = CollisionPair::new(0, 1);
958 assert_eq!(pair.a, 0);
959 assert_eq!(pair.b, 1);
960 }
961
962 #[test]
963 fn test_collision_pair_canonical() {
964 let p1 = CollisionPair::canonical(3, 1);
965 assert!(p1.a <= p1.b);
966 let p2 = CollisionPair::canonical(1, 3);
967 assert_eq!(p1, p2);
968 }
969
970 #[test]
971 fn test_collision_pair_contains() {
972 let p = CollisionPair::new(2, 7);
973 assert!(p.contains(2));
974 assert!(p.contains(7));
975 assert!(!p.contains(5));
976 }
977
978 #[test]
979 fn test_collision_pair_other() {
980 let p = CollisionPair::new(2, 7);
981 assert_eq!(p.other(2), 7);
982 assert_eq!(p.other(7), 2);
983 }
984
985 #[test]
986 #[should_panic]
987 fn test_collision_pair_other_panics() {
988 let p = CollisionPair::new(2, 7);
989 let _ = p.other(99);
990 }
991
992 #[test]
993 fn test_feature_id() {
994 assert!(FeatureId::Vertex(0).is_vertex());
995 assert!(FeatureId::Edge(2).is_edge());
996 assert!(FeatureId::Face(5).is_face());
997 assert_eq!(FeatureId::Face(5).index(), Some(5));
998 assert_eq!(FeatureId::Unknown.index(), None);
999 }
1000
1001 #[test]
1002 fn test_contact_manifold_basic() {
1003 let pair = CollisionPair::new(0, 1);
1004 let mut manifold = ContactManifold::new(pair);
1005 manifold.add_contact(Contact {
1006 point_a: Vec3::zeros(),
1007 point_b: Vec3::new(0.0, -0.1, 0.0),
1008 normal: Vec3::new(0.0, 1.0, 0.0),
1009 depth: 0.1,
1010 });
1011 assert_eq!(manifold.contacts.len(), 1);
1012 }
1013
1014 fn make_contact(depth: f64) -> Contact {
1015 Contact {
1016 point_a: Vec3::zeros(),
1017 point_b: Vec3::zeros(),
1018 normal: Vec3::new(0.0, 1.0, 0.0),
1019 depth,
1020 }
1021 }
1022
1023 #[test]
1024 fn test_manifold_max_contacts_cap() {
1025 let pair = CollisionPair::new(0, 1);
1026 let mut manifold = ContactManifold::new(pair);
1027 for i in 0..8 {
1028 manifold.add_contact(make_contact(i as f64 * 0.01));
1029 }
1030 assert!(manifold.contacts.len() <= MAX_CONTACTS);
1031 }
1032
1033 #[test]
1034 fn test_manifold_max_depth() {
1035 let pair = CollisionPair::new(0, 1);
1036 let mut m = ContactManifold::new(pair);
1037 m.add_contact(make_contact(0.1));
1038 m.add_contact(make_contact(0.5));
1039 m.add_contact(make_contact(0.3));
1040 assert!((m.max_depth() - 0.5).abs() < 1e-12);
1041 }
1042
1043 #[test]
1044 fn test_manifold_prune_shallow() {
1045 let pair = CollisionPair::new(0, 1);
1046 let mut m = ContactManifold::new(pair);
1047 m.add_contact(make_contact(0.1));
1048 m.add_contact(make_contact(-0.05));
1049 m.prune_shallow(0.0);
1050 assert_eq!(m.contacts.len(), 1);
1051 assert!((m.contacts[0].depth - 0.1).abs() < 1e-12);
1052 }
1053
1054 #[test]
1055 fn test_manifold_average_normal() {
1056 let pair = CollisionPair::new(0, 1);
1057 let mut m = ContactManifold::new(pair);
1058 m.add_contact(make_contact(0.1));
1059 m.add_contact(make_contact(0.2));
1060 let avg = m.average_normal();
1061 assert!((avg.norm() - 1.0).abs() < 1e-10);
1062 }
1063
1064 #[test]
1065 fn test_manifold_empty_average_normal() {
1066 let m = ContactManifold::new(CollisionPair::new(0, 1));
1067 let avg = m.average_normal();
1068 assert_eq!(avg.norm(), 0.0);
1069 }
1070
1071 #[test]
1072 fn test_rich_contact_tangent_basis_orthogonal() {
1073 let c = RichContact::new(Vec3::zeros(), Vec3::zeros(), Vec3::new(0.0, 1.0, 0.0), 0.1);
1074 let (t1, t2) = c.tangent_basis();
1075 assert!(c.normal.dot(&t1).abs() < 1e-10, "t1 not perp to normal");
1076 assert!(c.normal.dot(&t2).abs() < 1e-10, "t2 not perp to normal");
1077 assert!(t1.dot(&t2).abs() < 1e-10, "t1 not perp to t2");
1078 }
1079
1080 #[test]
1081 fn test_physics_material_combine() {
1082 let a = PhysicsMaterial::new(0.8, 0.4, 0.3);
1083 let b = PhysicsMaterial::new(0.5, 0.9, 0.6);
1084 let c = PhysicsMaterial::combine(&a, &b);
1085 assert!((c.restitution - 0.5).abs() < 1e-12, "min restitution");
1086 let expected_fs = (0.4_f64 * 0.9).sqrt();
1087 assert!((c.friction_static - expected_fs).abs() < 1e-12);
1088 }
1089
1090 #[test]
1091 fn test_collision_filter_default_collides() {
1092 let f1 = CollisionFilter::default();
1093 let f2 = CollisionFilter::default();
1094 assert!(f1.should_collide(&f2));
1095 }
1096
1097 #[test]
1098 fn test_collision_filter_none_no_collide() {
1099 let f1 = CollisionFilter::none();
1100 let f2 = CollisionFilter::default();
1101 assert!(!f1.should_collide(&f2));
1102 }
1103
1104 #[test]
1105 fn test_collision_filter_category_mask() {
1106 let player = CollisionFilter::new(0b0001, 0b0010);
1108 let wall = CollisionFilter::new(0b0010, 0b0001);
1109 assert!(player.should_collide(&wall));
1110 let player2 = CollisionFilter::new(0b0001, 0b0010);
1112 assert!(!player.should_collide(&player2)); }
1114
1115 #[test]
1116 fn test_collision_event_pair() {
1117 let ev = CollisionEvent::ContactStarted(CollisionPair::new(3, 7));
1118 assert_eq!(ev.pair(), (3, 7));
1119 assert!(ev.is_enter());
1120 assert!(!ev.is_exit());
1121 }
1122
1123 #[test]
1124 fn test_collision_event_trigger() {
1125 let ev = CollisionEvent::TriggerEntered {
1126 sensor: 10,
1127 body: 2,
1128 };
1129 assert_eq!(ev.pair(), (10, 2));
1130 assert!(ev.is_enter());
1131 let ev2 = CollisionEvent::TriggerExited {
1132 sensor: 10,
1133 body: 2,
1134 };
1135 assert!(ev2.is_exit());
1136 }
1137
1138 #[test]
1139 fn test_rich_manifold_warm_start_from() {
1140 let pair = CollisionPair::new(0, 1);
1141 let mut old = RichContactManifold::new(pair);
1142 let mut c = RichContact::new(Vec3::zeros(), Vec3::zeros(), Vec3::new(0.0, 1.0, 0.0), 0.1);
1143 c.feature_a = FeatureId::Face(0);
1144 c.feature_b = FeatureId::Face(1);
1145 c.impulse_n = 5.0;
1146 old.add_contact(c);
1147
1148 let mut new_m = RichContactManifold::new(pair);
1149 let mut c2 = RichContact::new(Vec3::zeros(), Vec3::zeros(), Vec3::new(0.0, 1.0, 0.0), 0.12);
1150 c2.feature_a = FeatureId::Face(0);
1151 c2.feature_b = FeatureId::Face(1);
1152 new_m.add_contact(c2);
1153
1154 new_m.warm_start_from(&old);
1155 assert!((new_m.contacts[0].impulse_n - 5.0).abs() < 1e-12);
1156 }
1157
1158 #[test]
1159 fn test_manifold_tick_age() {
1160 let mut m = ContactManifold::new(CollisionPair::new(0, 1));
1161 assert_eq!(m.age, 0);
1162 m.tick();
1163 m.tick();
1164 assert_eq!(m.age, 2);
1165 }
1166
1167 #[test]
1168 fn test_rich_contact_from_contact_roundtrip() {
1169 let c = Contact {
1170 point_a: Vec3::new(1.0, 0.0, 0.0),
1171 point_b: Vec3::new(2.0, 0.0, 0.0),
1172 normal: Vec3::new(0.0, 1.0, 0.0),
1173 depth: 0.3,
1174 };
1175 let rc = RichContact::from_contact(&c);
1176 assert_eq!(rc.depth, 0.3);
1177 assert_eq!(rc.feature_a, FeatureId::Unknown);
1178 let back = rc.to_contact();
1179 assert!((back.depth - 0.3).abs() < 1e-12);
1180 }
1181
1182 #[test]
1183 fn test_rich_contact_is_penetrating() {
1184 let c_pen = RichContact::new(Vec3::zeros(), Vec3::zeros(), Vec3::new(0.0, 1.0, 0.0), 0.1);
1185 let c_sep = RichContact::new(Vec3::zeros(), Vec3::zeros(), Vec3::new(0.0, 1.0, 0.0), -0.1);
1186 assert!(c_pen.is_penetrating());
1187 assert!(!c_sep.is_penetrating());
1188 }
1189
1190 #[test]
1193 fn island_id_none_is_sentinel() {
1194 let id = IslandId::NONE;
1195 assert!(id.is_none());
1196 assert!(!id.is_some());
1197 }
1198
1199 #[test]
1200 fn island_id_valid() {
1201 let id = IslandId(3);
1202 assert!(!id.is_none());
1203 assert!(id.is_some());
1204 assert_eq!(id.index(), 3);
1205 }
1206
1207 #[test]
1208 fn island_id_default_is_none() {
1209 let id = IslandId::default();
1210 assert!(id.is_none());
1211 }
1212
1213 #[test]
1214 fn island_id_ordering() {
1215 assert!(IslandId(0) < IslandId(1));
1216 assert!(IslandId(0) < IslandId::NONE);
1217 }
1218
1219 #[test]
1220 fn island_id_display_none() {
1221 let s = format!("{}", IslandId::NONE);
1222 assert!(s.contains("NONE"), "display: {s}");
1223 }
1224
1225 #[test]
1226 fn island_id_display_value() {
1227 let s = format!("{}", IslandId(42));
1228 assert!(s.contains("42"), "display: {s}");
1229 }
1230
1231 #[test]
1234 fn rigid_body_kind_movable() {
1235 assert!(RigidBodyKind::Dynamic.is_movable());
1236 assert!(RigidBodyKind::Kinematic.is_movable());
1237 assert!(!RigidBodyKind::Static.is_movable());
1238 assert!(!RigidBodyKind::Sensor.is_movable());
1239 }
1240
1241 #[test]
1242 fn rigid_body_kind_generates_contacts() {
1243 assert!(RigidBodyKind::Dynamic.generates_contacts());
1244 assert!(RigidBodyKind::Static.generates_contacts());
1245 assert!(RigidBodyKind::Kinematic.generates_contacts());
1246 assert!(!RigidBodyKind::Sensor.generates_contacts());
1247 }
1248
1249 #[test]
1250 fn rigid_body_kind_island_member() {
1251 assert!(RigidBodyKind::Dynamic.is_island_member());
1252 assert!(!RigidBodyKind::Static.is_island_member());
1253 assert!(!RigidBodyKind::Kinematic.is_island_member());
1254 assert!(!RigidBodyKind::Sensor.is_island_member());
1255 }
1256
1257 #[test]
1260 fn body_group_basic() {
1261 let g = BodyGroup::new(IslandId(0), vec![1, 2, 3]);
1262 assert_eq!(g.len(), 3);
1263 assert!(g.contains(2));
1264 assert!(!g.contains(99));
1265 assert!(!g.is_empty());
1266 }
1267
1268 #[test]
1269 fn body_group_empty() {
1270 let g = BodyGroup::new(IslandId(0), vec![]);
1271 assert!(g.is_empty());
1272 assert_eq!(g.len(), 0);
1273 }
1274
1275 #[test]
1276 fn body_group_set_sleeping() {
1277 let mut g = BodyGroup::new(IslandId(0), vec![1]);
1278 assert!(!g.all_sleeping);
1279 g.set_sleeping(true);
1280 assert!(g.all_sleeping);
1281 g.set_sleeping(false);
1282 assert!(!g.all_sleeping);
1283 }
1284
1285 #[test]
1288 fn contact_constraint_new() {
1289 let pair = CollisionPair::new(0, 1);
1290 let c = ContactConstraint::new(pair, Vec3::new(0.0, 1.0, 0.0), 0.05, Vec3::zeros());
1291 assert!(c.is_penetrating());
1292 assert_eq!(c.impulse_n, 0.0);
1293 assert_eq!(c.impulse_t1, 0.0);
1294 assert_eq!(c.impulse_t2, 0.0);
1295 }
1296
1297 #[test]
1298 fn contact_constraint_not_penetrating() {
1299 let pair = CollisionPair::new(0, 1);
1300 let c = ContactConstraint::new(pair, Vec3::new(0.0, 1.0, 0.0), -0.01, Vec3::zeros());
1301 assert!(!c.is_penetrating());
1302 }
1303
1304 #[test]
1305 fn contact_constraint_tangent_basis_orthogonal() {
1306 let pair = CollisionPair::new(0, 1);
1307 let c = ContactConstraint::new(pair, Vec3::new(0.0, 1.0, 0.0), 0.1, Vec3::zeros());
1308 let (t1, t2) = c.tangent_basis();
1309 assert!(c.normal.dot(&t1).abs() < 1e-10, "t1 not perp to normal");
1310 assert!(c.normal.dot(&t2).abs() < 1e-10, "t2 not perp to normal");
1311 assert!(t1.dot(&t2).abs() < 1e-10, "t1 not perp to t2");
1312 }
1313
1314 #[test]
1315 fn contact_constraint_clear_impulses() {
1316 let pair = CollisionPair::new(0, 1);
1317 let mut c = ContactConstraint::new(pair, Vec3::new(0.0, 1.0, 0.0), 0.1, Vec3::zeros());
1318 c.impulse_n = 5.0;
1319 c.impulse_t1 = 2.0;
1320 c.impulse_t2 = -1.0;
1321 c.clear_impulses();
1322 assert_eq!(c.impulse_n, 0.0);
1323 assert_eq!(c.impulse_t1, 0.0);
1324 assert_eq!(c.impulse_t2, 0.0);
1325 }
1326
1327 #[test]
1330 fn velocity_constraint_new_zeroed() {
1331 let c = ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1332 assert!(!c.has_impulse());
1333 assert_eq!(c.impulse_magnitude(), 0.0);
1334 }
1335
1336 #[test]
1337 fn velocity_constraint_has_impulse_after_set() {
1338 let mut c =
1339 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1340 c.impulse_n = 3.0;
1341 assert!(c.has_impulse());
1342 }
1343
1344 #[test]
1345 fn velocity_constraint_impulse_magnitude() {
1346 let mut c =
1347 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1348 c.impulse_n = 3.0;
1349 c.impulse_t1 = 4.0;
1350 let mag = c.impulse_magnitude();
1351 assert!((mag - 5.0).abs() < 1e-10, "magnitude={mag}");
1352 }
1353
1354 #[test]
1355 fn velocity_constraint_warm_start_from() {
1356 let mut prev =
1357 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1358 prev.impulse_n = 7.0;
1359 prev.impulse_t1 = -2.0;
1360 prev.impulse_t2 = 1.5;
1361
1362 let mut curr =
1363 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1364 curr.warm_start_from(&prev);
1365 assert!((curr.impulse_n - 7.0).abs() < 1e-12);
1366 assert!((curr.impulse_t1 - (-2.0)).abs() < 1e-12);
1367 assert!((curr.impulse_t2 - 1.5).abs() < 1e-12);
1368 }
1369
1370 #[test]
1373 fn contact_batch_empty() {
1374 let batch = ContactBatch::new(IslandId(0));
1375 assert!(batch.is_empty());
1376 assert_eq!(batch.len(), 0);
1377 assert_eq!(batch.total_normal_impulse(), 0.0);
1378 }
1379
1380 #[test]
1381 fn contact_batch_push_and_len() {
1382 let mut batch = ContactBatch::new(IslandId(1));
1383 let c = ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1384 batch.push(c);
1385 assert_eq!(batch.len(), 1);
1386 assert!(!batch.is_empty());
1387 }
1388
1389 #[test]
1390 fn contact_batch_total_normal_impulse() {
1391 let mut batch = ContactBatch::new(IslandId(0));
1392 let mut c1 =
1393 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1394 c1.impulse_n = 3.0;
1395 let mut c2 =
1396 ContactVelocityConstraint::new(CollisionPair::new(1, 2), Vec3::new(0.0, 1.0, 0.0));
1397 c2.impulse_n = 5.0;
1398 batch.push(c1);
1399 batch.push(c2);
1400 assert!((batch.total_normal_impulse() - 8.0).abs() < 1e-12);
1401 }
1402
1403 #[test]
1404 fn contact_batch_clear_impulses() {
1405 let mut batch = ContactBatch::new(IslandId(0));
1406 let mut c =
1407 ContactVelocityConstraint::new(CollisionPair::new(0, 1), Vec3::new(0.0, 1.0, 0.0));
1408 c.impulse_n = 9.0;
1409 batch.push(c);
1410 batch.clear_impulses();
1411 assert_eq!(batch.total_normal_impulse(), 0.0);
1412 }
1413
1414 #[test]
1417 fn collision_matrix_all_collide() {
1418 let m = CollisionMatrix::all_collide();
1419 for a in 0..32u8 {
1420 for b in 0..32u8 {
1421 assert!(
1422 m.should_collide(a, b),
1423 "all_collide: ({a},{b}) should collide"
1424 );
1425 }
1426 }
1427 }
1428
1429 #[test]
1430 fn collision_matrix_none_collide() {
1431 let m = CollisionMatrix::none_collide();
1432 for a in 0..32u8 {
1433 for b in 0..32u8 {
1434 assert!(
1435 !m.should_collide(a, b),
1436 "none_collide: ({a},{b}) should not collide"
1437 );
1438 }
1439 }
1440 }
1441
1442 #[test]
1443 fn collision_matrix_enable_symmetric() {
1444 let mut m = CollisionMatrix::none_collide();
1445 m.enable(2, 5);
1446 assert!(m.should_collide(2, 5));
1447 assert!(m.should_collide(5, 2), "must be symmetric");
1448 assert!(!m.should_collide(2, 3));
1449 }
1450
1451 #[test]
1452 fn collision_matrix_disable_symmetric() {
1453 let mut m = CollisionMatrix::all_collide();
1454 m.disable(3, 7);
1455 assert!(!m.should_collide(3, 7));
1456 assert!(!m.should_collide(7, 3), "must be symmetric");
1457 assert!(m.should_collide(3, 8), "other pairs unaffected");
1458 }
1459
1460 #[test]
1461 fn collision_matrix_self_collision() {
1462 let mut m = CollisionMatrix::none_collide();
1463 m.enable(4, 4);
1464 assert!(m.should_collide(4, 4));
1465 }
1466
1467 #[test]
1470 fn contact_stats_default_zeroed() {
1471 let s = ContactStats::new();
1472 assert_eq!(s.broadphase_pairs, 0);
1473 assert_eq!(s.narrowphase_pairs, 0);
1474 assert_eq!(s.contacts_generated, 0);
1475 }
1476
1477 #[test]
1478 fn contact_stats_narrowphase_ratio_zero_broadphase() {
1479 let s = ContactStats::new();
1480 assert_eq!(s.narrowphase_ratio(), 0.0);
1481 }
1482
1483 #[test]
1484 fn contact_stats_narrowphase_ratio() {
1485 let s = ContactStats {
1486 broadphase_pairs: 100,
1487 narrowphase_pairs: 25,
1488 ..ContactStats::default()
1489 };
1490 assert!((s.narrowphase_ratio() - 0.25).abs() < 1e-12);
1491 }
1492
1493 #[test]
1494 fn contact_stats_reset() {
1495 let mut s = ContactStats {
1496 broadphase_pairs: 50,
1497 contacts_generated: 10,
1498 islands: 3,
1499 ..ContactStats::default()
1500 };
1501 s.reset();
1502 assert_eq!(s.broadphase_pairs, 0);
1503 assert_eq!(s.contacts_generated, 0);
1504 assert_eq!(s.islands, 0);
1505 }
1506}