Skip to main content

alice_physics/
contact_cache.rs

1//! Contact Manifold Cache with Warm Starting
2//!
3//! Persistent contact manifolds that survive across frames for stable stacking
4//! and improved solver convergence. Implements warm starting by carrying over
5//! accumulated impulses (lambdas) from the previous frame.
6//!
7//! # AAA Features
8//!
9//! - **4-point manifold**: Up to 4 contact points per body pair
10//! - **Persistent contacts**: Matching via feature IDs across frames
11//! - **Warm starting**: Pre-apply previous frame's impulses for faster convergence
12//! - **Contact aging**: Auto-remove stale contacts
13
14use crate::collider::Contact;
15use crate::math::{Fix128, Vec3Fix};
16
17#[cfg(not(feature = "std"))]
18use alloc::vec::Vec;
19
20#[cfg(feature = "std")]
21use crate::det_hash::DetHashMap;
22
23/// Maximum contact points per manifold (internal cap, currently 4).
24pub(crate) const MAX_MANIFOLD_POINTS: usize = 4;
25
26/// A single cached contact point within a manifold.
27///
28/// Marked `#[non_exhaustive]` so future warm-start / analytics fields
29/// can be added without a breaking API change; construct via the internal
30/// `CachedContactPoint::new` and inspect via public fields.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32#[non_exhaustive]
33pub struct CachedContactPoint {
34    /// Contact point on body A (local space)
35    pub local_point_a: Vec3Fix,
36    /// Contact point on body B (local space)
37    pub local_point_b: Vec3Fix,
38    /// Contact normal (world space, pointing from B to A — the
39    /// [`crate::collider::Contact::normal`] convention it is copied from)
40    pub normal: Vec3Fix,
41    /// Penetration depth
42    pub depth: Fix128,
43    /// Accumulated normal impulse (for warm starting)
44    pub lambda_n: Fix128,
45    /// Accumulated tangent impulse X (for warm starting)
46    pub lambda_t1: Fix128,
47    /// Accumulated tangent impulse Y (for warm starting)
48    pub lambda_t2: Fix128,
49    /// Number of frames this contact has persisted
50    pub age: u32,
51}
52
53impl CachedContactPoint {
54    /// Create a new cached contact point
55    #[must_use]
56    pub const fn new(local_a: Vec3Fix, local_b: Vec3Fix, normal: Vec3Fix, depth: Fix128) -> Self {
57        Self {
58            local_point_a: local_a,
59            local_point_b: local_b,
60            normal,
61            depth,
62            lambda_n: Fix128::ZERO,
63            lambda_t1: Fix128::ZERO,
64            lambda_t2: Fix128::ZERO,
65            age: 0,
66        }
67    }
68}
69
70/// Body pair key for manifold lookup
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
72pub struct BodyPairKey {
73    /// Index of body A (always the smaller index)
74    pub body_a: u32,
75    /// Index of body B (always the larger index)
76    pub body_b: u32,
77}
78
79impl BodyPairKey {
80    /// Create a canonical body pair key (ensures a < b)
81    #[inline]
82    #[must_use]
83    pub const fn new(a: usize, b: usize) -> Self {
84        if a < b {
85            Self {
86                body_a: a as u32,
87                body_b: b as u32,
88            }
89        } else {
90            Self {
91                body_a: b as u32,
92                body_b: a as u32,
93            }
94        }
95    }
96}
97
98/// Contact manifold: up to 4 persistent contact points between two bodies.
99///
100/// Marked `#[non_exhaustive]` so additional accumulator / diagnostic
101/// fields can be added post-v1.0 without a breaking API change.
102#[derive(Clone, Debug)]
103#[non_exhaustive]
104pub struct ContactManifold {
105    /// Body pair this manifold belongs to
106    pub pair: BodyPairKey,
107    /// Active contact points (up to 4 per manifold).
108    pub points: Vec<CachedContactPoint>,
109    /// Shared normal direction (average of point normals, pointing from B to A)
110    pub normal: Vec3Fix,
111    /// Friction coefficient for this pair
112    pub friction: Fix128,
113    /// Restitution coefficient for this pair
114    pub restitution: Fix128,
115    /// Number of frames since last update (for expiry)
116    pub stale_frames: u32,
117}
118
119impl ContactManifold {
120    /// Create a new empty manifold
121    #[must_use]
122    pub fn new(pair: BodyPairKey, friction: Fix128, restitution: Fix128) -> Self {
123        Self {
124            pair,
125            points: Vec::with_capacity(MAX_MANIFOLD_POINTS),
126            normal: Vec3Fix::ZERO,
127            friction,
128            restitution,
129            stale_frames: 0,
130        }
131    }
132
133    /// Add or update a contact point in the manifold
134    ///
135    /// If a matching point exists (within threshold), update it and preserve lambdas.
136    /// Otherwise, add as new. If full (4 points), replace the shallowest.
137    pub fn add_or_update(&mut self, contact: &Contact, local_a: Vec3Fix, local_b: Vec3Fix) {
138        // Squared distance threshold for contact matching.
139        // 0.0001 = (0.01m)^2, matches contacts within 1cm.
140        let threshold_sq = Fix128::from_ratio(1, 10000);
141
142        // Try to find matching existing point
143        let mut best_match: Option<usize> = None;
144        let mut best_dist_sq = threshold_sq;
145
146        for (i, existing) in self.points.iter().enumerate() {
147            let dist_sq = (existing.local_point_a - local_a).length_squared();
148            if dist_sq < best_dist_sq {
149                best_dist_sq = dist_sq;
150                best_match = Some(i);
151            }
152        }
153
154        if let Some(idx) = best_match {
155            // Update existing point — preserve accumulated impulses (warm starting)
156            let lambda_n = self.points[idx].lambda_n;
157            let lambda_t1 = self.points[idx].lambda_t1;
158            let lambda_t2 = self.points[idx].lambda_t2;
159            let age = self.points[idx].age;
160
161            self.points[idx] =
162                CachedContactPoint::new(local_a, local_b, contact.normal, contact.depth);
163            self.points[idx].lambda_n = lambda_n;
164            self.points[idx].lambda_t1 = lambda_t1;
165            self.points[idx].lambda_t2 = lambda_t2;
166            self.points[idx].age = age + 1;
167        } else if self.points.len() < MAX_MANIFOLD_POINTS {
168            // Add new point
169            self.points.push(CachedContactPoint::new(
170                local_a,
171                local_b,
172                contact.normal,
173                contact.depth,
174            ));
175        } else {
176            // Replace shallowest point
177            let mut shallowest_idx = 0;
178            let mut shallowest_depth = self.points[0].depth;
179            for (i, p) in self.points.iter().enumerate().skip(1) {
180                if p.depth < shallowest_depth {
181                    shallowest_depth = p.depth;
182                    shallowest_idx = i;
183                }
184            }
185            if contact.depth > shallowest_depth {
186                self.points[shallowest_idx] =
187                    CachedContactPoint::new(local_a, local_b, contact.normal, contact.depth);
188            }
189        }
190
191        // Update shared normal
192        self.update_normal();
193        self.stale_frames = 0;
194    }
195
196    /// Update the shared normal (average of point normals)
197    fn update_normal(&mut self) {
198        if self.points.is_empty() {
199            self.normal = Vec3Fix::ZERO;
200            return;
201        }
202
203        let mut sum = Vec3Fix::ZERO;
204        for p in &self.points {
205            sum = sum + p.normal;
206        }
207        let len = sum.length();
208        if !len.is_zero() {
209            self.normal = sum / len;
210        }
211    }
212
213    /// Number of active contact points
214    #[inline]
215    #[must_use]
216    pub fn point_count(&self) -> usize {
217        self.points.len()
218    }
219
220    /// Check if this manifold is empty
221    #[inline]
222    #[must_use]
223    pub fn is_empty(&self) -> bool {
224        self.points.is_empty()
225    }
226
227    /// Clear all points (reset manifold)
228    pub fn clear(&mut self) {
229        self.points.clear();
230        self.normal = Vec3Fix::ZERO;
231    }
232
233    /// Get warm-start impulse for a contact point
234    #[must_use]
235    pub fn warm_start_impulse(&self, point_idx: usize) -> (Fix128, Fix128, Fix128) {
236        if point_idx < self.points.len() {
237            let p = &self.points[point_idx];
238            (p.lambda_n, p.lambda_t1, p.lambda_t2)
239        } else {
240            (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO)
241        }
242    }
243
244    /// Store solver impulses back into the cache
245    pub fn store_impulses(
246        &mut self,
247        point_idx: usize,
248        lambda_n: Fix128,
249        lambda_t1: Fix128,
250        lambda_t2: Fix128,
251    ) {
252        if point_idx < self.points.len() {
253            self.points[point_idx].lambda_n = lambda_n;
254            self.points[point_idx].lambda_t1 = lambda_t1;
255            self.points[point_idx].lambda_t2 = lambda_t2;
256        }
257    }
258}
259
260/// Contact cache: stores all active manifolds across frames
261pub struct ContactCache {
262    /// All active manifolds
263    pub manifolds: Vec<ContactManifold>,
264    /// `HashMap` index for O(1) manifold lookup (std feature only)
265    #[cfg(feature = "std")]
266    pub(crate) pair_index: DetHashMap<BodyPairKey, usize>,
267    /// Maximum stale frames before manifold is removed
268    pub max_stale_frames: u32,
269    /// Warm starting factor (0.0 = off, 1.0 = full warm start)
270    pub warm_start_factor: Fix128,
271}
272
273impl ContactCache {
274    /// Create a new contact cache
275    #[must_use]
276    pub fn new() -> Self {
277        Self {
278            manifolds: Vec::new(),
279            #[cfg(feature = "std")]
280            pair_index: DetHashMap::default(),
281            max_stale_frames: 3,
282            warm_start_factor: Fix128::from_ratio(8, 10), // 0.8 default
283        }
284    }
285
286    /// Find or create manifold for a body pair
287    ///
288    /// # Panics
289    ///
290    /// Panics if the internal manifold `Vec` is empty after a push (should never happen).
291    pub fn get_or_create(
292        &mut self,
293        pair: BodyPairKey,
294        friction: Fix128,
295        restitution: Fix128,
296    ) -> &mut ContactManifold {
297        // Find existing using HashMap (O(1)) with std feature, or linear scan (O(n)) without
298        //
299        // `manifolds` is `pub`, so an external `clear()`/`retain()`/`remove()` on it
300        // can leave `pair_index` pointing past the end or at another pair's slot;
301        // `locate` checks the slot and falls back to a scan, and a hit found by
302        // the scan is re-indexed here.
303        let pos = self.locate(&pair);
304        #[cfg(feature = "std")]
305        if let Some(idx) = pos {
306            self.pair_index.insert(pair, idx);
307        }
308
309        if let Some(idx) = pos {
310            &mut self.manifolds[idx]
311        } else {
312            #[cfg(feature = "std")]
313            {
314                let idx = self.manifolds.len();
315                self.manifolds
316                    .push(ContactManifold::new(pair, friction, restitution));
317                self.pair_index.insert(pair, idx);
318                self.manifolds
319                    .last_mut()
320                    .expect("just pushed, cannot be empty")
321            }
322            #[cfg(not(feature = "std"))]
323            {
324                self.manifolds
325                    .push(ContactManifold::new(pair, friction, restitution));
326                self.manifolds
327                    .last_mut()
328                    .expect("just pushed, cannot be empty")
329            }
330        }
331    }
332
333    /// Find manifold for a body pair (read-only)
334    ///
335    /// `manifolds` is `pub`, so an external `clear()`/`retain()`/`remove()`
336    /// on it can leave the private `pair_index` pointing past the end or at
337    /// another pair's slot; the slot is checked, so a stale index neither
338    /// panics nor answers another pair.
339    #[must_use]
340    pub fn find(&self, pair: &BodyPairKey) -> Option<&ContactManifold> {
341        self.locate(pair).map(|idx| &self.manifolds[idx])
342    }
343
344    /// Slot of the manifold for `pair`. With `std` the pair index answers in
345    /// O(1): a pair it does not hold has no manifold (the miss stays O(1), so
346    /// creating new pairs stays linear per frame), and an entry left stale by a
347    /// direct edit of `manifolds` (past the end, or shifted onto another
348    /// pair's slot) falls back to the O(n) scan, so another pair's manifold is
349    /// never answered. Without `std` it is the scan.
350    fn locate(&self, pair: &BodyPairKey) -> Option<usize> {
351        #[cfg(feature = "std")]
352        {
353            let idx = *self.pair_index.get(pair)?;
354            if self.manifolds.get(idx).is_some_and(|m| m.pair == *pair) {
355                return Some(idx);
356            }
357        }
358        self.manifolds.iter().position(|m| m.pair == *pair)
359    }
360
361    /// Mark all manifolds as potentially stale (call at start of frame)
362    pub fn begin_frame(&mut self) {
363        for manifold in &mut self.manifolds {
364            manifold.stale_frames += 1;
365        }
366    }
367
368    /// Remove expired manifolds (call at end of frame)
369    pub fn end_frame(&mut self) {
370        let max_stale = self.max_stale_frames;
371        self.manifolds.retain(|m| m.stale_frames <= max_stale);
372
373        // Rebuild the HashMap index after retain (std feature only)
374        #[cfg(feature = "std")]
375        {
376            self.pair_index.clear();
377            for (idx, manifold) in self.manifolds.iter().enumerate() {
378                self.pair_index.insert(manifold.pair, idx);
379            }
380        }
381    }
382
383    /// Total number of active manifolds
384    #[inline]
385    #[must_use]
386    pub fn manifold_count(&self) -> usize {
387        self.manifolds.len()
388    }
389
390    /// Total number of active contact points across all manifolds
391    pub fn total_contact_points(&self) -> usize {
392        self.manifolds
393            .iter()
394            .map(ContactManifold::point_count)
395            .sum()
396    }
397
398    /// Clear all manifolds
399    pub fn clear(&mut self) {
400        self.manifolds.clear();
401        #[cfg(feature = "std")]
402        self.pair_index.clear();
403    }
404
405    /// Apply warm starting impulses to bodies
406    ///
407    /// Pre-applies accumulated impulses from the previous frame's solution,
408    /// scaled by `warm_start_factor`. This dramatically improves convergence.
409    pub fn apply_warm_start(&self, bodies: &mut [crate::solver::RigidBody]) {
410        let factor = self.warm_start_factor;
411
412        for manifold in &self.manifolds {
413            let a_idx = manifold.pair.body_a as usize;
414            let b_idx = manifold.pair.body_b as usize;
415
416            if a_idx >= bodies.len() || b_idx >= bodies.len() {
417                continue;
418            }
419
420            for point in &manifold.points {
421                let impulse_n = manifold.normal * (point.lambda_n * factor);
422
423                // Build tangent frame
424                let (t1, t2) = tangent_frame(manifold.normal);
425                let impulse_t = t1 * (point.lambda_t1 * factor) + t2 * (point.lambda_t2 * factor);
426
427                let total_impulse = impulse_n + impulse_t;
428
429                if !bodies[a_idx].inv_mass.is_zero() {
430                    bodies[a_idx].velocity =
431                        bodies[a_idx].velocity + total_impulse * bodies[a_idx].inv_mass;
432                }
433                if !bodies[b_idx].inv_mass.is_zero() {
434                    bodies[b_idx].velocity =
435                        bodies[b_idx].velocity - total_impulse * bodies[b_idx].inv_mass;
436                }
437            }
438        }
439    }
440}
441
442impl Default for ContactCache {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448impl core::fmt::Debug for ContactCache {
449    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
450        let mut s = f.debug_struct("ContactCache");
451        s.field(
452            "manifolds",
453            &format_args!("[{} items]", self.manifolds.len()),
454        );
455        #[cfg(feature = "std")]
456        s.field(
457            "pair_index",
458            &format_args!("[{} items]", self.pair_index.len()),
459        );
460        s.field("max_stale_frames", &self.max_stale_frames);
461        s.field("warm_start_factor", &self.warm_start_factor);
462        s.finish()
463    }
464}
465
466/// Build orthonormal tangent frame from a normal vector (crate-internal helper).
467#[must_use]
468pub(crate) fn tangent_frame(normal: Vec3Fix) -> (Vec3Fix, Vec3Fix) {
469    // Pick axis least parallel to normal
470    let abs_x = normal.x.abs();
471    let abs_y = normal.y.abs();
472    let abs_z = normal.z.abs();
473
474    let reference = if abs_x <= abs_y && abs_x <= abs_z {
475        Vec3Fix::UNIT_X
476    } else if abs_y <= abs_z {
477        Vec3Fix::UNIT_Y
478    } else {
479        Vec3Fix::UNIT_Z
480    };
481
482    let t1 = normal.cross(reference).normalize();
483    let t2 = normal.cross(t1);
484    (t1, t2)
485}
486
487#[cfg(all(test, feature = "std"))]
488mod tests {
489    use super::*;
490    use crate::solver::RigidBody;
491
492    #[test]
493    fn test_body_pair_key_canonical() {
494        let k1 = BodyPairKey::new(3, 7);
495        let k2 = BodyPairKey::new(7, 3);
496        assert_eq!(k1, k2);
497        assert_eq!(k1.body_a, 3);
498        assert_eq!(k1.body_b, 7);
499    }
500
501    #[test]
502    fn test_manifold_add_point() {
503        let pair = BodyPairKey::new(0, 1);
504        let mut manifold =
505            ContactManifold::new(pair, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
506
507        let contact = Contact {
508            depth: Fix128::from_ratio(1, 10),
509            normal: Vec3Fix::UNIT_Y,
510            point_a: Vec3Fix::ZERO,
511            point_b: Vec3Fix::ZERO,
512        };
513
514        manifold.add_or_update(&contact, Vec3Fix::ZERO, Vec3Fix::ZERO);
515        assert_eq!(manifold.point_count(), 1);
516    }
517
518    #[test]
519    fn test_manifold_max_points() {
520        let pair = BodyPairKey::new(0, 1);
521        let mut manifold =
522            ContactManifold::new(pair, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
523
524        // Add 5 distinct points — should cap at 4
525        for i in 0..5 {
526            let contact = Contact {
527                depth: Fix128::from_int(i as i64 + 1),
528                normal: Vec3Fix::UNIT_Y,
529                point_a: Vec3Fix::from_int(i as i64 * 10, 0, 0),
530                point_b: Vec3Fix::from_int(i as i64 * 10, 0, 0),
531            };
532            manifold.add_or_update(
533                &contact,
534                Vec3Fix::from_int(i as i64 * 10, 0, 0),
535                Vec3Fix::from_int(i as i64 * 10, 0, 0),
536            );
537        }
538
539        assert!(manifold.point_count() <= MAX_MANIFOLD_POINTS);
540    }
541
542    #[test]
543    fn test_manifold_warm_start_preserved() {
544        let pair = BodyPairKey::new(0, 1);
545        let mut manifold =
546            ContactManifold::new(pair, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
547
548        let contact = Contact {
549            depth: Fix128::from_ratio(1, 10),
550            normal: Vec3Fix::UNIT_Y,
551            point_a: Vec3Fix::ZERO,
552            point_b: Vec3Fix::ZERO,
553        };
554        manifold.add_or_update(&contact, Vec3Fix::ZERO, Vec3Fix::ZERO);
555
556        // Store impulses
557        manifold.store_impulses(0, Fix128::from_int(5), Fix128::ONE, Fix128::ONE);
558
559        // Update same contact point — impulses should be preserved
560        manifold.add_or_update(&contact, Vec3Fix::ZERO, Vec3Fix::ZERO);
561        let (ln, lt1, lt2) = manifold.warm_start_impulse(0);
562        assert_eq!(ln.hi, 5);
563        assert_eq!(lt1.hi, 1);
564        assert_eq!(lt2.hi, 1);
565    }
566
567    #[test]
568    fn test_contact_cache_lifecycle() {
569        let mut cache = ContactCache::new();
570
571        let pair = BodyPairKey::new(0, 1);
572        {
573            let manifold =
574                cache.get_or_create(pair, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
575            let contact = Contact {
576                depth: Fix128::from_ratio(1, 10),
577                normal: Vec3Fix::UNIT_Y,
578                point_a: Vec3Fix::ZERO,
579                point_b: Vec3Fix::ZERO,
580            };
581            manifold.add_or_update(&contact, Vec3Fix::ZERO, Vec3Fix::ZERO);
582        }
583
584        assert_eq!(cache.manifold_count(), 1);
585        assert_eq!(cache.total_contact_points(), 1);
586
587        // Simulate stale frames — should expire after max_stale_frames
588        for _ in 0..5 {
589            cache.begin_frame();
590            cache.end_frame();
591        }
592
593        assert_eq!(cache.manifold_count(), 0);
594    }
595
596    #[test]
597    fn test_tangent_frame() {
598        let (t1, t2) = tangent_frame(Vec3Fix::UNIT_Y);
599
600        // t1 and t2 should be perpendicular to normal and each other
601        let dot_n_t1 = Vec3Fix::UNIT_Y.dot(t1);
602        let dot_n_t2 = Vec3Fix::UNIT_Y.dot(t2);
603        let dot_t1_t2 = t1.dot(t2);
604
605        assert!(dot_n_t1.abs() < Fix128::from_ratio(1, 100));
606        assert!(dot_n_t2.abs() < Fix128::from_ratio(1, 100));
607        assert!(dot_t1_t2.abs() < Fix128::from_ratio(1, 100));
608    }
609
610    #[test]
611    fn test_warm_start_application() {
612        let mut cache = ContactCache::new();
613        let pair = BodyPairKey::new(0, 1);
614
615        {
616            let manifold =
617                cache.get_or_create(pair, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
618            let contact = Contact {
619                depth: Fix128::from_ratio(1, 10),
620                normal: Vec3Fix::UNIT_Y,
621                point_a: Vec3Fix::ZERO,
622                point_b: Vec3Fix::ZERO,
623            };
624            manifold.add_or_update(&contact, Vec3Fix::ZERO, Vec3Fix::ZERO);
625            manifold.store_impulses(0, Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO);
626        }
627
628        let mut bodies = vec![
629            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
630            RigidBody::new_dynamic(Vec3Fix::from_int(0, 1, 0), Fix128::ONE),
631        ];
632
633        cache.apply_warm_start(&mut bodies);
634
635        // Body 0 should have gained upward velocity, body 1 downward
636        assert!(bodies[0].velocity.y > Fix128::ZERO);
637        assert!(bodies[1].velocity.y < Fix128::ZERO);
638    }
639
640    // ---- mutation-score tests (2026-09-15) ----------------------------
641
642    fn fi(n: i64) -> Fix128 {
643        Fix128::from_int(n)
644    }
645
646    fn v3i(x: i64, y: i64, z: i64) -> Vec3Fix {
647        Vec3Fix::from_int(x, y, z)
648    }
649
650    fn contact(normal: Vec3Fix, depth: Fix128) -> Contact {
651        Contact {
652            depth,
653            normal,
654            point_a: Vec3Fix::ZERO,
655            point_b: Vec3Fix::ZERO,
656        }
657    }
658
659    fn manifold() -> ContactManifold {
660        ContactManifold::new(
661            BodyPairKey::new(0, 1),
662            Fix128::from_ratio(1, 2),
663            Fix128::ZERO,
664        )
665    }
666
667    #[test]
668    fn manifold_add_matches_within_1cm_and_preserves_impulses() {
669        let mut m = manifold();
670        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(1)), v3i(1, 0, 0), v3i(1, 1, 0));
671        assert_eq!(m.point_count(), 1);
672        assert!(!m.is_empty());
673        m.store_impulses(0, fi(5), fi(6), fi(7));
674        // 0.5 cm ずれた点 = 同一点として更新 (impulse 保持、age +1、depth 更新)
675        let near = Vec3Fix::new(
676            Fix128::ONE + Fix128::from_ratio(1, 200),
677            Fix128::ZERO,
678            Fix128::ZERO,
679        );
680        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(2)), near, v3i(1, 1, 0));
681        assert_eq!(m.point_count(), 1);
682        assert_eq!(m.warm_start_impulse(0), (fi(5), fi(6), fi(7)));
683        assert_eq!(m.points[0].age, 1);
684        assert_eq!(m.points[0].depth, fi(2));
685        assert_eq!(m.points[0].local_point_a, near);
686        // 更新後の点 (1.005) から 2 cm 離れた点は新規点 (1 cm 閾値の外)
687        let edge = Vec3Fix::new(
688            Fix128::ONE + Fix128::from_ratio(5, 200),
689            Fix128::ZERO,
690            Fix128::ZERO,
691        );
692        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(1)), edge, v3i(1, 1, 0));
693        assert_eq!(m.point_count(), 2);
694        assert_eq!(
695            m.warm_start_impulse(1),
696            (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO)
697        );
698        // 範囲外 index は 0、store は無視
699        assert_eq!(
700            m.warm_start_impulse(9),
701            (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO)
702        );
703        m.store_impulses(9, fi(1), fi(1), fi(1));
704        assert_eq!(m.point_count(), 2);
705        m.clear();
706        assert!(m.is_empty());
707        assert_eq!(m.point_count(), 0);
708        assert_eq!(m.normal, Vec3Fix::ZERO);
709    }
710
711    #[test]
712    fn manifold_picks_nearest_of_several_candidates() {
713        let mut m = manifold();
714        m.add_or_update(
715            &contact(Vec3Fix::UNIT_Y, fi(1)),
716            v3i(0, 0, 0),
717            Vec3Fix::ZERO,
718        );
719        m.add_or_update(
720            &contact(Vec3Fix::UNIT_Y, fi(1)),
721            v3i(5, 0, 0),
722            Vec3Fix::ZERO,
723        );
724        m.store_impulses(0, fi(10), Fix128::ZERO, Fix128::ZERO);
725        m.store_impulses(1, fi(20), Fix128::ZERO, Fix128::ZERO);
726        // (5.004, 0, 0) は点 1 に一致 → 点 1 の impulse 20 が残り、点 0 は不変
727        let p = Vec3Fix::new(
728            fi(5) + Fix128::from_ratio(1, 250),
729            Fix128::ZERO,
730            Fix128::ZERO,
731        );
732        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(3)), p, Vec3Fix::ZERO);
733        assert_eq!(m.point_count(), 2);
734        assert_eq!(m.warm_start_impulse(1).0, fi(20));
735        assert_eq!(m.points[1].depth, fi(3));
736        assert_eq!(m.points[0].depth, fi(1));
737    }
738
739    #[test]
740    fn manifold_full_replaces_shallowest_only_if_deeper() {
741        let mut m = manifold();
742        for (i, d) in [3, 1, 4, 2].iter().enumerate() {
743            m.add_or_update(
744                &contact(Vec3Fix::UNIT_Y, fi(*d)),
745                v3i(i as i64 * 10, 0, 0),
746                Vec3Fix::ZERO,
747            );
748        }
749        assert_eq!(m.point_count(), MAX_MANIFOLD_POINTS);
750        // 5 点目 depth 0.5 < 最浅 1 → 追加されない
751        m.add_or_update(
752            &contact(Vec3Fix::UNIT_Y, Fix128::from_ratio(1, 2)),
753            v3i(100, 0, 0),
754            Vec3Fix::ZERO,
755        );
756        assert_eq!(m.point_count(), 4);
757        assert!(m.points.iter().all(|p| p.local_point_a.x != fi(100)));
758        // depth 1 == 最浅 1 → `>` は false → 追加されない
759        m.add_or_update(
760            &contact(Vec3Fix::UNIT_Y, fi(1)),
761            v3i(100, 0, 0),
762            Vec3Fix::ZERO,
763        );
764        assert!(m.points.iter().all(|p| p.local_point_a.x != fi(100)));
765        // depth 2.5 > 最浅 1 (index 1) → index 1 が置換される
766        m.add_or_update(
767            &contact(Vec3Fix::UNIT_Y, Fix128::from_ratio(5, 2)),
768            v3i(100, 0, 0),
769            Vec3Fix::ZERO,
770        );
771        assert_eq!(m.point_count(), 4);
772        assert_eq!(m.points[1].local_point_a, v3i(100, 0, 0));
773        assert_eq!(m.points[1].depth, Fix128::from_ratio(5, 2));
774        let depths: Vec<Fix128> = m.points.iter().map(|p| p.depth).collect();
775        assert_eq!(depths, vec![fi(3), Fix128::from_ratio(5, 2), fi(4), fi(2)]);
776    }
777
778    #[test]
779    fn manifold_normal_is_normalized_average_of_point_normals() {
780        let mut m = manifold();
781        m.add_or_update(&contact(v3i(3, 0, 0), fi(1)), v3i(0, 0, 0), Vec3Fix::ZERO);
782        assert_eq!(m.normal, Vec3Fix::UNIT_X); // (3,0,0)/3
783        m.add_or_update(&contact(v3i(0, 4, 0), fi(1)), v3i(10, 0, 0), Vec3Fix::ZERO);
784        // sum (3,4,0)、len 5 → (0.6, 0.8, 0)
785        assert_eq!(
786            m.normal,
787            Vec3Fix::new(fi(3) / fi(5), fi(4) / fi(5), Fix128::ZERO)
788        );
789        // 打ち消し合う normal → len 0 → 直前の normal を維持
790        let before = m.normal;
791        m.add_or_update(
792            &contact(v3i(-3, -4, 0), fi(1)),
793            v3i(20, 0, 0),
794            Vec3Fix::ZERO,
795        );
796        assert_eq!(m.normal, before);
797        assert_eq!(m.stale_frames, 0);
798    }
799
800    #[test]
801    fn cache_get_or_create_find_frames_and_counts() {
802        let mut cache = ContactCache::new();
803        let k01 = BodyPairKey::new(0, 1);
804        let k23 = BodyPairKey::new(2, 3);
805        cache
806            .get_or_create(k01, Fix128::ONE, Fix128::ZERO)
807            .add_or_update(
808                &contact(Vec3Fix::UNIT_Y, fi(1)),
809                Vec3Fix::ZERO,
810                Vec3Fix::ZERO,
811            );
812        cache
813            .get_or_create(k01, Fix128::ONE, Fix128::ZERO)
814            .add_or_update(
815                &contact(Vec3Fix::UNIT_Y, fi(1)),
816                v3i(5, 0, 0),
817                Vec3Fix::ZERO,
818            );
819        cache
820            .get_or_create(k23, Fix128::ONE, Fix128::ZERO)
821            .add_or_update(
822                &contact(Vec3Fix::UNIT_Y, fi(1)),
823                Vec3Fix::ZERO,
824                Vec3Fix::ZERO,
825            );
826        assert_eq!(cache.manifold_count(), 2, "同じ pair は再利用");
827        assert_eq!(cache.total_contact_points(), 3);
828        assert_eq!(cache.find(&k01).map(ContactManifold::point_count), Some(2));
829        assert_eq!(cache.find(&k23).map(ContactManifold::point_count), Some(1));
830        assert!(cache.find(&BodyPairKey::new(4, 5)).is_none());
831        // 順序違いの key は同一 (正規化)
832        assert_eq!(BodyPairKey::new(1, 0), k01);
833        assert!(cache.find(&BodyPairKey::new(1, 0)).is_some());
834        // stale: begin_frame で +1、max 3 を超えると end_frame で落ちる (k23 だけ touch し続ける)
835        for _ in 0..3 {
836            cache.begin_frame();
837            cache
838                .get_or_create(k23, Fix128::ONE, Fix128::ZERO)
839                .add_or_update(
840                    &contact(Vec3Fix::UNIT_Y, fi(1)),
841                    Vec3Fix::ZERO,
842                    Vec3Fix::ZERO,
843                );
844            cache.end_frame();
845        }
846        assert_eq!(cache.manifold_count(), 2, "stale 3 == max 3 は残る");
847        cache.begin_frame();
848        cache.end_frame();
849        assert_eq!(cache.manifold_count(), 1, "stale 4 > 3 で落ちる");
850        assert!(cache.find(&k01).is_none());
851        assert!(cache.find(&k23).is_some(), "index は再構築される");
852        assert_eq!(cache.total_contact_points(), 1);
853        cache.clear();
854        assert_eq!(cache.manifold_count(), 0);
855        assert_eq!(cache.total_contact_points(), 0);
856        assert!(cache.find(&k23).is_none());
857        let dbg = format!("{cache:?}");
858        assert!(
859            dbg.contains("ContactCache") && dbg.contains("max_stale_frames: 3"),
860            "{dbg}"
861        );
862    }
863
864    #[test]
865    fn apply_warm_start_applies_scaled_cached_impulses() {
866        let mut cache = ContactCache::new();
867        cache.warm_start_factor = Fix128::from_ratio(1, 2);
868        let m = cache.get_or_create(BodyPairKey::new(0, 1), Fix128::ONE, Fix128::ZERO);
869        m.add_or_update(
870            &contact(Vec3Fix::UNIT_Z, fi(1)),
871            Vec3Fix::ZERO,
872            Vec3Fix::ZERO,
873        );
874        // normal z: tangent_frame(z) = (z × x = y, z × y = -x) → t1 = (0,1,0), t2 = (-1,0,0)
875        m.store_impulses(0, fi(4), fi(2), fi(6));
876        let mut bodies = vec![
877            crate::solver::RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
878            crate::solver::RigidBody::new_dynamic(v3i(1, 0, 0), Fix128::ONE),
879        ];
880        bodies[1].inv_mass = fi(3);
881        cache.apply_warm_start(&mut bodies);
882        // total = z*(4*0.5) + y*(2*0.5) + (-x)*(6*0.5) = (-3, 1, 2)、A += total*1、B -= total*3
883        assert_eq!(bodies[0].velocity, v3i(-3, 1, 2));
884        assert_eq!(bodies[1].velocity, v3i(9, -3, -6));
885        // static 側は不変、body index 範囲外の manifold は skip
886        let mut st = vec![
887            crate::solver::RigidBody::new_static(Vec3Fix::ZERO),
888            crate::solver::RigidBody::new_dynamic(v3i(1, 0, 0), Fix128::ONE),
889        ];
890        cache.apply_warm_start(&mut st);
891        assert_eq!(st[0].velocity, Vec3Fix::ZERO);
892        assert_eq!(st[1].velocity, v3i(3, -1, -2));
893        let mut short = vec![crate::solver::RigidBody::new_dynamic(
894            Vec3Fix::ZERO,
895            Fix128::ONE,
896        )];
897        cache.apply_warm_start(&mut short);
898        assert_eq!(short[0].velocity, Vec3Fix::ZERO);
899    }
900
901    #[test]
902    fn tangent_frame_picks_least_parallel_axis_and_is_orthonormal() {
903        // normal = x → reference は y (abs_x が最小でない、abs_y <= abs_z) → t1 = x × y = z、t2 = x × z = -y
904        assert_eq!(
905            tangent_frame(Vec3Fix::UNIT_X),
906            (Vec3Fix::UNIT_Z, -Vec3Fix::UNIT_Y)
907        );
908        // normal = y → reference x (abs_x <= abs_y && abs_x <= abs_z) → t1 = y × x = -z、t2 = y × -z = -x
909        assert_eq!(
910            tangent_frame(Vec3Fix::UNIT_Y),
911            (-Vec3Fix::UNIT_Z, -Vec3Fix::UNIT_X)
912        );
913        // normal = z → reference x → t1 = z × x = y、t2 = z × y = -x
914        assert_eq!(
915            tangent_frame(Vec3Fix::UNIT_Z),
916            (Vec3Fix::UNIT_Y, -Vec3Fix::UNIT_X)
917        );
918        // 同率 (abs_x == abs_y == abs_z): `<=` で x が選ばれる
919        let d = Vec3Fix::new(Fix128::ONE, Fix128::ONE, Fix128::ONE);
920        let (t1, t2) = tangent_frame(d);
921        let expect_t1 = d.cross(Vec3Fix::UNIT_X).normalize();
922        assert_eq!(t1, expect_t1);
923        assert_eq!(t2, d.cross(t1));
924        // 一般 normal: t1 ⊥ n、t2 ⊥ n、t1 ⊥ t2 (数 ulp)
925        let n = Vec3Fix::new(
926            Fix128::from_ratio(1, 3),
927            Fix128::from_ratio(-2, 3),
928            Fix128::from_ratio(2, 3),
929        );
930        let (t1, t2) = tangent_frame(n);
931        let small = Fix128 { hi: 0, lo: 1 << 24 };
932        assert!(n.dot(t1).abs() < small && n.dot(t2).abs() < small && t1.dot(t2).abs() < small);
933        assert!((t1.length() - Fix128::ONE).abs() < small);
934    }
935
936    #[test]
937    fn apply_warm_start_factor_scales_normal_and_both_tangents() {
938        let mut cache = ContactCache::new();
939        cache.warm_start_factor = Fix128::from_ratio(1, 4);
940        let m = cache.get_or_create(BodyPairKey::new(0, 1), Fix128::ONE, Fix128::ZERO);
941        m.add_or_update(
942            &contact(Vec3Fix::UNIT_X, fi(1)),
943            Vec3Fix::ZERO,
944            Vec3Fix::ZERO,
945        );
946        // normal x: t1 = x × y = z、t2 = x × z = -y
947        m.store_impulses(0, fi(8), fi(12), fi(20));
948        let mut bodies = vec![
949            crate::solver::RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
950            crate::solver::RigidBody::new_static(v3i(1, 0, 0)),
951        ];
952        cache.apply_warm_start(&mut bodies);
953        // total = x*2 + z*3 + (-y)*5 = (2, -5, 3)
954        assert_eq!(bodies[0].velocity, v3i(2, -5, 3));
955        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
956    }
957
958    // ---- mutation-score tests batch 7 (2026-09-15、cargo-mutants missed 分) ----
959
960    /// `dist_sq < best_dist_sq` (厳密 less-than): 2 点と等距離の新規点は
961    /// 先に見つかった index 0 に一致する (`<=` なら後勝ちで index 1 が更新される)
962    #[test]
963    fn manifold_match_tie_prefers_first_candidate() {
964        let mut m = manifold();
965        // A at 0、B at 1/64 (= 1.56 cm 離れ → 1 cm 閾値の外、別点として登録)
966        m.add_or_update(
967            &contact(Vec3Fix::UNIT_Y, fi(1)),
968            Vec3Fix::ZERO,
969            Vec3Fix::ZERO,
970        );
971        let b = Vec3Fix::new(Fix128::from_ratio(1, 64), Fix128::ZERO, Fix128::ZERO);
972        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(2)), b, Vec3Fix::ZERO);
973        assert_eq!(m.point_count(), 2);
974        // 中点 1/128: 両点との dist_sq が厳密に 1/16384 で等しい (dyadic、丸めなし)
975        let mid = Vec3Fix::new(Fix128::from_ratio(1, 128), Fix128::ZERO, Fix128::ZERO);
976        let d_a = (m.points[0].local_point_a - mid).length_squared();
977        let d_b = (m.points[1].local_point_a - mid).length_squared();
978        assert_eq!(d_a, d_b);
979        assert_eq!(d_a, Fix128::from_ratio(1, 16384));
980        m.add_or_update(&contact(Vec3Fix::UNIT_Y, fi(3)), mid, Vec3Fix::ZERO);
981        assert_eq!(m.point_count(), 2);
982        assert_eq!(m.points[0].depth, fi(3), "先勝ち: index 0 が更新される");
983        assert_eq!(m.points[0].local_point_a, mid);
984        assert_eq!(m.points[0].age, 1);
985        assert_eq!(m.points[1].depth, fi(2), "index 1 は不変");
986        assert_eq!(m.points[1].local_point_a, b);
987        assert_eq!(m.points[1].age, 0);
988    }
989
990    /// 満杯時の最浅探索 `p.depth < shallowest_depth` (厳密): 最浅が同点なら
991    /// 先頭 (index 0) が置換される (`<=` なら最後の同点 index 1 が置換される)
992    #[test]
993    fn manifold_full_replaces_first_of_equal_shallowest() {
994        let mut m = manifold();
995        for (i, d) in [1, 1, 3, 4].iter().enumerate() {
996            m.add_or_update(
997                &contact(Vec3Fix::UNIT_Y, fi(*d)),
998                v3i(i as i64 * 10, 0, 0),
999                Vec3Fix::ZERO,
1000            );
1001        }
1002        assert_eq!(m.point_count(), MAX_MANIFOLD_POINTS);
1003        m.add_or_update(
1004            &contact(Vec3Fix::UNIT_Y, fi(2)),
1005            v3i(100, 0, 0),
1006            Vec3Fix::ZERO,
1007        );
1008        assert_eq!(m.point_count(), 4);
1009        assert_eq!(m.points[0].local_point_a, v3i(100, 0, 0));
1010        assert_eq!(m.points[0].depth, fi(2));
1011        assert_eq!(m.points[1].local_point_a, v3i(10, 0, 0), "同点の後方は不変");
1012        assert_eq!(m.points[1].depth, fi(1));
1013        let depths: Vec<Fix128> = m.points.iter().map(|p| p.depth).collect();
1014        assert_eq!(depths, vec![fi(2), fi(1), fi(3), fi(4)]);
1015    }
1016
1017    /// `point_idx < len` の境界: idx == len は範囲外 (`<=` なら index out of bounds panic)
1018    #[test]
1019    fn warm_start_and_store_at_exact_len_are_out_of_range() {
1020        let mut m = manifold();
1021        m.add_or_update(
1022            &contact(Vec3Fix::UNIT_Y, fi(1)),
1023            Vec3Fix::ZERO,
1024            Vec3Fix::ZERO,
1025        );
1026        m.store_impulses(0, fi(2), fi(3), fi(4));
1027        assert_eq!(m.point_count(), 1);
1028        // idx 1 == len 1
1029        assert_eq!(
1030            m.warm_start_impulse(1),
1031            (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO)
1032        );
1033        m.store_impulses(1, fi(9), fi(9), fi(9));
1034        assert_eq!(m.point_count(), 1);
1035        assert_eq!(m.warm_start_impulse(0), (fi(2), fi(3), fi(4)));
1036        // 空 manifold: idx 0 == len 0
1037        let e = manifold();
1038        assert_eq!(
1039            e.warm_start_impulse(0),
1040            (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO)
1041        );
1042        let mut e2 = manifold();
1043        e2.store_impulses(0, fi(1), fi(1), fi(1));
1044        assert!(e2.is_empty());
1045    }
1046
1047    /// body A の `total_impulse * inv_mass` (inv_mass ≠ 1) と tangent `t1 * (λ * factor)`
1048    /// (λ * factor ≠ 1): 乗算と除算で結果が異なる値を使う
1049    #[test]
1050    fn apply_warm_start_scales_by_body_a_inv_mass() {
1051        let mut cache = ContactCache::new();
1052        cache.warm_start_factor = Fix128::from_ratio(1, 2);
1053        let m = cache.get_or_create(BodyPairKey::new(0, 1), Fix128::ONE, Fix128::ZERO);
1054        m.add_or_update(
1055            &contact(Vec3Fix::UNIT_Z, fi(1)),
1056            Vec3Fix::ZERO,
1057            Vec3Fix::ZERO,
1058        );
1059        // normal z: t1 = z × x = y、t2 = z × y = -x、λ_t1 = 6 → 6 * 0.5 = 3、他 0
1060        m.store_impulses(0, Fix128::ZERO, fi(6), Fix128::ZERO);
1061        let mut bodies = vec![
1062            crate::solver::RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
1063            crate::solver::RigidBody::new_static(v3i(1, 0, 0)),
1064        ];
1065        bodies[0].inv_mass = fi(4);
1066        cache.apply_warm_start(&mut bodies);
1067        // total = y * 3、A += total * 4 = (0, 12, 0)  (÷ なら (0, 3/4, 0) / t1 ÷ 3 なら (0, 4/3, 0))
1068        assert_eq!(bodies[0].velocity, v3i(0, 12, 0));
1069        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
1070    }
1071
1072    /// reference 軸選択 `abs_x <= abs_y && abs_x <= abs_z`: 片側だけ真の normal で
1073    /// X が選ばれない (`||` なら X が選ばれ t1 の零成分が変わる)
1074    #[test]
1075    fn tangent_frame_reference_requires_both_comparisons() {
1076        // (2, 4, 1): abs_x <= abs_y 真、abs_x <= abs_z 偽 → else-if 4 <= 1 偽 → Z
1077        //   t1 = normalize(n × Z) = normalize((4, -2, 0)) → z == 0、x > 0、y < 0
1078        //   (X 参照なら n × X = (0, 1, -4) → x == 0)
1079        let n = v3i(2, 4, 1);
1080        let (t1, t2) = tangent_frame(n);
1081        assert_eq!(t1.z, Fix128::ZERO);
1082        assert!(t1.x > Fix128::ZERO && t1.y < Fix128::ZERO, "{t1:?}");
1083        assert_eq!(t2, n.cross(t1));
1084        // (2, 1, 4): abs_x <= abs_y 偽、abs_x <= abs_z 真 → else-if 1 <= 4 真 → Y
1085        //   t1 = normalize(n × Y) = normalize((-4, 0, 2)) → y == 0、x < 0、z > 0
1086        //   (X 参照なら n × X = (0, 4, -1) → x == 0)
1087        let n2 = v3i(2, 1, 4);
1088        let (u1, u2) = tangent_frame(n2);
1089        assert_eq!(u1.y, Fix128::ZERO);
1090        assert!(u1.x < Fix128::ZERO && u1.z > Fix128::ZERO, "{u1:?}");
1091        assert_eq!(u2, n2.cross(u1));
1092    }
1093
1094    #[cfg(feature = "std")]
1095    #[test]
1096    fn a_stale_index_found_by_the_scan_is_re_indexed() {
1097        let mut cache = ContactCache::new();
1098        let (a, b) = (BodyPairKey::new(0, 1), BodyPairKey::new(2, 3));
1099        cache.get_or_create(a, Fix128::ONE, Fix128::ZERO);
1100        cache.get_or_create(b, Fix128::ONE, Fix128::ZERO);
1101        cache.manifolds.remove(0); // b moves to slot 0, its index still says 1
1102        assert_eq!(cache.pair_index.get(&b), Some(&1));
1103        assert_eq!(cache.get_or_create(b, Fix128::ONE, Fix128::ZERO).pair, b);
1104        assert_eq!(cache.pair_index.get(&b), Some(&0));
1105        assert_eq!(cache.manifolds.len(), 1, "no duplicate manifold for b");
1106    }
1107}