1use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct ContactKey {
17 pub body_a: usize,
19 pub body_b: usize,
21}
22
23impl ContactKey {
24 pub fn new(a: usize, b: usize) -> Self {
26 if a <= b {
27 Self {
28 body_a: a,
29 body_b: b,
30 }
31 } else {
32 Self {
33 body_a: b,
34 body_b: a,
35 }
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
44pub struct PersistedContact {
45 pub key: ContactKey,
47 pub normal: [f64; 3],
49 pub depth: f64,
51 pub contact_point: [f64; 3],
53 pub age: u32,
55 pub cached_impulse: f64,
57 pub is_active: bool,
59}
60
61#[derive(Debug, Default)]
65pub struct ContactGraph {
66 contacts: HashMap<ContactKey, PersistedContact>,
67}
68
69impl ContactGraph {
70 pub fn new() -> Self {
72 Self {
73 contacts: HashMap::new(),
74 }
75 }
76
77 pub fn update(&mut self, key: ContactKey, normal: [f64; 3], depth: f64, point: [f64; 3]) {
79 let entry = self.contacts.entry(key).or_insert(PersistedContact {
80 key,
81 normal,
82 depth,
83 contact_point: point,
84 age: 0,
85 cached_impulse: 0.0,
86 is_active: true,
87 });
88 entry.normal = normal;
89 entry.depth = depth;
90 entry.contact_point = point;
91 entry.age += 1;
92 entry.is_active = true;
93 }
94
95 pub fn remove(&mut self, key: &ContactKey) {
97 self.contacts.remove(key);
98 }
99
100 pub fn get(&self, key: &ContactKey) -> Option<&PersistedContact> {
102 self.contacts.get(key)
103 }
104
105 pub fn get_mut(&mut self, key: &ContactKey) -> Option<&mut PersistedContact> {
107 self.contacts.get_mut(key)
108 }
109
110 pub fn mark_inactive_all(&mut self) {
112 for c in self.contacts.values_mut() {
113 c.is_active = false;
114 }
115 }
116
117 pub fn purge_inactive(&mut self) {
119 self.contacts.retain(|_, v| v.is_active);
120 }
121
122 pub fn active_contacts(&self) -> Vec<&PersistedContact> {
124 self.contacts.values().filter(|c| c.is_active).collect()
125 }
126
127 pub fn contacts_for_body(&self, body: usize) -> Vec<&PersistedContact> {
129 self.contacts
130 .values()
131 .filter(|c| c.key.body_a == body || c.key.body_b == body)
132 .collect()
133 }
134
135 pub fn len(&self) -> usize {
137 self.contacts.len()
138 }
139
140 pub fn is_empty(&self) -> bool {
142 self.contacts.is_empty()
143 }
144}
145
146#[derive(Debug, Clone)]
150pub struct SpeculativeContact {
151 pub body_a: usize,
153 pub body_b: usize,
155 pub normal: [f64; 3],
157 pub separation: f64,
159 pub closing_velocity: f64,
161}
162
163pub fn speculative_contact(
168 pos_a: [f64; 3],
169 vel_a: [f64; 3],
170 pos_b: [f64; 3],
171 vel_b: [f64; 3],
172 radius_a: f64,
173 radius_b: f64,
174 dt: f64,
175) -> Option<SpeculativeContact> {
176 let dx = pos_b[0] - pos_a[0];
177 let dy = pos_b[1] - pos_a[1];
178 let dz = pos_b[2] - pos_a[2];
179 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
180
181 let (nx, ny, nz) = if dist > 1e-12 {
182 (dx / dist, dy / dist, dz / dist)
183 } else {
184 (0.0, 1.0, 0.0)
185 };
186
187 let separation = dist - (radius_a + radius_b);
188
189 let rel_vx = vel_b[0] - vel_a[0];
191 let rel_vy = vel_b[1] - vel_a[1];
192 let rel_vz = vel_b[2] - vel_a[2];
193 let rel_v_along_n = rel_vx * nx + rel_vy * ny + rel_vz * nz;
195 let closing_velocity = -rel_v_along_n;
197
198 let will_contact =
199 separation <= 0.0 || (closing_velocity > 0.0 && separation < closing_velocity * dt);
200
201 if will_contact {
202 Some(SpeculativeContact {
203 body_a: 0,
204 body_b: 1,
205 normal: [nx, ny, nz],
206 separation,
207 closing_velocity,
208 })
209 } else {
210 None
211 }
212}
213
214pub fn speculative_impulse(
218 contact: &SpeculativeContact,
219 inv_mass_a: f64,
220 inv_mass_b: f64,
221 restitution: f64,
222) -> f64 {
223 let denom = inv_mass_a + inv_mass_b;
224 if denom < 1e-30 {
225 return 0.0;
226 }
227 let numerator = (1.0 + restitution) * contact.closing_velocity;
229 (numerator / denom).max(0.0)
230}
231
232#[derive(Debug)]
239pub struct ContactCache {
240 entries: Vec<(ContactKey, f64)>,
241 capacity: usize,
242}
243
244impl ContactCache {
245 pub fn new(capacity: usize) -> Self {
247 Self {
248 entries: Vec::with_capacity(capacity),
249 capacity,
250 }
251 }
252
253 pub fn insert(&mut self, key: ContactKey, impulse: f64) {
255 if let Some(e) = self.entries.iter_mut().find(|e| e.0 == key) {
257 e.1 = impulse;
258 return;
259 }
260 if self.capacity == 0 {
262 return;
263 }
264 if self.entries.len() >= self.capacity {
266 self.entries.remove(0);
267 }
268 self.entries.push((key, impulse));
269 }
270
271 pub fn lookup(&self, key: &ContactKey) -> Option<f64> {
273 self.entries.iter().find(|e| &e.0 == key).map(|e| e.1)
274 }
275
276 pub fn len(&self) -> usize {
278 self.entries.len()
279 }
280
281 pub fn is_empty(&self) -> bool {
283 self.entries.is_empty()
284 }
285}
286
287#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::ContactGraph;
293 use crate::ContactKey;
294
295 #[test]
298 fn contact_key_canonical_order() {
299 let k1 = ContactKey::new(3, 1);
300 let k2 = ContactKey::new(1, 3);
301 assert_eq!(k1, k2);
302 assert_eq!(k1.body_a, 1);
303 assert_eq!(k1.body_b, 3);
304 }
305
306 #[test]
307 fn contact_key_equal_indices() {
308 let k = ContactKey::new(5, 5);
309 assert_eq!(k.body_a, 5);
310 assert_eq!(k.body_b, 5);
311 }
312
313 #[test]
314 fn contact_key_hash_consistent() {
315 use std::collections::HashSet;
316 let mut s = HashSet::new();
317 s.insert(ContactKey::new(0, 1));
318 s.insert(ContactKey::new(1, 0));
319 assert_eq!(s.len(), 1);
320 }
321
322 #[test]
323 fn contact_key_different_pairs_not_equal() {
324 let k1 = ContactKey::new(0, 1);
325 let k2 = ContactKey::new(0, 2);
326 assert_ne!(k1, k2);
327 }
328
329 #[test]
330 fn contact_key_zero_indices() {
331 let k = ContactKey::new(0, 0);
332 assert_eq!(k.body_a, 0);
333 assert_eq!(k.body_b, 0);
334 }
335
336 #[test]
339 fn contact_graph_new_is_empty() {
340 let g = ContactGraph::new();
341 assert!(g.is_empty());
342 assert_eq!(g.len(), 0);
343 }
344
345 #[test]
346 fn contact_graph_update_inserts() {
347 let mut g = ContactGraph::new();
348 let key = ContactKey::new(0, 1);
349 g.update(key, [0.0, 1.0, 0.0], 0.1, [0.0, 0.0, 0.0]);
350 assert_eq!(g.len(), 1);
351 }
352
353 #[test]
354 fn contact_graph_update_increments_age() {
355 let mut g = ContactGraph::new();
356 let key = ContactKey::new(0, 1);
357 g.update(key, [0.0, 1.0, 0.0], 0.1, [0.0, 0.0, 0.0]);
358 g.update(key, [0.0, 1.0, 0.0], 0.2, [0.0, 0.0, 0.0]);
359 let c = g.get(&key).unwrap();
360 assert_eq!(c.age, 2);
361 }
362
363 #[test]
364 fn contact_graph_update_overwrites_depth() {
365 let mut g = ContactGraph::new();
366 let key = ContactKey::new(0, 1);
367 g.update(key, [0.0, 1.0, 0.0], 0.5, [0.0, 0.0, 0.0]);
368 g.update(key, [0.0, 1.0, 0.0], 1.5, [0.0, 0.0, 0.0]);
369 assert!((g.get(&key).unwrap().depth - 1.5).abs() < 1e-12);
370 }
371
372 #[test]
373 fn contact_graph_remove() {
374 let mut g = ContactGraph::new();
375 let key = ContactKey::new(0, 1);
376 g.update(key, [0.0, 1.0, 0.0], 0.1, [0.0, 0.0, 0.0]);
377 g.remove(&key);
378 assert!(g.is_empty());
379 }
380
381 #[test]
382 fn contact_graph_get_missing_returns_none() {
383 let g = ContactGraph::new();
384 let key = ContactKey::new(7, 8);
385 assert!(g.get(&key).is_none());
386 }
387
388 #[test]
389 fn contact_graph_get_mut_modifies() {
390 let mut g = ContactGraph::new();
391 let key = ContactKey::new(0, 1);
392 g.update(key, [0.0, 1.0, 0.0], 0.1, [0.0, 0.0, 0.0]);
393 g.get_mut(&key).unwrap().cached_impulse = 3.125;
394 assert!((g.get(&key).unwrap().cached_impulse - 3.125).abs() < 1e-12);
395 }
396
397 #[test]
398 fn contact_graph_mark_inactive_all() {
399 let mut g = ContactGraph::new();
400 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
401 g.update(ContactKey::new(1, 2), [0.0, 1.0, 0.0], 0.2, [0.0; 3]);
402 g.mark_inactive_all();
403 assert_eq!(g.active_contacts().len(), 0);
404 }
405
406 #[test]
407 fn contact_graph_purge_inactive_removes_stale() {
408 let mut g = ContactGraph::new();
409 let k1 = ContactKey::new(0, 1);
410 let k2 = ContactKey::new(1, 2);
411 g.update(k1, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
412 g.update(k2, [0.0, 1.0, 0.0], 0.2, [0.0; 3]);
413 g.mark_inactive_all();
414 g.update(k1, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
416 g.purge_inactive();
417 assert_eq!(g.len(), 1);
418 assert!(g.get(&k1).is_some());
419 assert!(g.get(&k2).is_none());
420 }
421
422 #[test]
423 fn contact_graph_active_contacts_count() {
424 let mut g = ContactGraph::new();
425 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
426 g.update(ContactKey::new(2, 3), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
427 g.mark_inactive_all();
428 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
429 assert_eq!(g.active_contacts().len(), 1);
430 }
431
432 #[test]
433 fn contact_graph_contacts_for_body() {
434 let mut g = ContactGraph::new();
435 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
436 g.update(ContactKey::new(0, 2), [0.0, 1.0, 0.0], 0.2, [0.0; 3]);
437 g.update(ContactKey::new(3, 4), [0.0, 1.0, 0.0], 0.3, [0.0; 3]);
438 let contacts = g.contacts_for_body(0);
439 assert_eq!(contacts.len(), 2);
440 let contacts_4 = g.contacts_for_body(4);
441 assert_eq!(contacts_4.len(), 1);
442 }
443
444 #[test]
445 fn contact_graph_is_active_on_update() {
446 let mut g = ContactGraph::new();
447 let key = ContactKey::new(0, 1);
448 g.update(key, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
449 assert!(g.get(&key).unwrap().is_active);
450 }
451
452 #[test]
455 fn speculative_contact_approaching_predicts_contact() {
456 let pos_a = [0.0, 0.0, 0.0];
458 let vel_a = [0.5, 0.0, 0.0];
459 let pos_b = [1.0, 0.0, 0.0];
460 let vel_b = [-0.5, 0.0, 0.0];
461 let result = speculative_contact(pos_a, vel_a, pos_b, vel_b, 0.4, 0.4, 1.0 / 60.0);
462 assert!(result.is_none());
464 }
465
466 #[test]
467 fn speculative_contact_fast_approach_within_dt() {
468 let pos_a = [0.0, 0.0, 0.0];
470 let vel_a = [5.0, 0.0, 0.0];
471 let pos_b = [1.0, 0.0, 0.0];
472 let vel_b = [-5.0, 0.0, 0.0];
473 let result = speculative_contact(pos_a, vel_a, pos_b, vel_b, 0.495, 0.495, 1.0 / 60.0);
475 assert!(result.is_some());
476 let c = result.unwrap();
477 assert!(c.closing_velocity > 0.0);
478 }
479
480 #[test]
481 fn speculative_contact_penetrating_always_some() {
482 let pos_a = [0.0, 0.0, 0.0];
484 let pos_b = [0.5, 0.0, 0.0];
485 let result = speculative_contact(pos_a, [0.0; 3], pos_b, [0.0; 3], 0.4, 0.4, 1.0 / 60.0);
486 assert!(result.is_some());
487 assert!(result.unwrap().separation < 0.0);
488 }
489
490 #[test]
491 fn speculative_contact_separating_returns_none() {
492 let pos_a = [0.0, 0.0, 0.0];
493 let vel_a = [-5.0, 0.0, 0.0];
494 let pos_b = [2.0, 0.0, 0.0];
495 let vel_b = [5.0, 0.0, 0.0];
496 let result = speculative_contact(pos_a, vel_a, pos_b, vel_b, 0.1, 0.1, 1.0 / 60.0);
497 assert!(result.is_none());
498 }
499
500 #[test]
501 fn speculative_impulse_zero_for_static_bodies() {
502 let c = SpeculativeContact {
503 body_a: 0,
504 body_b: 1,
505 normal: [0.0, 1.0, 0.0],
506 separation: -0.05,
507 closing_velocity: 1.0,
508 };
509 let j = speculative_impulse(&c, 0.0, 0.0, 0.5);
510 assert!((j).abs() < 1e-12);
511 }
512
513 #[test]
514 fn speculative_impulse_positive_for_approaching() {
515 let c = SpeculativeContact {
516 body_a: 0,
517 body_b: 1,
518 normal: [0.0, 1.0, 0.0],
519 separation: -0.05,
520 closing_velocity: 2.0,
521 };
522 let j = speculative_impulse(&c, 1.0, 1.0, 0.0);
523 assert!(j > 0.0);
524 }
525
526 #[test]
527 fn speculative_impulse_no_negative() {
528 let c = SpeculativeContact {
530 body_a: 0,
531 body_b: 1,
532 normal: [0.0, 1.0, 0.0],
533 separation: 0.5,
534 closing_velocity: -1.0,
535 };
536 let j = speculative_impulse(&c, 1.0, 1.0, 0.5);
537 assert!(j >= 0.0);
538 }
539
540 #[test]
543 fn contact_cache_insert_and_lookup() {
544 let mut cache = ContactCache::new(8);
545 let key = ContactKey::new(0, 1);
546 cache.insert(key, 1.5);
547 assert_eq!(cache.lookup(&key), Some(1.5));
548 }
549
550 #[test]
551 fn contact_cache_lookup_missing_returns_none() {
552 let cache = ContactCache::new(8);
553 assert_eq!(cache.lookup(&ContactKey::new(9, 10)), None);
554 }
555
556 #[test]
557 fn contact_cache_update_existing() {
558 let mut cache = ContactCache::new(8);
559 let key = ContactKey::new(0, 1);
560 cache.insert(key, 1.0);
561 cache.insert(key, 2.0);
562 assert_eq!(cache.lookup(&key), Some(2.0));
563 assert_eq!(cache.len(), 1);
564 }
565
566 #[test]
567 fn contact_cache_lru_eviction() {
568 let mut cache = ContactCache::new(3);
569 let k0 = ContactKey::new(0, 1);
570 let k1 = ContactKey::new(1, 2);
571 let k2 = ContactKey::new(2, 3);
572 let k3 = ContactKey::new(3, 4);
573 cache.insert(k0, 0.0);
574 cache.insert(k1, 1.0);
575 cache.insert(k2, 2.0);
576 cache.insert(k3, 3.0);
578 assert_eq!(cache.len(), 3);
579 assert!(cache.lookup(&k0).is_none());
580 assert_eq!(cache.lookup(&k3), Some(3.0));
581 }
582
583 #[test]
584 fn contact_cache_is_empty() {
585 let cache = ContactCache::new(4);
586 assert!(cache.is_empty());
587 }
588
589 #[test]
590 fn contact_cache_zero_capacity_does_not_panic() {
591 let mut cache = ContactCache::new(0);
592 let key = ContactKey::new(0, 1);
593 cache.insert(key, 1.0); assert!(cache.is_empty());
595 }
596}
597
598#[derive(Debug, Clone, Copy)]
602pub struct ContactPoint {
603 pub position: [f64; 3],
605 pub depth: f64,
607 pub impulse: f64,
609 pub warm_started: bool,
611}
612
613impl ContactPoint {
614 pub fn new(position: [f64; 3], depth: f64) -> Self {
616 Self {
617 position,
618 depth,
619 impulse: 0.0,
620 warm_started: false,
621 }
622 }
623}
624
625#[derive(Debug, Clone)]
630pub struct ContactManifold {
631 pub key: ContactKey,
633 pub normal: [f64; 3],
635 pub points: Vec<ContactPoint>,
637 pub age: u32,
639 pub active: bool,
641}
642
643impl ContactManifold {
644 pub fn new(key: ContactKey, normal: [f64; 3]) -> Self {
646 Self {
647 key,
648 normal,
649 points: Vec::with_capacity(4),
650 age: 0,
651 active: true,
652 }
653 }
654
655 pub fn add_point(&mut self, point: ContactPoint) {
658 if self.points.len() < 4 {
659 self.points.push(point);
660 } else {
661 self.reduce_contacts(point);
663 }
664 }
665
666 fn reduce_contacts(&mut self, new_point: ContactPoint) {
669 let mut candidates = self.points.clone();
672 candidates.push(new_point);
673
674 let mut max_area = -1.0_f64;
675 let mut best_set = self.points.clone();
676
677 let deepest_idx = candidates
679 .iter()
680 .enumerate()
681 .max_by(|(_, a), (_, b)| {
682 a.depth
683 .partial_cmp(&b.depth)
684 .unwrap_or(std::cmp::Ordering::Equal)
685 })
686 .map(|(i, _)| i)
687 .unwrap_or(0);
688
689 let remaining: Vec<usize> = (0..candidates.len())
691 .filter(|&i| i != deepest_idx)
692 .collect();
693 for skip in &remaining {
694 let subset: Vec<&ContactPoint> = candidates
695 .iter()
696 .enumerate()
697 .filter(|(i, _)| *i != deepest_idx && i != skip)
698 .map(|(_, p)| p)
699 .collect();
700 if subset.len() < 3 {
701 continue;
702 }
703 let area = triangle_area(subset[0].position, subset[1].position, subset[2].position);
704 if area > max_area {
705 max_area = area;
706 let mut new_set = vec![candidates[deepest_idx]];
707 new_set.extend(subset.iter().copied().copied());
708 best_set = new_set;
709 }
710 }
711
712 self.points = best_set;
713 }
714
715 pub fn mark_inactive(&mut self) {
717 self.active = false;
718 }
719
720 pub fn clear_points(&mut self) {
722 self.points.clear();
723 }
724
725 pub fn tick(&mut self) {
727 self.age += 1;
728 }
729
730 pub fn n_points(&self) -> usize {
732 self.points.len()
733 }
734}
735
736fn triangle_area(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
738 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
739 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
740 let cross = [
741 ab[1] * ac[2] - ab[2] * ac[1],
742 ab[2] * ac[0] - ab[0] * ac[2],
743 ab[0] * ac[1] - ab[1] * ac[0],
744 ];
745 0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt()
746}
747
748#[derive(Debug, Default)]
752pub struct ManifoldStore {
753 manifolds: HashMap<ContactKey, ContactManifold>,
754}
755
756impl ManifoldStore {
757 pub fn new() -> Self {
759 Self::default()
760 }
761
762 pub fn get_or_create(&mut self, key: ContactKey, normal: [f64; 3]) -> &mut ContactManifold {
764 self.manifolds
765 .entry(key)
766 .or_insert_with(|| ContactManifold::new(key, normal))
767 }
768
769 pub fn get(&self, key: &ContactKey) -> Option<&ContactManifold> {
771 self.manifolds.get(key)
772 }
773
774 pub fn mark_all_inactive(&mut self) {
776 for m in self.manifolds.values_mut() {
777 m.mark_inactive();
778 }
779 }
780
781 pub fn purge_inactive(&mut self) {
783 self.manifolds.retain(|_, m| m.active);
784 }
785
786 pub fn tick_all(&mut self) {
788 for m in self.manifolds.values_mut() {
789 m.tick();
790 }
791 }
792
793 pub fn len(&self) -> usize {
795 self.manifolds.len()
796 }
797
798 pub fn is_empty(&self) -> bool {
800 self.manifolds.is_empty()
801 }
802
803 pub fn active_manifolds(&self) -> Vec<&ContactManifold> {
805 self.manifolds.values().filter(|m| m.active).collect()
806 }
807}
808
809#[derive(Debug, Clone, Copy)]
813pub struct ContactFrame {
814 pub normal: [f64; 3],
816 pub tangent1: [f64; 3],
818 pub tangent2: [f64; 3],
820}
821
822impl ContactFrame {
823 pub fn from_normal(n: [f64; 3]) -> Self {
827 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
829 let normal = if len > 1e-12 {
830 [n[0] / len, n[1] / len, n[2] / len]
831 } else {
832 [0.0, 1.0, 0.0]
833 };
834
835 let ref_vec = if normal[0].abs() < 0.9 {
837 [1.0, 0.0, 0.0]
838 } else {
839 [0.0, 1.0, 0.0]
840 };
841
842 let dot_rn = ref_vec[0] * normal[0] + ref_vec[1] * normal[1] + ref_vec[2] * normal[2];
844 let t1_raw = [
845 ref_vec[0] - dot_rn * normal[0],
846 ref_vec[1] - dot_rn * normal[1],
847 ref_vec[2] - dot_rn * normal[2],
848 ];
849 let t1_len = (t1_raw[0] * t1_raw[0] + t1_raw[1] * t1_raw[1] + t1_raw[2] * t1_raw[2]).sqrt();
850 let tangent1 = if t1_len > 1e-12 {
851 [t1_raw[0] / t1_len, t1_raw[1] / t1_len, t1_raw[2] / t1_len]
852 } else {
853 [0.0, 0.0, 1.0]
854 };
855
856 let tangent2 = [
858 normal[1] * tangent1[2] - normal[2] * tangent1[1],
859 normal[2] * tangent1[0] - normal[0] * tangent1[2],
860 normal[0] * tangent1[1] - normal[1] * tangent1[0],
861 ];
862
863 Self {
864 normal,
865 tangent1,
866 tangent2,
867 }
868 }
869
870 pub fn project(&self, v: [f64; 3]) -> [f64; 3] {
872 let vn = v[0] * self.normal[0] + v[1] * self.normal[1] + v[2] * self.normal[2];
873 let vt1 = v[0] * self.tangent1[0] + v[1] * self.tangent1[1] + v[2] * self.tangent1[2];
874 let vt2 = v[0] * self.tangent2[0] + v[1] * self.tangent2[1] + v[2] * self.tangent2[2];
875 [vn, vt1, vt2]
876 }
877
878 pub fn unproject(&self, v: [f64; 3]) -> [f64; 3] {
880 [
881 v[0] * self.normal[0] + v[1] * self.tangent1[0] + v[2] * self.tangent2[0],
882 v[0] * self.normal[1] + v[1] * self.tangent1[1] + v[2] * self.tangent2[1],
883 v[0] * self.normal[2] + v[1] * self.tangent1[2] + v[2] * self.tangent2[2],
884 ]
885 }
886}
887
888pub struct ContactCacheInvalidator {
895 pub prev_positions: Vec<[f64; 3]>,
897 pub threshold: f64,
899}
900
901impl ContactCacheInvalidator {
902 pub fn new(n_bodies: usize, threshold: f64) -> Self {
904 Self {
905 prev_positions: vec![[0.0; 3]; n_bodies],
906 threshold,
907 }
908 }
909
910 pub fn update_and_check(
912 &mut self,
913 new_positions: &[[f64; 3]],
914 contact_radius: f64,
915 ) -> Vec<usize> {
916 let limit = self.threshold * contact_radius;
917 let moved: Vec<usize> = new_positions
918 .iter()
919 .enumerate()
920 .filter(|(i, np)| {
921 let pp = self.prev_positions[*i];
922 let d = [np[0] - pp[0], np[1] - pp[1], np[2] - pp[2]];
923 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() > limit
924 })
925 .map(|(i, _)| i)
926 .collect();
927 let copy_len = new_positions.len().min(self.prev_positions.len());
928 self.prev_positions[..copy_len].copy_from_slice(&new_positions[..copy_len]);
929 moved
930 }
931
932 pub fn invalidate_contacts(&self, graph: &mut ContactGraph, moved_bodies: &[usize]) {
934 for &body in moved_bodies {
935 let keys_to_remove: Vec<ContactKey> = graph
936 .contacts_for_body(body)
937 .iter()
938 .map(|c| c.key)
939 .collect();
940 for key in keys_to_remove {
941 graph.remove(&key);
942 }
943 }
944 }
945}
946
947#[derive(Debug, Clone)]
954pub struct ContactIsland {
955 pub bodies: Vec<usize>,
957 pub edges: Vec<ContactKey>,
959 pub can_sleep: bool,
961}
962
963impl ContactIsland {
964 pub fn body_count(&self) -> usize {
966 self.bodies.len()
967 }
968
969 pub fn edge_count(&self) -> usize {
971 self.edges.len()
972 }
973}
974
975#[derive(Debug, Clone, Default)]
977pub struct IslandStats {
978 pub island_count: usize,
980 pub avg_size: f64,
982 pub max_size: usize,
984 pub sleeping_islands: usize,
986 pub total_bodies: usize,
988}
989
990impl IslandStats {
991 pub fn from_islands(islands: &[ContactIsland]) -> Self {
993 if islands.is_empty() {
994 return Self::default();
995 }
996 let total: usize = islands.iter().map(|i| i.bodies.len()).sum();
997 let max_size = islands.iter().map(|i| i.bodies.len()).max().unwrap_or(0);
998 let sleeping = islands.iter().filter(|i| i.can_sleep).count();
999 Self {
1000 island_count: islands.len(),
1001 avg_size: total as f64 / islands.len() as f64,
1002 max_size,
1003 sleeping_islands: sleeping,
1004 total_bodies: total,
1005 }
1006 }
1007}
1008
1009pub fn detect_islands(graph: &ContactGraph, n_bodies: usize) -> Vec<ContactIsland> {
1017 let mut adj: Vec<Vec<(usize, ContactKey)>> = vec![Vec::new(); n_bodies];
1019 for c in graph.active_contacts() {
1020 let a = c.key.body_a;
1021 let b = c.key.body_b;
1022 if a < n_bodies && b < n_bodies {
1023 adj[a].push((b, c.key));
1024 adj[b].push((a, c.key));
1025 }
1026 }
1027
1028 let mut visited = vec![false; n_bodies];
1029 let mut islands: Vec<ContactIsland> = Vec::new();
1030
1031 for start in 0..n_bodies {
1032 if visited[start] || adj[start].is_empty() {
1033 continue;
1034 }
1035 let mut queue = std::collections::VecDeque::new();
1037 queue.push_back(start);
1038 visited[start] = true;
1039 let mut bodies = Vec::new();
1040 let mut edges: std::collections::HashSet<ContactKey> = std::collections::HashSet::new();
1041
1042 while let Some(cur) = queue.pop_front() {
1043 bodies.push(cur);
1044 for &(neighbour, key) in &adj[cur] {
1045 edges.insert(key);
1046 if !visited[neighbour] {
1047 visited[neighbour] = true;
1048 queue.push_back(neighbour);
1049 }
1050 }
1051 }
1052
1053 islands.push(ContactIsland {
1054 bodies,
1055 edges: edges.into_iter().collect(),
1056 can_sleep: false,
1057 });
1058 }
1059
1060 islands
1061}
1062
1063pub fn propagate_sleep(
1073 islands: &mut [ContactIsland],
1074 velocities_sq: &[f64],
1075 sleep_counters: &mut [u32],
1076 sleep_threshold_sq: f64,
1077 min_sleep_frames: u32,
1078) -> Vec<usize> {
1079 let mut bodies_to_sleep = Vec::new();
1080
1081 for island in islands.iter_mut() {
1082 let all_slow = island
1084 .bodies
1085 .iter()
1086 .all(|&b| velocities_sq.get(b).copied().unwrap_or(f64::INFINITY) < sleep_threshold_sq);
1087
1088 if all_slow {
1089 for &b in &island.bodies {
1091 if let Some(cnt) = sleep_counters.get_mut(b) {
1092 *cnt += 1;
1093 }
1094 }
1095 let ready = island
1096 .bodies
1097 .iter()
1098 .all(|&b| sleep_counters.get(b).copied().unwrap_or(0) >= min_sleep_frames);
1099 if ready {
1100 island.can_sleep = true;
1101 bodies_to_sleep.extend(island.bodies.iter().copied());
1102 }
1103 } else {
1104 for &b in &island.bodies {
1106 if let Some(cnt) = sleep_counters.get_mut(b) {
1107 *cnt = 0;
1108 }
1109 }
1110 island.can_sleep = false;
1111 }
1112 }
1113
1114 bodies_to_sleep
1115}
1116
1117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1121pub enum ContactFrequency {
1122 Transient,
1124 ShortLived,
1126 Persistent,
1128}
1129
1130impl ContactFrequency {
1131 pub fn classify(age: u32) -> Self {
1133 match age {
1134 0..=1 => ContactFrequency::Transient,
1135 2..=4 => ContactFrequency::ShortLived,
1136 _ => ContactFrequency::Persistent,
1137 }
1138 }
1139}
1140
1141impl ContactGraph {
1142 pub fn frequency_counts(&self) -> (usize, usize, usize) {
1146 let mut t = 0usize;
1147 let mut s = 0usize;
1148 let mut p = 0usize;
1149 for c in self.active_contacts() {
1150 match ContactFrequency::classify(c.age) {
1151 ContactFrequency::Transient => t += 1,
1152 ContactFrequency::ShortLived => s += 1,
1153 ContactFrequency::Persistent => p += 1,
1154 }
1155 }
1156 (t, s, p)
1157 }
1158
1159 pub fn persistent_contacts(&self) -> Vec<&PersistedContact> {
1161 self.active_contacts()
1162 .into_iter()
1163 .filter(|c| {
1164 matches!(
1165 ContactFrequency::classify(c.age),
1166 ContactFrequency::Persistent
1167 )
1168 })
1169 .collect()
1170 }
1171}
1172
1173pub fn manifold_quality_score(contact: &PersistedContact) -> f64 {
1179 let depth_score = (contact.depth / 0.5).clamp(0.0, 1.0);
1181 let age_score = (contact.age as f64 / 10.0).min(1.0);
1183 let impulse_score = if contact.cached_impulse.abs() > 1e-6 {
1185 0.2
1186 } else {
1187 0.0
1188 };
1189 (0.5 * depth_score + 0.3 * age_score + 0.2 * impulse_score).min(1.0)
1190}
1191
1192pub fn is_static_dynamic_bipartite(graph: &ContactGraph, is_static: &[bool]) -> bool {
1204 for c in graph.active_contacts() {
1205 let a_static = is_static.get(c.key.body_a).copied().unwrap_or(false);
1206 let b_static = is_static.get(c.key.body_b).copied().unwrap_or(false);
1207 if a_static && b_static {
1208 return false;
1209 }
1210 }
1211 true
1212}
1213
1214#[cfg(test)]
1217mod tests_extended {
1218 use super::*;
1219
1220 #[test]
1223 fn contact_point_new() {
1224 let cp = ContactPoint::new([1.0, 2.0, 3.0], 0.05);
1225 assert!((cp.depth - 0.05).abs() < 1e-12);
1226 assert!(!cp.warm_started);
1227 assert_eq!(cp.impulse, 0.0);
1228 }
1229
1230 #[test]
1233 fn contact_manifold_add_up_to_four() {
1234 let key = ContactKey::new(0, 1);
1235 let mut m = ContactManifold::new(key, [0.0, 1.0, 0.0]);
1236 for i in 0..4 {
1237 m.add_point(ContactPoint::new([i as f64, 0.0, 0.0], 0.1));
1238 }
1239 assert_eq!(m.n_points(), 4);
1240 }
1241
1242 #[test]
1243 fn contact_manifold_reduction_keeps_four() {
1244 let key = ContactKey::new(0, 1);
1245 let mut m = ContactManifold::new(key, [0.0, 1.0, 0.0]);
1246 for i in 0..5 {
1247 m.add_point(ContactPoint::new(
1248 [i as f64 * 0.1, 0.0, 0.0],
1249 (i as f64 + 1.0) * 0.01,
1250 ));
1251 }
1252 assert_eq!(m.n_points(), 4, "Should keep exactly 4 after reduction");
1253 }
1254
1255 #[test]
1256 fn contact_manifold_tick_increments_age() {
1257 let key = ContactKey::new(0, 1);
1258 let mut m = ContactManifold::new(key, [0.0, 1.0, 0.0]);
1259 m.tick();
1260 m.tick();
1261 assert_eq!(m.age, 2);
1262 }
1263
1264 #[test]
1265 fn contact_manifold_mark_inactive() {
1266 let key = ContactKey::new(0, 1);
1267 let mut m = ContactManifold::new(key, [0.0, 1.0, 0.0]);
1268 assert!(m.active);
1269 m.mark_inactive();
1270 assert!(!m.active);
1271 }
1272
1273 #[test]
1276 fn manifold_store_get_or_create() {
1277 let mut store = ManifoldStore::new();
1278 let key = ContactKey::new(0, 1);
1279 let m = store.get_or_create(key, [0.0, 1.0, 0.0]);
1280 m.add_point(ContactPoint::new([0.0; 3], 0.1));
1281 assert_eq!(store.get(&key).unwrap().n_points(), 1);
1282 }
1283
1284 #[test]
1285 fn manifold_store_purge_inactive() {
1286 let mut store = ManifoldStore::new();
1287 let k1 = ContactKey::new(0, 1);
1288 let k2 = ContactKey::new(2, 3);
1289 store.get_or_create(k1, [0.0, 1.0, 0.0]);
1290 store.get_or_create(k2, [0.0, 1.0, 0.0]);
1291 store.mark_all_inactive();
1292 store.get_or_create(k1, [0.0, 1.0, 0.0]).active = true;
1294 store.purge_inactive();
1295 assert_eq!(store.len(), 1);
1296 assert!(store.get(&k1).is_some());
1297 assert!(store.get(&k2).is_none());
1298 }
1299
1300 #[test]
1301 fn manifold_store_active_count() {
1302 let mut store = ManifoldStore::new();
1303 store
1304 .get_or_create(ContactKey::new(0, 1), [0.0, 1.0, 0.0])
1305 .active = true;
1306 store
1307 .get_or_create(ContactKey::new(2, 3), [0.0, 1.0, 0.0])
1308 .active = false;
1309 assert_eq!(store.active_manifolds().len(), 1);
1310 }
1311
1312 #[test]
1315 fn contact_frame_normal_preserved() {
1316 let f = ContactFrame::from_normal([0.0, 1.0, 0.0]);
1317 let len =
1318 (f.normal[0] * f.normal[0] + f.normal[1] * f.normal[1] + f.normal[2] * f.normal[2])
1319 .sqrt();
1320 assert!((len - 1.0).abs() < 1e-10);
1321 assert!((f.normal[1] - 1.0).abs() < 1e-10);
1322 }
1323
1324 #[test]
1325 fn contact_frame_tangents_perpendicular_to_normal() {
1326 let f = ContactFrame::from_normal([1.0, 0.0, 0.0]);
1327 let dt1n =
1328 f.tangent1[0] * f.normal[0] + f.tangent1[1] * f.normal[1] + f.tangent1[2] * f.normal[2];
1329 let dt2n =
1330 f.tangent2[0] * f.normal[0] + f.tangent2[1] * f.normal[1] + f.tangent2[2] * f.normal[2];
1331 assert!(
1332 dt1n.abs() < 1e-10,
1333 "tangent1 not perpendicular to normal: {dt1n}"
1334 );
1335 assert!(
1336 dt2n.abs() < 1e-10,
1337 "tangent2 not perpendicular to normal: {dt2n}"
1338 );
1339 }
1340
1341 #[test]
1342 fn contact_frame_project_unproject_roundtrip() {
1343 let f = ContactFrame::from_normal([0.0, 0.0, 1.0]);
1344 let v = [1.0, 2.0, 3.0];
1345 let projected = f.project(v);
1346 let recovered = f.unproject(projected);
1347 for k in 0..3 {
1348 assert!(
1349 (recovered[k] - v[k]).abs() < 1e-10,
1350 "component {k}: {} vs {}",
1351 recovered[k],
1352 v[k]
1353 );
1354 }
1355 }
1356
1357 #[test]
1360 fn invalidator_detects_moved_body() {
1361 let mut inv = ContactCacheInvalidator::new(3, 0.5);
1362 let new_pos = [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
1363 let moved = inv.update_and_check(&new_pos, 1.0);
1364 assert!(moved.contains(&0), "body 0 should be in moved list");
1366 }
1367
1368 #[test]
1369 fn invalidator_static_body_not_moved() {
1370 let mut inv = ContactCacheInvalidator::new(2, 0.5);
1371 inv.prev_positions[0] = [0.1, 0.0, 0.0];
1372 let new_pos = [[0.1, 0.0, 0.0], [0.0, 0.0, 0.0]];
1373 let moved = inv.update_and_check(&new_pos, 1.0);
1374 assert!(
1375 !moved.contains(&0),
1376 "static body should not be in moved list"
1377 );
1378 }
1379
1380 #[test]
1381 fn invalidator_removes_contacts_for_moved_body() {
1382 let mut inv = ContactCacheInvalidator::new(3, 0.1);
1383 let mut graph = ContactGraph::new();
1384 let k01 = ContactKey::new(0, 1);
1385 let k12 = ContactKey::new(1, 2);
1386 graph.update(k01, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1387 graph.update(k12, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1388
1389 let new_pos = [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
1391 let moved = inv.update_and_check(&new_pos, 1.0);
1392 inv.invalidate_contacts(&mut graph, &moved);
1393
1394 assert!(graph.get(&k01).is_none(), "contact 0-1 should be removed");
1396 assert!(graph.get(&k12).is_some(), "contact 1-2 should stay");
1397 }
1398
1399 #[test]
1402 fn triangle_area_unit() {
1403 let area = triangle_area([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1405 assert!((area - 0.5).abs() < 1e-12, "area={area}");
1406 }
1407
1408 #[test]
1409 fn triangle_area_degenerate_zero() {
1410 let area = triangle_area([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
1412 assert!(area < 1e-12, "degenerate triangle area={area}");
1413 }
1414}
1415
1416#[cfg(test)]
1419mod tests_islands {
1420 use super::*;
1421
1422 fn make_graph_chain(n: usize) -> ContactGraph {
1423 let mut g = ContactGraph::new();
1424 for i in 0..(n - 1) {
1425 g.update(ContactKey::new(i, i + 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1426 }
1427 g
1428 }
1429
1430 #[test]
1433 fn island_chain_of_four_is_one_island() {
1434 let g = make_graph_chain(4);
1435 let islands = detect_islands(&g, 4);
1436 assert_eq!(islands.len(), 1, "chain of 4 should be 1 island");
1437 assert_eq!(islands[0].bodies.len(), 4);
1438 }
1439
1440 #[test]
1441 fn island_two_separate_pairs() {
1442 let mut g = ContactGraph::new();
1443 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1444 g.update(ContactKey::new(2, 3), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1445 let islands = detect_islands(&g, 4);
1446 assert_eq!(
1447 islands.len(),
1448 2,
1449 "two disconnected pairs should give 2 islands"
1450 );
1451 }
1452
1453 #[test]
1454 fn island_empty_graph_gives_no_islands() {
1455 let g = ContactGraph::new();
1456 let islands = detect_islands(&g, 5);
1457 assert_eq!(islands.len(), 0);
1458 }
1459
1460 #[test]
1461 fn island_single_contact() {
1462 let mut g = ContactGraph::new();
1463 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.05, [0.0; 3]);
1464 let islands = detect_islands(&g, 2);
1465 assert_eq!(islands.len(), 1);
1466 assert_eq!(islands[0].edges.len(), 1);
1467 }
1468
1469 #[test]
1470 fn island_star_topology() {
1471 let mut g = ContactGraph::new();
1473 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1474 g.update(ContactKey::new(0, 2), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1475 g.update(ContactKey::new(0, 3), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1476 let islands = detect_islands(&g, 4);
1477 assert_eq!(islands.len(), 1);
1478 assert_eq!(islands[0].bodies.len(), 4);
1479 assert_eq!(islands[0].edges.len(), 3);
1480 }
1481
1482 #[test]
1483 fn island_inactive_contacts_ignored() {
1484 let mut g = ContactGraph::new();
1485 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1486 g.update(ContactKey::new(2, 3), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1487 g.mark_inactive_all();
1488 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1490 let islands = detect_islands(&g, 4);
1491 assert_eq!(islands.len(), 1, "only 1 active island");
1492 }
1493
1494 #[test]
1497 fn island_stats_empty() {
1498 let stats = IslandStats::from_islands(&[]);
1499 assert_eq!(stats.island_count, 0);
1500 assert_eq!(stats.max_size, 0);
1501 }
1502
1503 #[test]
1504 fn island_stats_two_islands() {
1505 let mut g = ContactGraph::new();
1506 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1507 g.update(ContactKey::new(2, 3), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1508 g.update(ContactKey::new(3, 4), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1509 let islands = detect_islands(&g, 5);
1510 let stats = IslandStats::from_islands(&islands);
1511 assert_eq!(stats.island_count, 2);
1512 assert_eq!(stats.max_size, 3); assert_eq!(stats.total_bodies, 5);
1514 }
1515
1516 #[test]
1519 fn sleep_propagation_slow_bodies_sleep() {
1520 let mut g = ContactGraph::new();
1521 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1522 let mut islands = detect_islands(&g, 2);
1523
1524 let vel_sq = [0.0001, 0.0001];
1526 let mut sleep_counters = [0u32; 2];
1527
1528 let mut to_sleep = Vec::new();
1530 for _ in 0..3 {
1531 to_sleep = propagate_sleep(&mut islands, &vel_sq, &mut sleep_counters, 0.01, 3);
1532 }
1533 assert!(
1534 !to_sleep.is_empty(),
1535 "bodies should be put to sleep after 3 slow frames"
1536 );
1537 }
1538
1539 #[test]
1540 fn sleep_propagation_fast_body_resets_counter() {
1541 let mut g = ContactGraph::new();
1542 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1543 let mut islands = detect_islands(&g, 2);
1544
1545 let mut sleep_counters = [5u32; 2]; let vel_sq = [1.0, 0.0001];
1548 let to_sleep = propagate_sleep(&mut islands, &vel_sq, &mut sleep_counters, 0.01, 3);
1549 assert!(to_sleep.is_empty(), "fast body should prevent sleep");
1551 assert_eq!(sleep_counters[0], 0, "counter for fast body should reset");
1552 }
1553
1554 #[test]
1557 fn contact_frequency_classify() {
1558 assert_eq!(ContactFrequency::classify(0), ContactFrequency::Transient);
1559 assert_eq!(ContactFrequency::classify(1), ContactFrequency::Transient);
1560 assert_eq!(ContactFrequency::classify(2), ContactFrequency::ShortLived);
1561 assert_eq!(ContactFrequency::classify(4), ContactFrequency::ShortLived);
1562 assert_eq!(ContactFrequency::classify(5), ContactFrequency::Persistent);
1563 assert_eq!(
1564 ContactFrequency::classify(100),
1565 ContactFrequency::Persistent
1566 );
1567 }
1568
1569 #[test]
1570 fn contact_frequency_counts() {
1571 let mut g = ContactGraph::new();
1572 let k0 = ContactKey::new(0, 1);
1573 let k1 = ContactKey::new(1, 2);
1574 let k2 = ContactKey::new(2, 3);
1575 for _ in 0..6 {
1577 g.update(k2, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1578 }
1579 g.update(k0, [0.0, 1.0, 0.0], 0.1, [0.0; 3]); g.update(k1, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1581 g.update(k1, [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1582 g.update(k1, [0.0, 1.0, 0.0], 0.1, [0.0; 3]); let (t, s, p) = g.frequency_counts();
1585 assert_eq!(t, 1, "one transient contact");
1586 assert_eq!(s, 1, "one short-lived contact");
1587 assert!(p >= 1, "at least one persistent contact");
1588 }
1589
1590 #[test]
1591 fn persistent_contacts_only_old() {
1592 let mut g = ContactGraph::new();
1593 let k_new = ContactKey::new(0, 1);
1594 let k_old = ContactKey::new(1, 2);
1595 g.update(k_new, [0.0, 1.0, 0.0], 0.1, [0.0; 3]); for _ in 0..6 {
1597 g.update(k_old, [0.0, 1.0, 0.0], 0.1, [0.0; 3]); }
1599 let persistent = g.persistent_contacts();
1600 assert_eq!(persistent.len(), 1);
1601 assert_eq!(persistent[0].key, k_old);
1602 }
1603
1604 #[test]
1607 fn quality_score_deep_old_warm_is_high() {
1608 let key = ContactKey::new(0, 1);
1609 let c = PersistedContact {
1610 key,
1611 normal: [0.0, 1.0, 0.0],
1612 depth: 0.5,
1613 contact_point: [0.0; 3],
1614 age: 20,
1615 cached_impulse: 1.0,
1616 is_active: true,
1617 };
1618 let score = manifold_quality_score(&c);
1619 assert!(
1620 score > 0.8,
1621 "deep, old, warm contact should score > 0.8, got {score}"
1622 );
1623 assert!(score <= 1.0);
1624 }
1625
1626 #[test]
1627 fn quality_score_shallow_new_is_low() {
1628 let key = ContactKey::new(0, 1);
1629 let c = PersistedContact {
1630 key,
1631 normal: [0.0, 1.0, 0.0],
1632 depth: 0.001,
1633 contact_point: [0.0; 3],
1634 age: 0,
1635 cached_impulse: 0.0,
1636 is_active: true,
1637 };
1638 let score = manifold_quality_score(&c);
1639 assert!(
1640 score < 0.2,
1641 "shallow, new, cold contact should score < 0.2, got {score}"
1642 );
1643 }
1644
1645 #[test]
1646 fn quality_score_in_unit_range() {
1647 let key = ContactKey::new(0, 1);
1648 for depth in [0.0, 0.1, 0.5, 1.0, 10.0] {
1649 for age in [0u32, 5, 20] {
1650 let c = PersistedContact {
1651 key,
1652 normal: [0.0, 1.0, 0.0],
1653 depth,
1654 contact_point: [0.0; 3],
1655 age,
1656 cached_impulse: 0.0,
1657 is_active: true,
1658 };
1659 let score = manifold_quality_score(&c);
1660 assert!((0.0..=1.0).contains(&score), "score out of range: {score}");
1661 }
1662 }
1663 }
1664
1665 #[test]
1668 fn bipartite_check_all_dynamic() {
1669 let mut g = ContactGraph::new();
1670 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1671 g.update(ContactKey::new(1, 2), [0.0, 1.0, 0.0], 0.1, [0.0; 3]);
1672 let is_static = [false, false, false];
1673 assert!(is_static_dynamic_bipartite(&g, &is_static));
1674 }
1675
1676 #[test]
1677 fn bipartite_check_dynamic_on_static() {
1678 let mut g = ContactGraph::new();
1679 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.05, [0.0; 3]);
1681 let is_static = [true, false];
1682 assert!(is_static_dynamic_bipartite(&g, &is_static));
1683 }
1684
1685 #[test]
1686 fn bipartite_check_static_static_contact_fails() {
1687 let mut g = ContactGraph::new();
1688 g.update(ContactKey::new(0, 1), [0.0, 1.0, 0.0], 0.01, [0.0; 3]);
1689 let is_static = [true, true]; assert!(!is_static_dynamic_bipartite(&g, &is_static));
1691 }
1692
1693 #[test]
1694 fn bipartite_check_empty_graph_is_bipartite() {
1695 let g = ContactGraph::new();
1696 let is_static: Vec<bool> = vec![];
1697 assert!(is_static_dynamic_bipartite(&g, &is_static));
1698 }
1699}