1use std::collections::HashMap;
10
11#[inline]
14fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
15 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
16}
17
18#[inline]
19fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
21}
22
23#[inline]
24fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
25 [a[0] * s, a[1] * s, a[2] * s]
26}
27
28#[inline]
29fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
30 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
31}
32
33#[inline]
34fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
35 [
36 a[1] * b[2] - a[2] * b[1],
37 a[2] * b[0] - a[0] * b[2],
38 a[0] * b[1] - a[1] * b[0],
39 ]
40}
41
42#[inline]
43fn len_sq3(a: [f64; 3]) -> f64 {
44 dot3(a, a)
45}
46
47#[inline]
48fn len3(a: [f64; 3]) -> f64 {
49 len_sq3(a).sqrt()
50}
51
52#[inline]
53fn normalize3(a: [f64; 3]) -> [f64; 3] {
54 let l = len3(a);
55 if l > 1e-10 {
56 scale3(a, 1.0 / l)
57 } else {
58 [0.0, 0.0, 0.0]
59 }
60}
61
62#[inline]
64fn mat3_mul_vec(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
65 [
66 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
67 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
68 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
69 ]
70}
71
72#[inline]
75fn transform_point(transform: ([f64; 3], [[f64; 3]; 3]), local: [f64; 3]) -> [f64; 3] {
76 add3(transform.0, mat3_mul_vec(transform.1, local))
77}
78
79pub struct ContactPointId;
83
84impl ContactPointId {
85 pub fn compute(local_a: [f64; 3], local_b: [f64; 3]) -> u64 {
90 const QUANT: f64 = 100.0; let qa = [
92 (local_a[0] * QUANT).round() as i64,
93 (local_a[1] * QUANT).round() as i64,
94 (local_a[2] * QUANT).round() as i64,
95 ];
96 let qb = [
97 (local_b[0] * QUANT).round() as i64,
98 (local_b[1] * QUANT).round() as i64,
99 (local_b[2] * QUANT).round() as i64,
100 ];
101 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
103 for &v in qa.iter().chain(qb.iter()) {
104 for b in v.to_le_bytes() {
105 h ^= b as u64;
106 h = h.wrapping_mul(0x0000_0100_0000_01B3);
107 }
108 }
109 h
110 }
111}
112
113#[derive(Debug, Clone)]
117pub struct ContactPoint {
118 pub world_pos_a: [f64; 3],
120 pub world_pos_b: [f64; 3],
122 pub local_pos_a: [f64; 3],
124 pub local_pos_b: [f64; 3],
126 pub normal: [f64; 3],
128 pub depth: f64,
130 pub tangent_impulse: [f64; 2],
132 pub normal_impulse: f64,
134 pub lifetime: u32,
136 pub id: u64,
138}
139
140impl ContactPoint {
141 pub fn new(
143 world_pos_a: [f64; 3],
144 world_pos_b: [f64; 3],
145 local_pos_a: [f64; 3],
146 local_pos_b: [f64; 3],
147 normal: [f64; 3],
148 depth: f64,
149 ) -> Self {
150 let id = ContactPointId::compute(local_pos_a, local_pos_b);
151 Self {
152 world_pos_a,
153 world_pos_b,
154 local_pos_a,
155 local_pos_b,
156 normal,
157 depth,
158 tangent_impulse: [0.0, 0.0],
159 normal_impulse: 0.0,
160 lifetime: 0,
161 id,
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
173pub struct PersistentManifold {
174 pub body_a: u32,
176 pub body_b: u32,
178 pub points: Vec<ContactPoint>,
180 pub normal: [f64; 3],
182 pub friction: f64,
184 pub restitution: f64,
186 pub is_active: bool,
188}
189
190const MATCH_DIST_SQ: f64 = 0.01 * 0.01; const SEPARATION_THRESHOLD: f64 = 0.02; impl PersistentManifold {
198 pub fn new(body_a: u32, body_b: u32, friction: f64, restitution: f64) -> Self {
200 Self {
201 body_a,
202 body_b,
203 points: Vec::with_capacity(4),
204 normal: [0.0, 1.0, 0.0],
205 friction,
206 restitution,
207 is_active: true,
208 }
209 }
210
211 pub fn add_or_update(&mut self, mut new_point: ContactPoint) {
217 let match_idx = self.points.iter().position(|p| {
219 if p.id == new_point.id {
220 return true;
221 }
222 let d = len_sq3(sub3(p.local_pos_a, new_point.local_pos_a));
223 d < MATCH_DIST_SQ
224 });
225
226 if let Some(idx) = match_idx {
227 new_point.normal_impulse = self.points[idx].normal_impulse;
229 new_point.tangent_impulse = self.points[idx].tangent_impulse;
230 new_point.lifetime = self.points[idx].lifetime + 1;
231 self.points[idx] = new_point;
232 } else {
233 self.points.push(new_point);
234 if self.points.len() > 4 {
235 self.points = ManifoldReduction::reduce_to_4_points(&self.points);
236 }
237 }
238
239 self.recompute_normal();
240 }
241
242 pub fn remove_stale(
249 &mut self,
250 transform_a: ([f64; 3], [[f64; 3]; 3]),
251 transform_b: ([f64; 3], [[f64; 3]; 3]),
252 ) {
253 self.points.retain(|p| {
254 let wa = transform_point(transform_a, p.local_pos_a);
255 let wb = transform_point(transform_b, p.local_pos_b);
256
257 let sep = dot3(sub3(wa, wb), p.normal);
259 if sep > SEPARATION_THRESHOLD {
260 return false;
261 }
262
263 if len_sq3(sub3(wa, p.world_pos_a)) > SEPARATION_THRESHOLD * SEPARATION_THRESHOLD {
265 return false;
266 }
267
268 if len_sq3(sub3(wb, p.world_pos_b)) > SEPARATION_THRESHOLD * SEPARATION_THRESHOLD {
270 return false;
271 }
272
273 true
274 });
275
276 self.recompute_normal();
277 }
278
279 fn recompute_normal(&mut self) {
280 if self.points.is_empty() {
281 return;
282 }
283 let mut avg = [0.0f64; 3];
284 for p in &self.points {
285 avg = add3(avg, p.normal);
286 }
287 let n = self.points.len() as f64;
288 self.normal = normalize3(scale3(avg, 1.0 / n));
289 }
290}
291
292pub struct ManifoldReduction;
296
297impl ManifoldReduction {
298 pub fn reduce_to_4_points(points: &[ContactPoint]) -> Vec<ContactPoint> {
306 if points.len() <= 4 {
307 return points.to_vec();
308 }
309
310 let idx0 = points
312 .iter()
313 .enumerate()
314 .max_by(|(_, a), (_, b)| {
315 a.depth
316 .partial_cmp(&b.depth)
317 .unwrap_or(std::cmp::Ordering::Equal)
318 })
319 .map(|(i, _)| i)
320 .unwrap_or(0);
321
322 let p0 = points[idx0].world_pos_a;
324 let idx1 = points
325 .iter()
326 .enumerate()
327 .filter(|(i, _)| *i != idx0)
328 .max_by(|(_, a), (_, b)| {
329 len_sq3(sub3(a.world_pos_a, p0))
330 .partial_cmp(&len_sq3(sub3(b.world_pos_a, p0)))
331 .unwrap_or(std::cmp::Ordering::Equal)
332 })
333 .map(|(i, _)| i)
334 .unwrap_or(0);
335
336 let p1 = points[idx1].world_pos_a;
338 let idx2 = points
339 .iter()
340 .enumerate()
341 .filter(|(i, _)| *i != idx0 && *i != idx1)
342 .max_by(|(_, a), (_, b)| {
343 Self::tri_area_sq(p0, p1, a.world_pos_a)
344 .partial_cmp(&Self::tri_area_sq(p0, p1, b.world_pos_a))
345 .unwrap_or(std::cmp::Ordering::Equal)
346 })
347 .map(|(i, _)| i)
348 .unwrap_or(0);
349
350 let p2 = points[idx2].world_pos_a;
352 let maybe_idx3 = points
353 .iter()
354 .enumerate()
355 .filter(|(i, _)| *i != idx0 && *i != idx1 && *i != idx2)
356 .max_by(|(_, a), (_, b)| {
357 Self::quad_area_sq(p0, p1, p2, a.world_pos_a)
358 .partial_cmp(&Self::quad_area_sq(p0, p1, p2, b.world_pos_a))
359 .unwrap_or(std::cmp::Ordering::Equal)
360 })
361 .map(|(i, _)| i);
362
363 let mut result = vec![
364 points[idx0].clone(),
365 points[idx1].clone(),
366 points[idx2].clone(),
367 ];
368 if let Some(idx3) = maybe_idx3 {
369 result.push(points[idx3].clone());
370 }
371 result
372 }
373
374 fn tri_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
376 len_sq3(cross3(sub3(b, a), sub3(c, a)))
377 }
378
379 fn quad_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> f64 {
381 Self::tri_area_sq(a, b, c) + Self::tri_area_sq(a, c, d)
382 }
383
384 pub fn contact_area(points: &[ContactPoint]) -> f64 {
389 if points.len() < 3 {
390 return 0.0;
391 }
392
393 let n = points[0].normal;
394 let t1 = Self::make_tangent(n);
395 let t2 = cross3(n, t1);
396
397 let origin = points[0].world_pos_a;
398 let projected: Vec<[f64; 2]> = points
399 .iter()
400 .map(|p| {
401 let d = sub3(p.world_pos_a, origin);
402 [dot3(d, t1), dot3(d, t2)]
403 })
404 .collect();
405
406 shoelace_area(&projected)
407 }
408
409 fn make_tangent(n: [f64; 3]) -> [f64; 3] {
410 let candidate = if n[0].abs() < 0.9 {
411 [1.0, 0.0, 0.0]
412 } else {
413 [0.0, 1.0, 0.0]
414 };
415 normalize3(cross3(n, candidate))
416 }
417}
418
419fn shoelace_area(pts: &[[f64; 2]]) -> f64 {
422 let n = pts.len();
423 if n < 3 {
424 return 0.0;
425 }
426 let mut area = 0.0f64;
427 for i in 0..n {
428 let j = (i + 1) % n;
429 area += pts[i][0] * pts[j][1];
430 area -= pts[j][0] * pts[i][1];
431 }
432 area.abs() * 0.5
433}
434
435pub struct ContactManifoldCache {
442 pub manifolds: HashMap<(u32, u32), PersistentManifold>,
444 pub max_lifetime: u32,
446}
447
448impl ContactManifoldCache {
449 pub fn new(max_lifetime: u32) -> Self {
451 Self {
452 manifolds: HashMap::new(),
453 max_lifetime,
454 }
455 }
456
457 #[inline]
459 fn key(body_a: u32, body_b: u32) -> (u32, u32) {
460 (body_a.min(body_b), body_a.max(body_b))
461 }
462
463 pub fn get_or_create(
465 &mut self,
466 body_a: u32,
467 body_b: u32,
468 friction: f64,
469 restitution: f64,
470 ) -> &mut PersistentManifold {
471 let k = Self::key(body_a, body_b);
472 self.manifolds
473 .entry(k)
474 .or_insert_with(|| PersistentManifold::new(body_a, body_b, friction, restitution))
475 }
476
477 pub fn update_manifold(&mut self, body_a: u32, body_b: u32, new_points: Vec<ContactPoint>) {
482 let k = Self::key(body_a, body_b);
483 if let Some(manifold) = self.manifolds.get_mut(&k) {
484 manifold.is_active = true;
485 for pt in new_points {
486 manifold.add_or_update(pt);
487 }
488 for p in &mut manifold.points {
489 p.lifetime = p.lifetime.saturating_add(1);
490 }
491 }
492 }
493
494 pub fn remove_inactive(&mut self) {
496 self.manifolds
497 .retain(|_, m| m.is_active && !m.points.is_empty());
498 }
499
500 pub fn begin_frame(&mut self) {
505 for m in self.manifolds.values_mut() {
506 m.is_active = false;
507 }
508 }
509}
510
511pub struct ManifoldPointMatcher;
515
516impl ManifoldPointMatcher {
517 pub fn find_match(
525 existing: &[ContactPoint],
526 new_point: &ContactPoint,
527 dist_sq_threshold: f64,
528 ) -> Option<usize> {
529 for (i, p) in existing.iter().enumerate() {
530 if p.id == new_point.id {
531 return Some(i);
532 }
533 let d = len_sq3(sub3(p.local_pos_a, new_point.local_pos_a));
534 if d < dist_sq_threshold {
535 return Some(i);
536 }
537 }
538 None
539 }
540
541 pub fn match_all(
544 existing: &[ContactPoint],
545 new_points: &[ContactPoint],
546 dist_sq_threshold: f64,
547 ) -> Vec<(usize, usize)> {
548 let mut matches = Vec::new();
549 for (ni, np) in new_points.iter().enumerate() {
550 if let Some(ei) = Self::find_match(existing, np, dist_sq_threshold) {
551 matches.push((ni, ei));
552 }
553 }
554 matches
555 }
556}
557
558pub struct ManifoldLifetimeManager {
563 pub max_inactive_frames: u32,
565}
566
567impl ManifoldLifetimeManager {
568 pub fn new(max_inactive_frames: u32) -> Self {
570 Self {
571 max_inactive_frames,
572 }
573 }
574
575 pub fn age_manifold(&self, manifold: &mut PersistentManifold) {
578 manifold
579 .points
580 .retain(|p| p.lifetime <= self.max_inactive_frames);
581 }
582
583 pub fn process_cache(&self, cache: &mut ContactManifoldCache) {
585 for manifold in cache.manifolds.values_mut() {
586 self.age_manifold(manifold);
587 }
588 cache.manifolds.retain(|_, m| !m.points.is_empty());
589 }
590
591 pub fn active_count(&self, cache: &ContactManifoldCache) -> usize {
593 cache.manifolds.values().filter(|m| m.is_active).count()
594 }
595}
596
597pub struct ManifoldCompressor;
602
603impl ManifoldCompressor {
604 pub fn compress(points: &[ContactPoint], max_count: usize) -> Vec<ContactPoint> {
610 if points.len() <= max_count {
611 return points.to_vec();
612 }
613 if max_count == 0 {
614 return vec![];
615 }
616 if max_count == 1 {
617 let idx = points
619 .iter()
620 .enumerate()
621 .max_by(|(_, a), (_, b)| {
622 a.depth
623 .partial_cmp(&b.depth)
624 .unwrap_or(std::cmp::Ordering::Equal)
625 })
626 .map(|(i, _)| i)
627 .unwrap_or(0);
628 return vec![points[idx].clone()];
629 }
630 ManifoldReduction::reduce_to_4_points(points)
632 }
633
634 pub fn deduplicate(points: &mut Vec<ContactPoint>) {
636 let mut seen = std::collections::HashSet::new();
637 points.retain(|p| seen.insert(p.id));
638 }
639
640 pub fn merge(
642 existing: &[ContactPoint],
643 incoming: &[ContactPoint],
644 max_count: usize,
645 ) -> Vec<ContactPoint> {
646 let mut merged: Vec<ContactPoint> = existing.to_vec();
647 for inc in incoming {
648 if let Some(idx) = ManifoldPointMatcher::find_match(&merged, inc, MATCH_DIST_SQ) {
649 let mut updated = inc.clone();
651 updated.normal_impulse = merged[idx].normal_impulse;
652 updated.tangent_impulse = merged[idx].tangent_impulse;
653 updated.lifetime = merged[idx].lifetime + 1;
654 merged[idx] = updated;
655 } else {
656 merged.push(inc.clone());
657 }
658 }
659 Self::compress(&merged, max_count)
660 }
661}
662
663impl PersistentManifold {
666 pub fn update_from_new_contacts(&mut self, new_contacts: Vec<ContactPoint>) {
671 let merged = ManifoldCompressor::merge(&self.points, &new_contacts, 4);
672 self.points = merged;
673 self.recompute_normal();
674 self.is_active = true;
675 }
676
677 pub fn warmstart_data(&self) -> Vec<(f64, [f64; 2])> {
679 self.points
680 .iter()
681 .map(|p| (p.normal_impulse, p.tangent_impulse))
682 .collect()
683 }
684
685 pub fn store_solver_impulses(&mut self, impulses: &[(f64, [f64; 2])]) {
687 for (p, &(ni, ti)) in self.points.iter_mut().zip(impulses.iter()) {
688 p.normal_impulse = ni;
689 p.tangent_impulse = ti;
690 }
691 }
692
693 pub fn contact_count(&self) -> usize {
695 self.points.len()
696 }
697
698 pub fn max_depth(&self) -> f64 {
700 self.points.iter().map(|p| p.depth).fold(0.0f64, f64::max)
701 }
702
703 pub fn clamp_impulses(&mut self) {
705 for p in &mut self.points {
706 p.normal_impulse = p.normal_impulse.max(0.0);
707 }
708 }
709}
710
711impl ContactManifoldCache {
714 pub fn total_contact_points(&self) -> usize {
716 self.manifolds.values().map(|m| m.points.len()).sum()
717 }
718
719 pub fn manifold_count(&self) -> usize {
721 self.manifolds.len()
722 }
723
724 pub fn clear(&mut self) {
726 self.manifolds.clear();
727 }
728
729 pub fn get_warmstart(&self, body_a: u32, body_b: u32) -> Option<Vec<(f64, [f64; 2])>> {
731 let k = Self::key(body_a, body_b);
732 self.manifolds.get(&k).map(|m| m.warmstart_data())
733 }
734
735 pub fn remove_body(&mut self, body_id: u32) {
737 self.manifolds
738 .retain(|_, m| m.body_a != body_id && m.body_b != body_id);
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
746pub enum CachingStrategy {
747 IdOnly,
749 ProximityOnly,
751 IdThenProximity,
753}
754
755pub fn find_match_with_strategy(
757 existing: &[ContactPoint],
758 new_point: &ContactPoint,
759 strategy: CachingStrategy,
760 dist_sq_threshold: f64,
761) -> Option<usize> {
762 match strategy {
763 CachingStrategy::IdOnly => existing.iter().position(|p| p.id == new_point.id),
764 CachingStrategy::ProximityOnly => existing.iter().enumerate().find_map(|(i, p)| {
765 if len_sq3(sub3(p.local_pos_a, new_point.local_pos_a)) < dist_sq_threshold {
766 Some(i)
767 } else {
768 None
769 }
770 }),
771 CachingStrategy::IdThenProximity => {
772 ManifoldPointMatcher::find_match(existing, new_point, dist_sq_threshold)
773 }
774 }
775}
776
777pub fn age_warm_start(point: &mut ContactPoint, age_factor: f64) {
784 point.normal_impulse *= age_factor;
785 point.tangent_impulse[0] *= age_factor;
786 point.tangent_impulse[1] *= age_factor;
787}
788
789pub fn age_manifold_warm_start(manifold: &mut PersistentManifold, age_factor: f64) {
791 for p in &mut manifold.points {
792 age_warm_start(p, age_factor);
793 }
794}
795
796pub fn age_cache_warm_start(cache: &mut ContactManifoldCache, age_factor: f64) {
798 for manifold in cache.manifolds.values_mut() {
799 age_manifold_warm_start(manifold, age_factor);
800 }
801}
802
803pub fn baumgarte_correction(depth: f64, beta: f64, dt: f64) -> f64 {
810 if dt > 1e-12 {
811 (beta * depth / dt).max(0.0)
812 } else {
813 0.0
814 }
815}
816
817pub fn baumgarte_correction_slop(depth: f64, beta: f64, dt: f64, slop: f64) -> f64 {
821 baumgarte_correction((depth - slop).max(0.0), beta, dt)
822}
823
824pub fn apply_position_corrections(
828 manifold: &PersistentManifold,
829 beta: f64,
830 dt: f64,
831 slop: f64,
832) -> Vec<f64> {
833 manifold
834 .points
835 .iter()
836 .map(|p| baumgarte_correction_slop(p.depth, beta, dt, slop))
837 .collect()
838}
839
840#[derive(Debug, Clone)]
844pub struct ContactIsland {
845 pub bodies: Vec<u32>,
847 pub manifold_keys: Vec<(u32, u32)>,
849}
850
851impl ContactIsland {
852 pub fn new() -> Self {
854 Self {
855 bodies: Vec::new(),
856 manifold_keys: Vec::new(),
857 }
858 }
859
860 pub fn body_count(&self) -> usize {
862 self.bodies.len()
863 }
864
865 pub fn contact_count(&self) -> usize {
867 self.manifold_keys.len()
868 }
869}
870
871impl Default for ContactIsland {
872 fn default() -> Self {
873 Self::new()
874 }
875}
876
877pub fn build_contact_islands(cache: &ContactManifoldCache) -> Vec<ContactIsland> {
881 let mut all_bodies: Vec<u32> = Vec::new();
883 for (a, b) in cache.manifolds.keys() {
884 if !all_bodies.contains(a) {
885 all_bodies.push(*a);
886 }
887 if !all_bodies.contains(b) {
888 all_bodies.push(*b);
889 }
890 }
891
892 let n = all_bodies.len();
893 if n == 0 {
894 return Vec::new();
895 }
896
897 let mut parent: Vec<usize> = (0..n).collect();
899
900 let find = |parent: &mut Vec<usize>, mut x: usize| -> usize {
901 while parent[x] != x {
902 parent[x] = parent[parent[x]]; x = parent[x];
904 }
905 x
906 };
907
908 for (a, b) in cache.manifolds.keys() {
909 let Some(ia) = all_bodies.iter().position(|&id| id == *a) else {
910 continue;
911 };
912 let Some(ib) = all_bodies.iter().position(|&id| id == *b) else {
913 continue;
914 };
915 let ra = find(&mut parent, ia);
916 let rb = find(&mut parent, ib);
917 if ra != rb {
918 parent[ra] = rb;
919 }
920 }
921
922 let mut island_map: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
924 let mut islands: Vec<ContactIsland> = Vec::new();
925
926 for (i, &body_id) in all_bodies.iter().enumerate() {
927 let root = find(&mut parent, i);
928 let island_idx = *island_map.entry(root).or_insert_with(|| {
929 islands.push(ContactIsland::new());
930 islands.len() - 1
931 });
932 islands[island_idx].bodies.push(body_id);
933 }
934
935 for key in cache.manifolds.keys() {
937 let Some(ia) = all_bodies.iter().position(|&id| id == key.0) else {
938 continue;
939 };
940 let root = find(&mut parent, ia);
941 let island_idx = island_map[&root];
942 islands[island_idx].manifold_keys.push(*key);
943 }
944
945 islands
946}
947
948#[derive(Debug, Clone)]
952pub struct ManifoldMetrics {
953 pub contact_count: usize,
955 pub max_depth: f64,
957 pub avg_depth: f64,
959 pub spread: f64,
961 pub is_warm: bool,
963}
964
965pub fn compute_manifold_metrics(manifold: &PersistentManifold) -> ManifoldMetrics {
967 let n = manifold.points.len();
968 if n == 0 {
969 return ManifoldMetrics {
970 contact_count: 0,
971 max_depth: 0.0,
972 avg_depth: 0.0,
973 spread: 0.0,
974 is_warm: false,
975 };
976 }
977
978 let max_depth = manifold
979 .points
980 .iter()
981 .map(|p| p.depth)
982 .fold(0.0f64, f64::max);
983 let avg_depth = manifold.points.iter().map(|p| p.depth).sum::<f64>() / n as f64;
984 let is_warm = manifold
985 .points
986 .iter()
987 .all(|p| p.normal_impulse.abs() > 0.0 || p.lifetime > 0);
988
989 let spread = manifold
991 .points
992 .iter()
993 .enumerate()
994 .flat_map(|(i, a)| {
995 manifold.points[i + 1..]
996 .iter()
997 .map(move |b| len3(sub3(a.world_pos_a, b.world_pos_a)))
998 })
999 .fold(0.0f64, f64::max);
1000
1001 ManifoldMetrics {
1002 contact_count: n,
1003 max_depth,
1004 avg_depth,
1005 spread,
1006 is_warm,
1007 }
1008}
1009
1010#[cfg(test)]
1013mod tests {
1014 use super::*;
1015
1016 fn make_contact(
1017 pos_a: [f64; 3],
1018 pos_b: [f64; 3],
1019 depth: f64,
1020 normal_impulse: f64,
1021 ) -> ContactPoint {
1022 let mut cp = ContactPoint::new(pos_a, pos_b, pos_a, pos_b, [0.0, 1.0, 0.0], depth);
1023 cp.normal_impulse = normal_impulse;
1024 cp
1025 }
1026
1027 #[test]
1032 fn test_add_same_point_twice_no_overflow() {
1033 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1034 let pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
1035 m.add_or_update(pt.clone());
1036 m.add_or_update(pt.clone());
1037 assert_eq!(m.points.len(), 1, "same point added twice should stay as 1");
1038 }
1039
1040 #[test]
1041 fn test_add_or_update_max_4_points() {
1042 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1043 let positions: [[f64; 3]; 6] = [
1044 [1.0, 0.0, 0.0],
1045 [-1.0, 0.0, 0.0],
1046 [0.0, 0.0, 1.0],
1047 [0.0, 0.0, -1.0],
1048 [0.5, 0.0, 0.5],
1049 [-0.5, 0.0, -0.5],
1050 ];
1051 for &p in &positions {
1052 let neg_p = scale3(p, -1.0);
1053 let cp = make_contact(p, neg_p, 0.01, 0.0);
1054 m.add_or_update(cp);
1055 }
1056 assert!(
1057 m.points.len() <= 4,
1058 "manifold must not exceed 4 points, got {}",
1059 m.points.len()
1060 );
1061 }
1062
1063 #[test]
1068 fn test_reduce_to_4_points_with_6_inputs() {
1069 let positions: [[f64; 3]; 6] = [
1070 [1.0, 0.0, 0.0],
1071 [-1.0, 0.0, 0.0],
1072 [0.0, 0.0, 1.0],
1073 [0.0, 0.0, -1.0],
1074 [0.5, 0.0, 0.5],
1075 [-0.5, 0.0, -0.5],
1076 ];
1077 let points: Vec<ContactPoint> = positions
1078 .iter()
1079 .map(|&p| ContactPoint::new(p, p, p, p, [0.0, 1.0, 0.0], 0.01))
1080 .collect();
1081 let reduced = ManifoldReduction::reduce_to_4_points(&points);
1082 assert!(
1083 reduced.len() <= 4,
1084 "reduce_to_4_points must return <=4 points, got {}",
1085 reduced.len()
1086 );
1087 }
1088
1089 #[test]
1094 fn test_get_or_create_new_pair() {
1095 let mut cache = ContactManifoldCache::new(5);
1096 let m = cache.get_or_create(1, 2, 0.5, 0.3);
1097 assert_eq!(m.friction, 0.5);
1098 assert_eq!(m.restitution, 0.3);
1099 assert!(m.is_active);
1100 }
1101
1102 #[test]
1103 fn test_get_or_create_ordered_key() {
1104 let mut cache = ContactManifoldCache::new(5);
1105 let _ = cache.get_or_create(2, 1, 0.4, 0.2);
1106 assert!(
1107 cache.manifolds.contains_key(&(1, 2)),
1108 "key should be canonicalized to (1,2)"
1109 );
1110 }
1111
1112 #[test]
1117 fn test_begin_frame_marks_all_inactive() {
1118 let mut cache = ContactManifoldCache::new(5);
1119 let _ = cache.get_or_create(0, 1, 0.5, 0.3);
1120 let _ = cache.get_or_create(2, 3, 0.5, 0.3);
1121 cache.begin_frame();
1122 for m in cache.manifolds.values() {
1123 assert!(
1124 !m.is_active,
1125 "manifold should be inactive after begin_frame"
1126 );
1127 }
1128 }
1129
1130 #[test]
1135 fn test_warm_start_impulse_preserved() {
1136 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1137 let mut pt = ContactPoint::new(
1138 [0.0, 0.0, 0.0],
1139 [0.0, -0.01, 0.0],
1140 [0.0, 0.0, 0.0],
1141 [0.0, -0.01, 0.0],
1142 [0.0, 1.0, 0.0],
1143 0.01,
1144 );
1145 pt.normal_impulse = 42.0;
1146 m.add_or_update(pt);
1147
1148 let pt2 = ContactPoint::new(
1150 [0.0, 0.0, 0.0],
1151 [0.0, -0.01, 0.0],
1152 [0.0, 0.0, 0.0],
1153 [0.0, -0.01, 0.0],
1154 [0.0, 1.0, 0.0],
1155 0.01,
1156 );
1157 m.add_or_update(pt2);
1158
1159 assert_eq!(
1160 m.points[0].normal_impulse, 42.0,
1161 "normal_impulse should be warm-started from previous frame"
1162 );
1163 }
1164
1165 #[test]
1170 fn test_contact_area_nonzero_for_triangle() {
1171 let pts = vec![
1172 ContactPoint::new(
1173 [0.0, 0.0, 0.0],
1174 [0.0, 0.0, 0.0],
1175 [0.0, 0.0, 0.0],
1176 [0.0, 0.0, 0.0],
1177 [0.0, 1.0, 0.0],
1178 0.01,
1179 ),
1180 ContactPoint::new(
1181 [1.0, 0.0, 0.0],
1182 [1.0, 0.0, 0.0],
1183 [1.0, 0.0, 0.0],
1184 [1.0, 0.0, 0.0],
1185 [0.0, 1.0, 0.0],
1186 0.01,
1187 ),
1188 ContactPoint::new(
1189 [0.0, 0.0, 1.0],
1190 [0.0, 0.0, 1.0],
1191 [0.0, 0.0, 1.0],
1192 [0.0, 0.0, 1.0],
1193 [0.0, 1.0, 0.0],
1194 0.01,
1195 ),
1196 ];
1197 let area = ManifoldReduction::contact_area(&pts);
1198 assert!(
1199 area > 0.0,
1200 "contact_area of non-collinear triangle must be > 0, got {}",
1201 area
1202 );
1203 }
1204
1205 #[test]
1210 fn test_matcher_finds_by_id() {
1211 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0)];
1212 let new_pt = ContactPoint::new(
1214 [0.0, 0.0, 0.0],
1215 [0.0, -0.01, 0.0],
1216 [0.0, 0.0, 0.0],
1217 [0.0, -0.01, 0.0],
1218 [0.0, 1.0, 0.0],
1219 0.01,
1220 );
1221 let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
1222 assert_eq!(idx, Some(0), "should match by ID");
1223 }
1224
1225 #[test]
1226 fn test_matcher_finds_by_proximity() {
1227 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0)];
1228 let new_pt = ContactPoint::new(
1230 [0.001, 0.0, 0.0],
1231 [0.001, -0.01, 0.0],
1232 [0.001, 0.0, 0.0], [0.001, -0.01, 0.0],
1234 [0.0, 1.0, 0.0],
1235 0.01,
1236 );
1237 let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
1238 assert!(idx.is_some(), "should match by proximity");
1239 }
1240
1241 #[test]
1242 fn test_matcher_no_match_far() {
1243 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0)];
1244 let new_pt = ContactPoint::new(
1245 [5.0, 0.0, 0.0],
1246 [5.0, -0.01, 0.0],
1247 [5.0, 0.0, 0.0],
1248 [5.0, -0.01, 0.0],
1249 [0.0, 1.0, 0.0],
1250 0.01,
1251 );
1252 let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
1253 assert!(idx.is_none(), "far point should not match");
1254 }
1255
1256 #[test]
1261 fn test_lifetime_manager_ages_out_old_points() {
1262 let mgr = ManifoldLifetimeManager::new(2);
1263 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1264 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
1265 pt.lifetime = 100; m.points.push(pt);
1267 mgr.age_manifold(&mut m);
1268 assert!(m.points.is_empty(), "old point should be removed");
1269 }
1270
1271 #[test]
1272 fn test_lifetime_manager_keeps_young_points() {
1273 let mgr = ManifoldLifetimeManager::new(10);
1274 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1275 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
1276 pt.lifetime = 3;
1277 m.points.push(pt);
1278 mgr.age_manifold(&mut m);
1279 assert_eq!(m.points.len(), 1, "young point should be kept");
1280 }
1281
1282 #[test]
1287 fn test_compressor_no_change_small_set() {
1288 let pts: Vec<ContactPoint> = (0..3)
1289 .map(|i| make_contact([i as f64, 0.0, 0.0], [i as f64, -0.01, 0.0], 0.01, 0.0))
1290 .collect();
1291 let out = ManifoldCompressor::compress(&pts, 4);
1292 assert_eq!(out.len(), 3);
1293 }
1294
1295 #[test]
1296 fn test_compressor_single_point() {
1297 let pts: Vec<ContactPoint> = vec![
1298 make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.05, 1.0),
1299 make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0),
1300 ];
1301 let out = ManifoldCompressor::compress(&pts, 1);
1302 assert_eq!(out.len(), 1);
1303 assert!((out[0].depth - 0.05).abs() < 1e-10);
1305 }
1306
1307 #[test]
1308 fn test_compressor_merge_preserves_warmstart() {
1309 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 7.0)];
1310 let incoming = vec![ContactPoint::new(
1311 [0.0, 0.0, 0.0],
1312 [0.0, -0.01, 0.0],
1313 [0.0, 0.0, 0.0],
1314 [0.0, -0.01, 0.0],
1315 [0.0, 1.0, 0.0],
1316 0.01,
1317 )];
1318 let merged = ManifoldCompressor::merge(&existing, &incoming, 4);
1319 assert_eq!(merged.len(), 1);
1320 assert!(
1321 (merged[0].normal_impulse - 7.0).abs() < 1e-10,
1322 "warm-start should be 7.0"
1323 );
1324 }
1325
1326 #[test]
1331 fn test_update_from_new_contacts() {
1332 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1333 let contacts = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
1334 m.update_from_new_contacts(contacts);
1335 assert_eq!(m.contact_count(), 1);
1336 assert!(m.is_active);
1337 }
1338
1339 #[test]
1340 fn test_max_depth() {
1341 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1342 m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.02, 0.0));
1343 m.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.05, 0.0));
1344 assert!((m.max_depth() - 0.05).abs() < 1e-10);
1345 }
1346
1347 #[test]
1348 fn test_warmstart_data_roundtrip() {
1349 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1350 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0);
1351 pt.normal_impulse = 3.0;
1352 pt.tangent_impulse = [1.0, 2.0];
1353 m.points.push(pt);
1354 let ws = m.warmstart_data();
1355 assert_eq!(ws.len(), 1);
1356 assert!((ws[0].0 - 3.0).abs() < 1e-12);
1357 m.store_solver_impulses(&[(10.0, [5.0, 6.0])]);
1359 assert!((m.points[0].normal_impulse - 10.0).abs() < 1e-12);
1360 }
1361
1362 #[test]
1363 fn test_clamp_impulses() {
1364 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1365 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0);
1366 pt.normal_impulse = -3.0;
1367 m.points.push(pt);
1368 m.clamp_impulses();
1369 assert_eq!(m.points[0].normal_impulse, 0.0);
1370 }
1371
1372 #[test]
1377 fn test_cache_total_contact_points() {
1378 let mut cache = ContactManifoldCache::new(5);
1379 let m = cache.get_or_create(0, 1, 0.5, 0.3);
1380 m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
1381 m.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0));
1382 assert_eq!(cache.total_contact_points(), 2);
1383 }
1384
1385 #[test]
1386 fn test_cache_remove_body() {
1387 let mut cache = ContactManifoldCache::new(5);
1388 let _ = cache.get_or_create(0, 1, 0.5, 0.3);
1389 let _ = cache.get_or_create(2, 3, 0.5, 0.3);
1390 cache.remove_body(0);
1391 assert_eq!(cache.manifold_count(), 1);
1392 }
1393
1394 #[test]
1395 fn test_cache_clear() {
1396 let mut cache = ContactManifoldCache::new(5);
1397 let _ = cache.get_or_create(0, 1, 0.5, 0.3);
1398 cache.clear();
1399 assert_eq!(cache.manifold_count(), 0);
1400 }
1401
1402 #[test]
1403 fn test_cache_get_warmstart_none() {
1404 let cache = ContactManifoldCache::new(5);
1405 assert!(cache.get_warmstart(0, 1).is_none());
1406 }
1407
1408 #[test]
1411 fn test_caching_strategy_id_only_finds_match() {
1412 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
1413 let new_pt = ContactPoint::new(
1414 [0.0, 0.0, 0.0],
1415 [0.0, -0.01, 0.0],
1416 [0.0, 0.0, 0.0],
1417 [0.0, -0.01, 0.0],
1418 [0.0, 1.0, 0.0],
1419 0.01,
1420 );
1421 let result =
1422 find_match_with_strategy(&existing, &new_pt, CachingStrategy::IdOnly, MATCH_DIST_SQ);
1423 assert_eq!(result, Some(0), "ID-only strategy should match by hash ID");
1424 }
1425
1426 #[test]
1427 fn test_caching_strategy_proximity_only_finds_match() {
1428 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
1429 let new_pt = ContactPoint::new(
1431 [0.001, 0.0, 0.0],
1432 [0.001, -0.01, 0.0],
1433 [0.001, 0.0, 0.0],
1434 [0.001, -0.01, 0.0],
1435 [0.0, 1.0, 0.0],
1436 0.01,
1437 );
1438 let result = find_match_with_strategy(
1439 &existing,
1440 &new_pt,
1441 CachingStrategy::ProximityOnly,
1442 MATCH_DIST_SQ,
1443 );
1444 assert!(
1445 result.is_some(),
1446 "Proximity strategy should find nearby match"
1447 );
1448 }
1449
1450 #[test]
1451 fn test_caching_strategy_proximity_only_no_match_far() {
1452 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
1453 let new_pt = ContactPoint::new(
1454 [10.0, 0.0, 0.0],
1455 [10.0, -0.01, 0.0],
1456 [10.0, 0.0, 0.0],
1457 [10.0, -0.01, 0.0],
1458 [0.0, 1.0, 0.0],
1459 0.01,
1460 );
1461 let result = find_match_with_strategy(
1462 &existing,
1463 &new_pt,
1464 CachingStrategy::ProximityOnly,
1465 MATCH_DIST_SQ,
1466 );
1467 assert!(
1468 result.is_none(),
1469 "Far point should not match with proximity strategy"
1470 );
1471 }
1472
1473 #[test]
1474 fn test_caching_strategy_id_then_proximity_finds_by_id() {
1475 let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 3.0)];
1476 let new_pt = ContactPoint::new(
1477 [0.0, 0.0, 0.0],
1478 [0.0, -0.01, 0.0],
1479 [0.0, 0.0, 0.0],
1480 [0.0, -0.01, 0.0],
1481 [0.0, 1.0, 0.0],
1482 0.01,
1483 );
1484 let result = find_match_with_strategy(
1485 &existing,
1486 &new_pt,
1487 CachingStrategy::IdThenProximity,
1488 MATCH_DIST_SQ,
1489 );
1490 assert_eq!(result, Some(0), "IdThenProximity should find by ID");
1491 }
1492
1493 #[test]
1496 fn test_age_warm_start_scales_impulses() {
1497 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 10.0);
1498 pt.tangent_impulse = [4.0, 2.0];
1499 age_warm_start(&mut pt, 0.9);
1500 assert!(
1501 (pt.normal_impulse - 9.0).abs() < 1e-10,
1502 "normal_impulse should be 9.0"
1503 );
1504 assert!(
1505 (pt.tangent_impulse[0] - 3.6).abs() < 1e-10,
1506 "tangent[0] should be 3.6"
1507 );
1508 assert!(
1509 (pt.tangent_impulse[1] - 1.8).abs() < 1e-10,
1510 "tangent[1] should be 1.8"
1511 );
1512 }
1513
1514 #[test]
1515 fn test_age_manifold_warm_start() {
1516 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1517 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0);
1518 pt.tangent_impulse = [2.0, 0.0];
1519 m.points.push(pt);
1520 age_manifold_warm_start(&mut m, 0.5);
1521 assert!(
1522 (m.points[0].normal_impulse - 2.5).abs() < 1e-10,
1523 "normal_impulse should be 2.5"
1524 );
1525 assert!(
1526 (m.points[0].tangent_impulse[0] - 1.0).abs() < 1e-10,
1527 "tangent[0] should be 1.0"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_age_cache_warm_start() {
1533 let mut cache = ContactManifoldCache::new(5);
1534 let m = cache.get_or_create(0, 1, 0.5, 0.3);
1535 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 8.0);
1536 pt.tangent_impulse = [4.0, 0.0];
1537 m.points.push(pt);
1538 age_cache_warm_start(&mut cache, 0.5);
1539 let m2 = cache.manifolds.get(&(0, 1)).unwrap();
1540 assert!(
1541 (m2.points[0].normal_impulse - 4.0).abs() < 1e-10,
1542 "normal_impulse should be 4.0"
1543 );
1544 }
1545
1546 #[test]
1549 fn test_baumgarte_correction_basic() {
1550 let corr = baumgarte_correction(0.1, 0.2, 0.016);
1551 let expected = 0.2 * 0.1 / 0.016;
1552 assert!(
1553 (corr - expected).abs() < 1e-10,
1554 "Expected {expected}, got {corr}"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_baumgarte_correction_zero_depth() {
1560 let corr = baumgarte_correction(0.0, 0.2, 0.016);
1561 assert_eq!(corr, 0.0, "Zero depth should produce no correction");
1562 }
1563
1564 #[test]
1565 fn test_baumgarte_correction_slop_no_correction_below_slop() {
1566 let corr = baumgarte_correction_slop(0.003, 0.2, 0.016, 0.005);
1567 assert_eq!(corr, 0.0, "Depth below slop should produce no correction");
1568 }
1569
1570 #[test]
1571 fn test_baumgarte_correction_slop_correction_above_slop() {
1572 let corr = baumgarte_correction_slop(0.01, 0.2, 0.016, 0.005);
1573 let expected = baumgarte_correction(0.005, 0.2, 0.016);
1574 assert!(
1575 (corr - expected).abs() < 1e-10,
1576 "Expected {expected}, got {corr}"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_apply_position_corrections_returns_per_point() {
1582 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1583 m.points
1584 .push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
1585 m.points
1586 .push(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.02, 0.0));
1587 let corrections = apply_position_corrections(&m, 0.2, 0.016, 0.005);
1588 assert_eq!(
1589 corrections.len(),
1590 2,
1591 "Should return one correction per point"
1592 );
1593 assert!(
1594 corrections[0] >= 0.0 && corrections[1] >= 0.0,
1595 "Corrections should be non-negative"
1596 );
1597 }
1598
1599 #[test]
1602 fn test_contact_island_default() {
1603 let island = ContactIsland::default();
1604 assert_eq!(island.body_count(), 0);
1605 assert_eq!(island.contact_count(), 0);
1606 }
1607
1608 #[test]
1609 fn test_build_contact_islands_single_pair() {
1610 let mut cache = ContactManifoldCache::new(5);
1611 let m = cache.get_or_create(0, 1, 0.5, 0.3);
1612 m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
1613 let islands = build_contact_islands(&cache);
1614 assert_eq!(islands.len(), 1, "One pair should form one island");
1615 assert_eq!(islands[0].body_count(), 2, "Island should have 2 bodies");
1616 }
1617
1618 #[test]
1619 fn test_build_contact_islands_two_separate_pairs() {
1620 let mut cache = ContactManifoldCache::new(5);
1621 let m1 = cache.get_or_create(0, 1, 0.5, 0.3);
1622 m1.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
1623 let m2 = cache.get_or_create(2, 3, 0.5, 0.3);
1624 m2.add_or_update(make_contact([5.0, 0.0, 0.0], [5.0, -0.01, 0.0], 0.01, 0.0));
1625 let islands = build_contact_islands(&cache);
1626 assert_eq!(
1628 islands.len(),
1629 2,
1630 "Two separate pairs should form two islands"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_build_contact_islands_connected_chain() {
1636 let mut cache = ContactManifoldCache::new(5);
1638 let m1 = cache.get_or_create(0, 1, 0.5, 0.3);
1639 m1.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
1640 let m2 = cache.get_or_create(1, 2, 0.5, 0.3);
1641 m2.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0));
1642 let islands = build_contact_islands(&cache);
1643 assert_eq!(islands.len(), 1, "Connected chain should form one island");
1644 assert_eq!(
1645 islands[0].body_count(),
1646 3,
1647 "Chain of 3 bodies should have 3 bodies in island"
1648 );
1649 }
1650
1651 #[test]
1654 fn test_manifold_metrics_empty() {
1655 let m = PersistentManifold::new(0, 1, 0.5, 0.3);
1656 let metrics = compute_manifold_metrics(&m);
1657 assert_eq!(metrics.contact_count, 0);
1658 assert_eq!(metrics.max_depth, 0.0);
1659 assert!(!metrics.is_warm);
1660 }
1661
1662 #[test]
1663 fn test_manifold_metrics_single_point() {
1664 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1665 m.points
1666 .push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.05, 0.0));
1667 let metrics = compute_manifold_metrics(&m);
1668 assert_eq!(metrics.contact_count, 1);
1669 assert!(
1670 (metrics.max_depth - 0.05).abs() < 1e-10,
1671 "max_depth should be 0.05"
1672 );
1673 assert!(
1674 (metrics.avg_depth - 0.05).abs() < 1e-10,
1675 "avg_depth should be 0.05"
1676 );
1677 assert_eq!(metrics.spread, 0.0, "Single point has zero spread");
1678 }
1679
1680 #[test]
1681 fn test_manifold_metrics_two_points() {
1682 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1683 m.points
1684 .push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.02, 0.0));
1685 m.points
1686 .push(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.04, 0.0));
1687 let metrics = compute_manifold_metrics(&m);
1688 assert_eq!(metrics.contact_count, 2);
1689 assert!((metrics.max_depth - 0.04).abs() < 1e-10);
1690 assert!((metrics.avg_depth - 0.03).abs() < 1e-10);
1691 assert!(
1692 (metrics.spread - 1.0).abs() < 1e-10,
1693 "Spread should be 1.0, got {}",
1694 metrics.spread
1695 );
1696 }
1697
1698 #[test]
1699 fn test_manifold_metrics_is_warm_with_impulse() {
1700 let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
1701 let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0);
1702 pt.normal_impulse = 5.0;
1703 m.points.push(pt);
1704 let metrics = compute_manifold_metrics(&m);
1705 assert!(
1706 metrics.is_warm,
1707 "Manifold with non-zero normal_impulse should be warm"
1708 );
1709 }
1710}