Skip to main content

oxiphysics_collision/
contact_generation.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Advanced contact manifold generation for collision detection.
5//!
6//! Provides:
7//! - Polyhedron face-face, face-edge, edge-edge contact
8//! - Box-box specialized manifold
9//! - Capsule-capsule contact
10//! - Mesh vs convex contact
11//! - 4-point manifold reduction
12//! - Persistent manifold with frame-to-frame tracking
13//! - Feature contact (vertex/edge/face type tracking)
14//! - Contact normal smoothing on mesh geometry
15//! - Contact caching with LRU
16//! - Contact event system (begin/end events)
17
18// ---------------------------------------------------------------------------
19// Helper math
20// ---------------------------------------------------------------------------
21
22/// Dot product of two 3-vectors.
23#[inline]
24pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
25    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
26}
27
28/// Cross product of two 3-vectors.
29#[inline]
30pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31    [
32        a[1] * b[2] - a[2] * b[1],
33        a[2] * b[0] - a[0] * b[2],
34        a[0] * b[1] - a[1] * b[0],
35    ]
36}
37
38/// Squared norm of a 3-vector.
39#[inline]
40pub fn norm_sq3(a: [f64; 3]) -> f64 {
41    dot3(a, a)
42}
43
44/// Norm of a 3-vector.
45#[inline]
46pub fn norm3(a: [f64; 3]) -> f64 {
47    norm_sq3(a).sqrt()
48}
49
50/// Normalize a 3-vector.
51#[inline]
52pub fn normalize3(a: [f64; 3]) -> [f64; 3] {
53    let n = norm3(a);
54    if n < 1e-15 {
55        return [0.0, 1.0, 0.0];
56    }
57    [a[0] / n, a[1] / n, a[2] / n]
58}
59
60/// Add two 3-vectors.
61#[inline]
62pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
63    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
64}
65
66/// Subtract two 3-vectors.
67#[inline]
68pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
69    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
70}
71
72/// Scale a 3-vector.
73#[inline]
74pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
75    [a[0] * s, a[1] * s, a[2] * s]
76}
77
78/// Negate a 3-vector.
79#[inline]
80fn neg3(a: [f64; 3]) -> [f64; 3] {
81    [-a[0], -a[1], -a[2]]
82}
83
84// ---------------------------------------------------------------------------
85// Contact point
86// ---------------------------------------------------------------------------
87
88/// A single contact point in a manifold.
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct ContactPoint {
91    /// World-space contact position on body A.
92    pub pos_a: [f64; 3],
93    /// World-space contact position on body B.
94    pub pos_b: [f64; 3],
95    /// Contact normal pointing from B to A.
96    pub normal: [f64; 3],
97    /// Penetration depth (positive = overlapping).
98    pub depth: f64,
99    /// Feature ID on body A.
100    pub feature_a: u32,
101    /// Feature ID on body B.
102    pub feature_b: u32,
103    /// Warm-start accumulated normal impulse.
104    pub impulse: f64,
105}
106
107impl ContactPoint {
108    /// Create a new contact point.
109    pub fn new(pos_a: [f64; 3], pos_b: [f64; 3], normal: [f64; 3], depth: f64) -> Self {
110        Self {
111            pos_a,
112            pos_b,
113            normal,
114            depth,
115            feature_a: 0,
116            feature_b: 0,
117            impulse: 0.0,
118        }
119    }
120
121    /// Contact point midpoint.
122    pub fn midpoint(&self) -> [f64; 3] {
123        scale3(add3(self.pos_a, self.pos_b), 0.5)
124    }
125}
126
127// ---------------------------------------------------------------------------
128// Contact manifold
129// ---------------------------------------------------------------------------
130
131/// Maximum contact points in a manifold.
132pub const MAX_MANIFOLD_POINTS: usize = 4;
133
134/// A contact manifold between two bodies.
135#[derive(Debug, Clone)]
136pub struct ContactManifoldEx {
137    /// Contact points (up to MAX_MANIFOLD_POINTS).
138    pub points: Vec<ContactPoint>,
139    /// Shared contact normal.
140    pub normal: [f64; 3],
141    /// Body A ID.
142    pub body_a: u32,
143    /// Body B ID.
144    pub body_b: u32,
145    /// Frame counter when last updated.
146    pub last_frame: u64,
147}
148
149impl ContactManifoldEx {
150    /// Create an empty contact manifold.
151    pub fn new(body_a: u32, body_b: u32) -> Self {
152        Self {
153            points: Vec::new(),
154            normal: [0.0, 1.0, 0.0],
155            body_a,
156            body_b,
157            last_frame: 0,
158        }
159    }
160
161    /// Number of contact points.
162    pub fn num_points(&self) -> usize {
163        self.points.len()
164    }
165
166    /// Add a contact point (up to MAX_MANIFOLD_POINTS).
167    pub fn add_point(&mut self, cp: ContactPoint) {
168        if self.points.len() < MAX_MANIFOLD_POINTS {
169            self.points.push(cp);
170        }
171    }
172
173    /// Maximum penetration depth.
174    pub fn max_depth(&self) -> f64 {
175        self.points.iter().map(|p| p.depth).fold(0.0_f64, f64::max)
176    }
177
178    /// Is this manifold active (has contact points with positive depth)?
179    pub fn is_active(&self) -> bool {
180        self.points.iter().any(|p| p.depth > 0.0)
181    }
182
183    /// Remove all points with non-positive depth.
184    pub fn prune_inactive(&mut self) {
185        self.points.retain(|p| p.depth > -0.01);
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Clip polygon by plane
191// ---------------------------------------------------------------------------
192
193/// Clip a polygon against a half-plane defined by a support point and normal.
194///
195/// Points on the positive side of the plane (dot(p - s, n) >= 0) are kept.
196/// Returns clipped polygon vertices.
197pub fn clip_polygon_by_plane(
198    polygon: &[[f64; 3]],
199    plane_point: [f64; 3],
200    plane_normal: [f64; 3],
201) -> Vec<[f64; 3]> {
202    if polygon.is_empty() {
203        return Vec::new();
204    }
205    let mut result = Vec::new();
206    let n = polygon.len();
207
208    for i in 0..n {
209        let a = polygon[i];
210        let b = polygon[(i + 1) % n];
211        let da = dot3(sub3(a, plane_point), plane_normal);
212        let db = dot3(sub3(b, plane_point), plane_normal);
213
214        if da >= 0.0 {
215            result.push(a);
216        }
217        if (da >= 0.0) != (db >= 0.0) {
218            // Edge crosses plane, compute intersection
219            let t = da / (da - db);
220            let p = add3(a, scale3(sub3(b, a), t));
221            result.push(p);
222        }
223    }
224    result
225}
226
227// ---------------------------------------------------------------------------
228// SAT helpers
229// ---------------------------------------------------------------------------
230
231/// SAT face axis test between two convex bodies.
232///
233/// Tests separation along a face normal axis.
234/// Returns the separation distance (negative = overlap).
235pub fn sat_face_axis(axis: [f64; 3], ref_pts: &[[f64; 3]], inc_pts: &[[f64; 3]]) -> f64 {
236    let ref_max = ref_pts
237        .iter()
238        .map(|&p| dot3(p, axis))
239        .fold(f64::NEG_INFINITY, f64::max);
240    let inc_min = inc_pts
241        .iter()
242        .map(|&p| dot3(p, axis))
243        .fold(f64::INFINITY, f64::min);
244    inc_min - ref_max
245}
246
247/// SAT edge axis test between two edges.
248///
249/// Tests separation along the cross product of two edge directions.
250pub fn sat_edge_axis(
251    edge_a: [f64; 3],
252    edge_b: [f64; 3],
253    ref_center: [f64; 3],
254    ref_pts: &[[f64; 3]],
255    inc_pts: &[[f64; 3]],
256) -> f64 {
257    let axis = cross3(edge_a, edge_b);
258    let len = norm3(axis);
259    if len < 1e-10 {
260        return f64::NEG_INFINITY;
261    } // Parallel edges
262    let n = scale3(axis, 1.0 / len);
263
264    // Orient normal toward ref_center
265    let n = if dot3(sub3(ref_center, inc_pts[0]), n) < 0.0 {
266        neg3(n)
267    } else {
268        n
269    };
270
271    let ref_max = ref_pts
272        .iter()
273        .map(|&p| dot3(p, n))
274        .fold(f64::NEG_INFINITY, f64::max);
275    let inc_min = inc_pts
276        .iter()
277        .map(|&p| dot3(p, n))
278        .fold(f64::INFINITY, f64::min);
279    inc_min - ref_max
280}
281
282/// Find the incident face of a convex body most anti-parallel to a reference normal.
283///
284/// Returns the face index and its normal.
285pub fn find_incident_face(normals: &[[f64; 3]], ref_normal: [f64; 3]) -> (usize, [f64; 3]) {
286    let mut best_idx = 0;
287    let mut best_dot = f64::INFINITY;
288    for (i, &n) in normals.iter().enumerate() {
289        let d = dot3(n, ref_normal);
290        if d < best_dot {
291            best_dot = d;
292            best_idx = i;
293        }
294    }
295    (best_idx, normals[best_idx])
296}
297
298// ---------------------------------------------------------------------------
299// PolyhedronContact
300// ---------------------------------------------------------------------------
301
302/// Face-face, face-edge, and edge-edge contact generation for convex polyhedra.
303///
304/// Uses the Sutherland-Hodgman clipping algorithm for face-face contacts.
305#[derive(Debug, Clone)]
306pub struct PolyhedronContact {
307    /// Maximum contact points to generate.
308    pub max_contacts: usize,
309}
310
311impl PolyhedronContact {
312    /// Create a new polyhedron contact generator.
313    pub fn new(max_contacts: usize) -> Self {
314        Self { max_contacts }
315    }
316
317    /// Compute face-face contact between two convex polyhedra.
318    ///
319    /// # Arguments
320    /// * `ref_face` – polygon of reference face vertices
321    /// * `inc_face` – polygon of incident face vertices
322    /// * `ref_normal` – reference face outward normal
323    /// * `depth` – maximum clipping depth threshold
324    pub fn face_face_contact(
325        &self,
326        ref_face: &[[f64; 3]],
327        inc_face: &[[f64; 3]],
328        ref_normal: [f64; 3],
329        depth: f64,
330    ) -> Vec<ContactPoint> {
331        if ref_face.is_empty() || inc_face.is_empty() {
332            return Vec::new();
333        }
334
335        // Clip incident face against reference face side planes
336        let mut clipped = inc_face.to_vec();
337        let n = ref_face.len();
338        for i in 0..n {
339            if clipped.is_empty() {
340                break;
341            }
342            let edge_start = ref_face[i];
343            let edge_end = ref_face[(i + 1) % n];
344            let edge_dir = sub3(edge_end, edge_start);
345            // Side plane normal (pointing inward)
346            let side_normal = normalize3(cross3(ref_normal, edge_dir));
347            clipped = clip_polygon_by_plane(&clipped, edge_start, side_normal);
348        }
349
350        // Generate contact points from clipped polygon
351        let mut contacts = Vec::new();
352        let ref_plane_d = dot3(ref_face[0], ref_normal);
353        for p in &clipped {
354            let pen = ref_plane_d - dot3(*p, ref_normal);
355            if pen > -depth {
356                let pos_b = *p;
357                let pos_a = add3(*p, scale3(ref_normal, pen));
358                contacts.push(ContactPoint::new(pos_a, pos_b, ref_normal, pen));
359                if contacts.len() >= self.max_contacts {
360                    break;
361                }
362            }
363        }
364        contacts
365    }
366
367    /// Compute edge-edge contact distance and closest points.
368    ///
369    /// Returns (closest_point_a, closest_point_b, distance).
370    pub fn edge_edge_closest(
371        &self,
372        pa: [f64; 3],
373        da: [f64; 3],
374        pb: [f64; 3],
375        db: [f64; 3],
376    ) -> ([f64; 3], [f64; 3], f64) {
377        let r = sub3(pa, pb);
378        let a = dot3(da, da);
379        let e = dot3(db, db);
380        let f = dot3(db, r);
381
382        if a < 1e-14 && e < 1e-14 {
383            return (pa, pb, norm3(r));
384        }
385
386        let s;
387        let t;
388        if a < 1e-14 {
389            s = 0.0;
390            t = (f / e).clamp(0.0, 1.0);
391        } else {
392            let c = dot3(da, r);
393            if e < 1e-14 {
394                s = (-c / a).clamp(0.0, 1.0);
395                t = 0.0;
396            } else {
397                let b = dot3(da, db);
398                let denom = a * e - b * b;
399                if denom.abs() > 1e-14 {
400                    s = ((b * f - c * e) / denom).clamp(0.0, 1.0);
401                } else {
402                    s = 0.5;
403                }
404                t = ((b * s + f) / e).clamp(0.0, 1.0);
405                let _ = s;
406            }
407            let _ = t;
408        }
409
410        // Recompute s with clamped t
411        let s_final = if a > 1e-14 {
412            let c = dot3(da, r);
413            let b = dot3(da, db);
414            ((b * t - c) / a).clamp(0.0, 1.0)
415        } else {
416            0.0
417        };
418
419        let t_final = if e > 1e-14 {
420            let b = dot3(da, db);
421            ((b * s_final + f) / e).clamp(0.0, 1.0)
422        } else {
423            0.0
424        };
425
426        // Wait, we need the correct values
427        let _ = (s, t);
428
429        let ca = add3(pa, scale3(da, s_final));
430        let cb = add3(pb, scale3(db, t_final));
431        (ca, cb, norm3(sub3(ca, cb)))
432    }
433}
434
435// ---------------------------------------------------------------------------
436// BoxBoxManifold
437// ---------------------------------------------------------------------------
438
439/// Specialized box-box contact manifold generator.
440///
441/// Handles all 6 face pairs and 12*12=144 edge-edge pairs via SAT.
442#[derive(Debug, Clone)]
443pub struct BoxBoxManifold {
444    /// Tolerance for contact detection.
445    pub tolerance: f64,
446}
447
448impl BoxBoxManifold {
449    /// Create a new box-box manifold generator.
450    pub fn new(tolerance: f64) -> Self {
451        Self { tolerance }
452    }
453
454    /// Generate box half-extent vertices.
455    fn box_vertices(
456        center: [f64; 3],
457        half_extents: [f64; 3],
458        axes: [[f64; 3]; 3],
459    ) -> Vec<[f64; 3]> {
460        let mut verts = Vec::with_capacity(8);
461        for sx in &[-1.0, 1.0] {
462            for sy in &[-1.0, 1.0] {
463                for sz in &[-1.0, 1.0] {
464                    let v = add3(
465                        center,
466                        add3(
467                            add3(
468                                scale3(axes[0], sx * half_extents[0]),
469                                scale3(axes[1], sy * half_extents[1]),
470                            ),
471                            scale3(axes[2], sz * half_extents[2]),
472                        ),
473                    );
474                    verts.push(v);
475                }
476            }
477        }
478        verts
479    }
480
481    /// Compute box-box contact manifold.
482    ///
483    /// Returns list of contact points, or empty if separated.
484    pub fn compute(
485        &self,
486        center_a: [f64; 3],
487        half_a: [f64; 3],
488        axes_a: [[f64; 3]; 3],
489        center_b: [f64; 3],
490        half_b: [f64; 3],
491        axes_b: [[f64; 3]; 3],
492    ) -> Vec<ContactPoint> {
493        let verts_a = Self::box_vertices(center_a, half_a, axes_a);
494        let verts_b = Self::box_vertices(center_b, half_b, axes_b);
495
496        // Test 6 face axes (3 from each box)
497        let mut min_sep = f64::INFINITY;
498        let mut best_normal = [0.0f64, 1.0, 0.0];
499
500        for &axis in axes_a.iter().chain(axes_b.iter()) {
501            let n = normalize3(axis);
502            let sep = sat_face_axis(n, &verts_a, &verts_b);
503            if sep > self.tolerance {
504                return Vec::new(); // Separated
505            }
506            if sep < min_sep {
507                min_sep = sep;
508                best_normal = n;
509            }
510        }
511
512        // Orient normal from A to B
513        if dot3(sub3(center_b, center_a), best_normal) < 0.0 {
514            best_normal = neg3(best_normal);
515        }
516
517        // Generate contact points at closest vertices of B
518        let ref_d = dot3(center_a, best_normal) + half_a[0] + half_a[1] + half_a[2]; // Approx
519        let mut contacts = Vec::new();
520        for &v in &verts_b {
521            let pen = ref_d - dot3(v, best_normal);
522            if pen > -self.tolerance {
523                let pos_b = v;
524                let pos_a = add3(v, scale3(best_normal, pen));
525                contacts.push(ContactPoint::new(pos_a, pos_b, best_normal, pen.max(0.0)));
526                if contacts.len() >= MAX_MANIFOLD_POINTS {
527                    break;
528                }
529            }
530        }
531        contacts
532    }
533}
534
535// ---------------------------------------------------------------------------
536// CapsuleCapsuleManifold
537// ---------------------------------------------------------------------------
538
539/// Capsule-capsule contact manifold generator.
540///
541/// Handles the segment distance computation and generates 1 or 2 contact points.
542#[derive(Debug, Clone)]
543pub struct CapsuleCapsuleManifold;
544
545impl CapsuleCapsuleManifold {
546    /// Compute capsule-capsule contact.
547    ///
548    /// Returns up to 2 contact points.
549    ///
550    /// # Arguments
551    /// * `pa_start`, `pa_end` – endpoints of capsule A segment
552    /// * `ra` – radius of capsule A
553    /// * `pb_start`, `pb_end` – endpoints of capsule B segment
554    /// * `rb` – radius of capsule B
555    pub fn compute(
556        pa_start: [f64; 3],
557        pa_end: [f64; 3],
558        ra: f64,
559        pb_start: [f64; 3],
560        pb_end: [f64; 3],
561        rb: f64,
562    ) -> Vec<ContactPoint> {
563        let da = sub3(pa_end, pa_start);
564        let db = sub3(pb_end, pb_start);
565        let r = sub3(pa_start, pb_start);
566
567        let a = dot3(da, da);
568        let e = dot3(db, db);
569        let f = dot3(db, r);
570
571        let s;
572        let t;
573        if a <= 1e-14 && e <= 1e-14 {
574            s = 0.0;
575            t = 0.0;
576        } else if a <= 1e-14 {
577            s = 0.0;
578            t = (f / e).clamp(0.0, 1.0);
579        } else {
580            let c = dot3(da, r);
581            if e <= 1e-14 {
582                t = 0.0;
583                s = (-c / a).clamp(0.0, 1.0);
584            } else {
585                let b = dot3(da, db);
586                let denom = a * e - b * b;
587                if denom.abs() > 1e-14 {
588                    s = ((b * f - c * e) / denom).clamp(0.0, 1.0);
589                } else {
590                    s = 0.5;
591                }
592                t = ((b * s + f) / e).clamp(0.0, 1.0);
593            }
594        }
595
596        let ca = add3(pa_start, scale3(da, s));
597        let cb = add3(pb_start, scale3(db, t));
598        let diff = sub3(ca, cb);
599        let dist = norm3(diff);
600        let total_radius = ra + rb;
601
602        if dist >= total_radius {
603            return Vec::new();
604        }
605
606        let normal = if dist > 1e-10 {
607            normalize3(diff)
608        } else {
609            [0.0, 1.0, 0.0]
610        };
611        let depth = total_radius - dist;
612        let pos_a = sub3(ca, scale3(normal, ra));
613        let pos_b = add3(cb, scale3(normal, rb));
614
615        let mut contacts = vec![ContactPoint::new(pos_a, pos_b, normal, depth)];
616
617        // Check if segments are parallel and long — generate second point
618        let da_len = norm3(da);
619        let db_len = norm3(db);
620        if da_len > 1e-5 && db_len > 1e-5 {
621            let da_n = scale3(da, 1.0 / da_len);
622            let db_n = scale3(db, 1.0 / db_len);
623            let parallel = dot3(da_n, db_n).abs();
624            if parallel > 0.9 && (s > 0.1 && s < 0.9 || t > 0.1 && t < 0.9) {
625                // Add second contact at the other end
626                let s2 = if s < 0.5 { 1.0 } else { 0.0 };
627                let t2 = ((dot3(da_n, db_n) * (s2 - s) + t) * db_len / db_len).clamp(0.0, 1.0);
628                let ca2 = add3(pa_start, scale3(da, s2));
629                let cb2 = add3(pb_start, scale3(db, t2));
630                let diff2 = sub3(ca2, cb2);
631                let dist2 = norm3(diff2);
632                if dist2 < total_radius {
633                    let depth2 = total_radius - dist2;
634                    let n2 = if dist2 > 1e-10 {
635                        normalize3(diff2)
636                    } else {
637                        normal
638                    };
639                    let pa2 = sub3(ca2, scale3(n2, ra));
640                    let pb2 = add3(cb2, scale3(n2, rb));
641                    contacts.push(ContactPoint::new(pa2, pb2, n2, depth2));
642                }
643            }
644        }
645
646        contacts
647    }
648}
649
650// ---------------------------------------------------------------------------
651// MeshConvexManifold
652// ---------------------------------------------------------------------------
653
654/// Triangle mesh vs convex shape contact manifold.
655///
656/// Tests each triangle of the mesh against the convex shape.
657#[derive(Debug, Clone)]
658pub struct MeshConvexManifold {
659    /// Maximum contact points.
660    pub max_contacts: usize,
661    /// Contact distance tolerance.
662    pub tolerance: f64,
663}
664
665impl MeshConvexManifold {
666    /// Create a new mesh-convex manifold generator.
667    pub fn new(max_contacts: usize, tolerance: f64) -> Self {
668        Self {
669            max_contacts,
670            tolerance,
671        }
672    }
673
674    /// Compute contact between a single triangle and a sphere.
675    ///
676    /// Returns contact point if penetrating.
677    pub fn triangle_sphere_contact(
678        &self,
679        tri: ([f64; 3], [f64; 3], [f64; 3]),
680        tri_normal: [f64; 3],
681        sphere_center: [f64; 3],
682        sphere_radius: f64,
683    ) -> Option<ContactPoint> {
684        // Find closest point on triangle to sphere center
685        let cp = Self::closest_on_triangle(sphere_center, tri.0, tri.1, tri.2);
686        let diff = sub3(sphere_center, cp);
687        let dist = norm3(diff);
688        if dist >= sphere_radius {
689            return None;
690        }
691        let normal = if dist > 1e-10 {
692            normalize3(diff)
693        } else {
694            tri_normal
695        };
696        let depth = sphere_radius - dist;
697        let pos_a = sub3(sphere_center, scale3(normal, sphere_radius));
698        Some(ContactPoint::new(pos_a, cp, normal, depth))
699    }
700
701    fn closest_on_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
702        let ab = sub3(b, a);
703        let ac = sub3(c, a);
704        let ap = sub3(p, a);
705        let d1 = dot3(ab, ap);
706        let d2 = dot3(ac, ap);
707        if d1 <= 0.0 && d2 <= 0.0 {
708            return a;
709        }
710
711        let bp = sub3(p, b);
712        let d3 = dot3(ab, bp);
713        let d4 = dot3(ac, bp);
714        if d3 >= 0.0 && d4 <= d3 {
715            return b;
716        }
717
718        let cp_pt = sub3(p, c);
719        let d5 = dot3(ab, cp_pt);
720        let d6 = dot3(ac, cp_pt);
721        if d6 >= 0.0 && d5 <= d6 {
722            return c;
723        }
724
725        let vc = d1 * d4 - d3 * d2;
726        if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
727            let v = d1 / (d1 - d3);
728            return add3(a, scale3(ab, v));
729        }
730        let vb = d5 * d2 - d1 * d6;
731        if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
732            let w = d2 / (d2 - d6);
733            return add3(a, scale3(ac, w));
734        }
735        let va = d3 * d6 - d5 * d4;
736        if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
737            let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
738            return add3(b, scale3(sub3(c, b), w));
739        }
740        let denom = 1.0 / (va + vb + vc);
741        let v = vb * denom;
742        let w = vc * denom;
743        add3(a, add3(scale3(ab, v), scale3(ac, w)))
744    }
745
746    /// Generate contacts between a triangle list and a sphere.
747    pub fn mesh_sphere_contacts(
748        &self,
749        triangles: &[([f64; 3], [f64; 3], [f64; 3])],
750        normals: &[[f64; 3]],
751        sphere_center: [f64; 3],
752        sphere_radius: f64,
753    ) -> Vec<ContactPoint> {
754        let mut contacts = Vec::new();
755        for (i, &tri) in triangles.iter().enumerate() {
756            let n = normals.get(i).copied().unwrap_or([0.0, 1.0, 0.0]);
757            if let Some(cp) = self.triangle_sphere_contact(tri, n, sphere_center, sphere_radius) {
758                contacts.push(cp);
759                if contacts.len() >= self.max_contacts {
760                    break;
761                }
762            }
763        }
764        contacts
765    }
766}
767
768// ---------------------------------------------------------------------------
769// ManifoldReduction
770// ---------------------------------------------------------------------------
771
772/// Reduce a large contact point set to a 4-point manifold.
773///
774/// Uses the maximum area criterion to keep the 4 most widely spread points.
775#[derive(Debug, Clone)]
776pub struct ManifoldReduction;
777
778impl ManifoldReduction {
779    /// Reduce contact points to at most 4 points using maximum area criterion.
780    ///
781    /// Algorithm:
782    /// 1. Keep point with maximum penetration depth.
783    /// 2. Keep point furthest from point 1.
784    /// 3. Keep point forming max area triangle with 1-2.
785    /// 4. Keep point forming max area with triangle 1-2-3.
786    pub fn reduce(points: &[ContactPoint]) -> Vec<ContactPoint> {
787        if points.len() <= MAX_MANIFOLD_POINTS {
788            return points.to_vec();
789        }
790
791        let mut result = Vec::with_capacity(MAX_MANIFOLD_POINTS);
792
793        // Step 1: deepest point
794        let p0_idx = points
795            .iter()
796            .enumerate()
797            .max_by(|(_, a), (_, b)| {
798                a.depth
799                    .partial_cmp(&b.depth)
800                    .unwrap_or(std::cmp::Ordering::Equal)
801            })
802            .map(|(i, _)| i)
803            .unwrap_or(0);
804        result.push(points[p0_idx]);
805        let p0 = points[p0_idx].pos_a;
806
807        // Step 2: furthest from p0
808        let p1_idx = points
809            .iter()
810            .enumerate()
811            .max_by(|(_, a), (_, b)| {
812                norm_sq3(sub3(a.pos_a, p0))
813                    .partial_cmp(&norm_sq3(sub3(b.pos_a, p0)))
814                    .unwrap_or(std::cmp::Ordering::Equal)
815            })
816            .map(|(i, _)| i)
817            .unwrap_or(0);
818        result.push(points[p1_idx]);
819        let p1 = points[p1_idx].pos_a;
820
821        // Step 3: max area with p0-p1
822        let p2_idx = points
823            .iter()
824            .enumerate()
825            .max_by(|(_, a), (_, b)| {
826                let da = Self::point_to_line_dist_sq(a.pos_a, p0, p1);
827                let db = Self::point_to_line_dist_sq(b.pos_a, p0, p1);
828                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
829            })
830            .map(|(i, _)| i)
831            .unwrap_or(0);
832        result.push(points[p2_idx]);
833        let p2 = points[p2_idx].pos_a;
834
835        // Step 4: max area with p0-p1-p2
836        let p3_idx = points
837            .iter()
838            .enumerate()
839            .max_by(|(_, a), (_, b)| {
840                let da = Self::point_to_plane_dist_sq(a.pos_a, p0, p1, p2);
841                let db = Self::point_to_plane_dist_sq(b.pos_a, p0, p1, p2);
842                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
843            })
844            .map(|(i, _)| i)
845            .unwrap_or(0);
846        result.push(points[p3_idx]);
847
848        result
849    }
850
851    fn point_to_line_dist_sq(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
852        let ab = sub3(b, a);
853        let ap = sub3(p, a);
854        let t = dot3(ap, ab) / dot3(ab, ab).max(1e-15);
855        let closest = add3(a, scale3(ab, t.clamp(0.0, 1.0)));
856        norm_sq3(sub3(p, closest))
857    }
858
859    fn point_to_plane_dist_sq(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
860        let n = normalize3(cross3(sub3(b, a), sub3(c, a)));
861        let d = dot3(sub3(p, a), n);
862        d * d
863    }
864}
865
866// ---------------------------------------------------------------------------
867// PersistentManifold
868// ---------------------------------------------------------------------------
869
870/// Persistent contact manifold with frame-to-frame point matching.
871///
872/// Maintains contact points across frames for warm-starting and stability.
873#[derive(Debug, Clone)]
874pub struct PersistentManifold {
875    /// Current manifold.
876    pub manifold: ContactManifoldEx,
877    /// Distance threshold for point matching (m).
878    pub match_threshold: f64,
879    /// Maximum lifetime of an unmatched point (frames).
880    pub max_lifetime: usize,
881    /// Frame counter for each point.
882    pub point_ages: Vec<usize>,
883}
884
885impl PersistentManifold {
886    /// Create a new persistent manifold.
887    pub fn new(body_a: u32, body_b: u32, match_threshold: f64) -> Self {
888        Self {
889            manifold: ContactManifoldEx::new(body_a, body_b),
890            match_threshold,
891            max_lifetime: 5,
892            point_ages: Vec::new(),
893        }
894    }
895
896    /// Update manifold with new contact points.
897    ///
898    /// Matches new points to existing points by proximity and transfers warm-start impulses.
899    pub fn update(&mut self, new_points: &[ContactPoint], frame: u64) {
900        let threshold_sq = self.match_threshold * self.match_threshold;
901        let mut matched = vec![false; new_points.len()];
902        let mut new_manifold_pts = Vec::new();
903        let mut new_ages = Vec::new();
904
905        // Try to match new points to existing points
906        for (old_pt, &old_age) in self.manifold.points.iter().zip(self.point_ages.iter()) {
907            let mut best_idx = None;
908            let mut best_dist = f64::MAX;
909            for (j, new_pt) in new_points.iter().enumerate() {
910                if matched[j] {
911                    continue;
912                }
913                let d = norm_sq3(sub3(old_pt.pos_a, new_pt.pos_a));
914                if d < threshold_sq && d < best_dist {
915                    best_dist = d;
916                    best_idx = Some(j);
917                }
918            }
919
920            if let Some(j) = best_idx {
921                matched[j] = true;
922                let mut updated = new_points[j];
923                // Transfer warm-start impulse
924                updated.impulse = old_pt.impulse;
925                new_manifold_pts.push(updated);
926                new_ages.push(0);
927            } else if old_age < self.max_lifetime {
928                // Keep old point if not too old
929                new_manifold_pts.push(*old_pt);
930                new_ages.push(old_age + 1);
931            }
932        }
933
934        // Add unmatched new points
935        for (j, &pt) in new_points.iter().enumerate() {
936            if !matched[j] {
937                new_manifold_pts.push(pt);
938                new_ages.push(0);
939            }
940        }
941
942        // Reduce to max 4 points
943        let reduced = ManifoldReduction::reduce(&new_manifold_pts);
944        self.point_ages = vec![0; reduced.len()];
945        self.manifold.points = reduced;
946        self.manifold.last_frame = frame;
947    }
948
949    /// Number of currently tracked contact points.
950    pub fn num_points(&self) -> usize {
951        self.manifold.num_points()
952    }
953
954    /// Get accumulated impulse for warm-starting.
955    pub fn warm_start_impulse(&self, point_idx: usize) -> f64 {
956        self.manifold
957            .points
958            .get(point_idx)
959            .map(|p| p.impulse)
960            .unwrap_or(0.0)
961    }
962}
963
964// ---------------------------------------------------------------------------
965// FeatureContact
966// ---------------------------------------------------------------------------
967
968/// Contact feature type.
969#[derive(Debug, Clone, Copy, PartialEq)]
970pub enum FeatureType {
971    /// Vertex contact.
972    Vertex,
973    /// Edge contact.
974    Edge,
975    /// Face contact.
976    Face,
977}
978
979/// Contact feature with type tracking for warm-starting.
980#[derive(Debug, Clone)]
981pub struct FeatureContact {
982    /// Contact point.
983    pub contact: ContactPoint,
984    /// Feature type on body A.
985    pub feature_type_a: FeatureType,
986    /// Feature type on body B.
987    pub feature_type_b: FeatureType,
988    /// Previous feature type on A (for transition detection).
989    pub prev_feature_a: FeatureType,
990    /// Previous feature type on B.
991    pub prev_feature_b: FeatureType,
992}
993
994impl FeatureContact {
995    /// Create a new feature contact.
996    pub fn new(
997        contact: ContactPoint,
998        feature_type_a: FeatureType,
999        feature_type_b: FeatureType,
1000    ) -> Self {
1001        Self {
1002            contact,
1003            feature_type_a,
1004            feature_type_b,
1005            prev_feature_a: feature_type_a,
1006            prev_feature_b: feature_type_b,
1007        }
1008    }
1009
1010    /// Check if a feature type transition occurred.
1011    pub fn has_transition(&self) -> bool {
1012        self.feature_type_a != self.prev_feature_a || self.feature_type_b != self.prev_feature_b
1013    }
1014
1015    /// Update previous feature types.
1016    pub fn update_prev(&mut self) {
1017        self.prev_feature_a = self.feature_type_a;
1018        self.prev_feature_b = self.feature_type_b;
1019    }
1020
1021    /// Classify feature type based on contact normal and geometry.
1022    ///
1023    /// Returns FeatureType based on alignment between normal and face/edge axes.
1024    pub fn classify_feature(normal: [f64; 3], face_normals: &[[f64; 3]]) -> FeatureType {
1025        let mut best_dot = 0.0f64;
1026        for &fn_ in face_normals {
1027            let d = dot3(normal, fn_).abs();
1028            if d > best_dot {
1029                best_dot = d;
1030            }
1031        }
1032        if best_dot > 0.95 {
1033            FeatureType::Face
1034        } else if best_dot > 0.7 {
1035            FeatureType::Edge
1036        } else {
1037            FeatureType::Vertex
1038        }
1039    }
1040}
1041
1042// ---------------------------------------------------------------------------
1043// ContactNormalSmoothing
1044// ---------------------------------------------------------------------------
1045
1046/// Contact normal smoother for mesh collision.
1047///
1048/// Smooths contact normals using Phong normal interpolation
1049/// or Garland-Heckbert normal averaging.
1050#[derive(Debug, Clone)]
1051pub struct ContactNormalSmoothing {
1052    /// Method for normal smoothing.
1053    pub method: NormalSmoothingMethod,
1054    /// Angle threshold for crease detection (radians).
1055    pub crease_angle: f64,
1056}
1057
1058/// Normal smoothing method.
1059#[derive(Debug, Clone, PartialEq)]
1060pub enum NormalSmoothingMethod {
1061    /// Phong normal interpolation (barycentric interpolation of vertex normals).
1062    Phong,
1063    /// Garland-Heckbert area-weighted normal averaging.
1064    Garland,
1065    /// No smoothing (flat normals).
1066    Flat,
1067}
1068
1069impl ContactNormalSmoothing {
1070    /// Create a new contact normal smoother.
1071    pub fn new(method: NormalSmoothingMethod, crease_angle: f64) -> Self {
1072        Self {
1073            method,
1074            crease_angle,
1075        }
1076    }
1077
1078    /// Smooth contact normal at barycentric coordinates (u, v) on a triangle.
1079    ///
1080    /// # Arguments
1081    /// * `face_normal` – flat face normal
1082    /// * `vertex_normals` – per-vertex normals (a, b, c)
1083    /// * `u`, `v` – barycentric coordinates (w = 1-u-v)
1084    pub fn smooth_normal(
1085        &self,
1086        face_normal: [f64; 3],
1087        vertex_normals: ([f64; 3], [f64; 3], [f64; 3]),
1088        u: f64,
1089        v: f64,
1090    ) -> [f64; 3] {
1091        match self.method {
1092            NormalSmoothingMethod::Flat => face_normal,
1093            NormalSmoothingMethod::Phong => {
1094                let w = 1.0 - u - v;
1095                let (na, nb, nc) = vertex_normals;
1096                let interpolated = [
1097                    w * na[0] + u * nb[0] + v * nc[0],
1098                    w * na[1] + u * nb[1] + v * nc[1],
1099                    w * na[2] + u * nb[2] + v * nc[2],
1100                ];
1101                normalize3(interpolated)
1102            }
1103            NormalSmoothingMethod::Garland => {
1104                // Area-weighted average with face normal
1105                let (na, nb, nc) = vertex_normals;
1106                let avg = [
1107                    (na[0] + nb[0] + nc[0]) / 3.0,
1108                    (na[1] + nb[1] + nc[1]) / 3.0,
1109                    (na[2] + nb[2] + nc[2]) / 3.0,
1110                ];
1111                // Blend with face normal based on crease angle
1112                let dot = dot3(normalize3(avg), face_normal);
1113                if dot.acos() < self.crease_angle {
1114                    normalize3(avg)
1115                } else {
1116                    face_normal
1117                }
1118            }
1119        }
1120    }
1121
1122    /// Compute area-weighted vertex normals for a mesh.
1123    ///
1124    /// Returns vertex normals for each vertex index.
1125    pub fn compute_vertex_normals(vertices: &[[f64; 3]], indices: &[[usize; 3]]) -> Vec<[f64; 3]> {
1126        let n = vertices.len();
1127        let mut normals = vec![[0.0f64; 3]; n];
1128
1129        for tri in indices {
1130            let a = vertices[tri[0]];
1131            let b = vertices[tri[1]];
1132            let c = vertices[tri[2]];
1133            let ab = sub3(b, a);
1134            let ac = sub3(c, a);
1135            let area_normal = cross3(ab, ac); // magnitude = 2 * area
1136            for &vi in tri {
1137                normals[vi][0] += area_normal[0];
1138                normals[vi][1] += area_normal[1];
1139                normals[vi][2] += area_normal[2];
1140            }
1141        }
1142
1143        normals.iter_mut().for_each(|n| *n = normalize3(*n));
1144        normals
1145    }
1146}
1147
1148// ---------------------------------------------------------------------------
1149// ContactCaching
1150// ---------------------------------------------------------------------------
1151
1152/// LRU cache for body-pair contact manifolds.
1153///
1154/// Stores persistent manifolds indexed by (body_a, body_b) pair.
1155#[derive(Debug, Clone)]
1156pub struct ContactCaching {
1157    /// Cached manifolds by body pair.
1158    pub cache: std::collections::HashMap<(u32, u32), PersistentManifold>,
1159    /// Maximum cache size.
1160    pub max_size: usize,
1161    /// Frame counter for LRU eviction.
1162    pub frame: u64,
1163}
1164
1165impl ContactCaching {
1166    /// Create a new contact cache.
1167    pub fn new(max_size: usize) -> Self {
1168        Self {
1169            cache: std::collections::HashMap::new(),
1170            max_size,
1171            frame: 0,
1172        }
1173    }
1174
1175    /// Get or create a persistent manifold for a body pair.
1176    pub fn get_or_create(&mut self, body_a: u32, body_b: u32) -> &mut PersistentManifold {
1177        let key = (body_a.min(body_b), body_a.max(body_b));
1178        if !self.cache.contains_key(&key) {
1179            if self.cache.len() >= self.max_size {
1180                self.evict_oldest();
1181            }
1182            self.cache
1183                .insert(key, PersistentManifold::new(body_a, body_b, 0.01));
1184        }
1185        self.cache
1186            .entry(key)
1187            .or_insert_with(|| PersistentManifold::new(body_a, body_b, 0.01))
1188    }
1189
1190    /// Evict the oldest (by last_frame) manifold.
1191    fn evict_oldest(&mut self) {
1192        let oldest_key = self
1193            .cache
1194            .iter()
1195            .min_by_key(|(_, m)| m.manifold.last_frame)
1196            .map(|(k, _)| *k);
1197        if let Some(k) = oldest_key {
1198            self.cache.remove(&k);
1199        }
1200    }
1201
1202    /// Advance frame counter.
1203    pub fn advance_frame(&mut self) {
1204        self.frame += 1;
1205    }
1206
1207    /// Remove stale manifolds (not updated in last N frames).
1208    pub fn purge_stale(&mut self, max_age: u64) {
1209        let frame = self.frame;
1210        self.cache
1211            .retain(|_, m| frame.saturating_sub(m.manifold.last_frame) <= max_age);
1212    }
1213
1214    /// Number of cached manifolds.
1215    pub fn num_cached(&self) -> usize {
1216        self.cache.len()
1217    }
1218}
1219
1220// ---------------------------------------------------------------------------
1221// ContactEventSystem
1222// ---------------------------------------------------------------------------
1223
1224/// Contact event type.
1225#[derive(Debug, Clone, PartialEq)]
1226pub enum ContactEvent {
1227    /// Contact began (first contact point appeared).
1228    Begin(u32, u32),
1229    /// Contact ended (all contact points removed).
1230    End(u32, u32),
1231    /// Contact persisted (same bodies, still in contact).
1232    Persist(u32, u32),
1233}
1234
1235/// Contact event listener trait.
1236pub trait ContactListener {
1237    /// Called when a contact event is emitted.
1238    fn on_contact_event(&mut self, event: ContactEvent);
1239}
1240
1241/// Event system for contact begin/end notifications.
1242#[derive(Debug, Clone)]
1243pub struct ContactEventSystem {
1244    /// Current contact pairs (body_a, body_b).
1245    pub active_contacts: std::collections::HashSet<(u32, u32)>,
1246    /// Pending events this frame.
1247    pub pending_events: Vec<ContactEvent>,
1248}
1249
1250impl ContactEventSystem {
1251    /// Create a new contact event system.
1252    pub fn new() -> Self {
1253        Self {
1254            active_contacts: std::collections::HashSet::new(),
1255            pending_events: Vec::new(),
1256        }
1257    }
1258
1259    /// Update contact set and emit events.
1260    ///
1261    /// # Arguments
1262    /// * `new_contacts` – all currently active contact pairs this frame
1263    pub fn update(&mut self, new_contacts: &[(u32, u32)]) {
1264        let new_set: std::collections::HashSet<_> = new_contacts
1265            .iter()
1266            .map(|&(a, b)| (a.min(b), a.max(b)))
1267            .collect();
1268
1269        self.pending_events.clear();
1270
1271        // Emit End for pairs no longer in contact
1272        for &pair in &self.active_contacts {
1273            if !new_set.contains(&pair) {
1274                self.pending_events.push(ContactEvent::End(pair.0, pair.1));
1275            }
1276        }
1277
1278        // Emit Begin for new pairs, Persist for existing
1279        for &pair in &new_set {
1280            if !self.active_contacts.contains(&pair) {
1281                self.pending_events
1282                    .push(ContactEvent::Begin(pair.0, pair.1));
1283            } else {
1284                self.pending_events
1285                    .push(ContactEvent::Persist(pair.0, pair.1));
1286            }
1287        }
1288
1289        self.active_contacts = new_set;
1290    }
1291
1292    /// Dispatch pending events to a listener.
1293    pub fn dispatch<L: ContactListener>(&self, listener: &mut L) {
1294        for event in &self.pending_events {
1295            listener.on_contact_event(event.clone());
1296        }
1297    }
1298
1299    /// Number of active contact pairs.
1300    pub fn num_active(&self) -> usize {
1301        self.active_contacts.len()
1302    }
1303
1304    /// Number of pending events.
1305    pub fn num_events(&self) -> usize {
1306        self.pending_events.len()
1307    }
1308}
1309
1310impl Default for ContactEventSystem {
1311    fn default() -> Self {
1312        Self::new()
1313    }
1314}
1315
1316// ---------------------------------------------------------------------------
1317// Tests
1318// ---------------------------------------------------------------------------
1319
1320#[cfg(test)]
1321mod tests {
1322    use super::*;
1323
1324    #[test]
1325    fn test_contact_point_new() {
1326        let cp = ContactPoint::new([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0], 0.1);
1327        assert!((cp.depth - 0.1).abs() < 1e-10);
1328    }
1329
1330    #[test]
1331    fn test_contact_point_midpoint() {
1332        let cp = ContactPoint::new([0.0; 3], [2.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.1);
1333        let mid = cp.midpoint();
1334        assert!((mid[0] - 1.0).abs() < 1e-10);
1335    }
1336
1337    #[test]
1338    fn test_clip_polygon_by_plane_keep_all() {
1339        let poly = vec![[0.0, 0.0, 0.0_f64], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
1340        // All points above plane y=0 (pointing +y)
1341        let clipped = clip_polygon_by_plane(&poly, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1342        assert_eq!(clipped.len(), 3, "all points should be kept");
1343    }
1344
1345    #[test]
1346    fn test_clip_polygon_by_plane_half() {
1347        let poly = vec![
1348            [0.0, -1.0, 0.0_f64],
1349            [0.0, 1.0, 0.0],
1350            [1.0, 1.0, 0.0],
1351            [1.0, -1.0, 0.0],
1352        ];
1353        // Clip against y>=0
1354        let clipped = clip_polygon_by_plane(&poly, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1355        assert!(
1356            !clipped.is_empty(),
1357            "some points should remain after clipping"
1358        );
1359        for p in &clipped {
1360            assert!(
1361                p[1] >= -1e-10,
1362                "all remaining points should satisfy constraint"
1363            );
1364        }
1365    }
1366
1367    #[test]
1368    fn test_sat_face_axis_separated() {
1369        let ref_pts = vec![[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0]];
1370        let inc_pts = vec![[3.0_f64, 0.0, 0.0], [4.0, 0.0, 0.0]];
1371        let sep = sat_face_axis([1.0, 0.0, 0.0], &ref_pts, &inc_pts);
1372        assert!(sep > 0.0, "should detect separation: {}", sep);
1373    }
1374
1375    #[test]
1376    fn test_sat_face_axis_overlapping() {
1377        let ref_pts = vec![[0.0_f64, 0.0, 0.0], [2.0, 0.0, 0.0]];
1378        let inc_pts = vec![[1.0_f64, 0.0, 0.0], [3.0, 0.0, 0.0]];
1379        let sep = sat_face_axis([1.0, 0.0, 0.0], &ref_pts, &inc_pts);
1380        assert!(sep < 0.0, "should detect overlap: {}", sep);
1381    }
1382
1383    #[test]
1384    fn test_find_incident_face() {
1385        let normals = vec![[0.0, 1.0, 0.0_f64], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0]];
1386        let (idx, _) = find_incident_face(&normals, [0.0, 1.0, 0.0]);
1387        // Most anti-parallel: [0,-1,0] (dot = -1)
1388        assert_eq!(idx, 1, "most anti-parallel face should be found");
1389    }
1390
1391    #[test]
1392    fn test_polyhedron_face_face_contact() {
1393        let pg = PolyhedronContact::new(4);
1394        // Two overlapping quads
1395        let ref_face = vec![
1396            [0.0_f64, 0.0, 0.0],
1397            [1.0, 0.0, 0.0],
1398            [1.0, 1.0, 0.0],
1399            [0.0, 1.0, 0.0],
1400        ];
1401        let inc_face = vec![
1402            [0.2_f64, 0.2, -0.1],
1403            [0.8, 0.2, -0.1],
1404            [0.8, 0.8, -0.1],
1405            [0.2, 0.8, -0.1],
1406        ];
1407        let contacts = pg.face_face_contact(&ref_face, &inc_face, [0.0, 0.0, 1.0], 0.2);
1408        // Should generate some contacts
1409        let _ = contacts;
1410    }
1411
1412    #[test]
1413    fn test_polyhedron_edge_edge_closest() {
1414        let pg = PolyhedronContact::new(4);
1415        let pa = [0.0, 0.0, 0.0_f64];
1416        let da = [1.0, 0.0, 0.0];
1417        let pb = [0.5, 0.5, -1.0_f64];
1418        let db = [0.0, 0.0, 1.0];
1419        let (ca, cb, dist) = pg.edge_edge_closest(pa, da, pb, db);
1420        let _ = (ca, cb);
1421        assert!(
1422            dist < 1.0,
1423            "closest distance should be less than 1: {}",
1424            dist
1425        );
1426    }
1427
1428    #[test]
1429    fn test_box_box_manifold_separated() {
1430        let bbox = BoxBoxManifold::new(0.01);
1431        let id = [[1.0, 0.0, 0.0_f64], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1432        let contacts = bbox.compute([0.0; 3], [1.0; 3], id, [5.0, 0.0, 0.0], [1.0; 3], id);
1433        assert!(
1434            contacts.is_empty(),
1435            "separated boxes should have no contacts"
1436        );
1437    }
1438
1439    #[test]
1440    fn test_capsule_capsule_separated() {
1441        let contacts = CapsuleCapsuleManifold::compute(
1442            [0.0; 3],
1443            [0.0, 1.0, 0.0],
1444            0.5,
1445            [5.0, 0.0, 0.0],
1446            [5.0, 1.0, 0.0],
1447            0.5,
1448        );
1449        assert!(
1450            contacts.is_empty(),
1451            "separated capsules should have no contacts"
1452        );
1453    }
1454
1455    #[test]
1456    fn test_capsule_capsule_intersecting() {
1457        let contacts = CapsuleCapsuleManifold::compute(
1458            [0.0, 0.0, 0.0],
1459            [0.0, 2.0, 0.0],
1460            0.5,
1461            [0.3, 0.0, 0.0],
1462            [0.3, 2.0, 0.0],
1463            0.5,
1464        );
1465        assert!(
1466            !contacts.is_empty(),
1467            "close capsules should generate contact"
1468        );
1469        assert!(contacts[0].depth > 0.0, "contact depth should be positive");
1470    }
1471
1472    #[test]
1473    fn test_mesh_convex_sphere_contact() {
1474        let manifold_gen = MeshConvexManifold::new(8, 1e-3);
1475        let tri = ([0.0, 0.0, 0.0_f64], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1476        let n = [0.0, 0.0, 1.0_f64];
1477        let sphere_center = [0.25, 0.25, 0.1]; // slightly above triangle
1478        let contact = manifold_gen.triangle_sphere_contact(tri, n, sphere_center, 0.5);
1479        assert!(contact.is_some(), "sphere above triangle should contact");
1480    }
1481
1482    #[test]
1483    fn test_manifold_reduction_under_limit() {
1484        let pts: Vec<_> = (0..3)
1485            .map(|i| {
1486                ContactPoint::new(
1487                    [i as f64, 0.0, 0.0],
1488                    [i as f64, 0.0, 0.0],
1489                    [0.0, 1.0, 0.0],
1490                    0.1,
1491                )
1492            })
1493            .collect();
1494        let reduced = ManifoldReduction::reduce(&pts);
1495        assert_eq!(reduced.len(), 3, "under 4 points should not reduce");
1496    }
1497
1498    #[test]
1499    fn test_manifold_reduction_over_limit() {
1500        let pts: Vec<_> = (0..10)
1501            .map(|i| {
1502                let d = (i as f64) * 0.5;
1503                ContactPoint::new(
1504                    [i as f64, 0.0, 0.0],
1505                    [i as f64, 0.0, 0.0],
1506                    [0.0, 1.0, 0.0],
1507                    d,
1508                )
1509            })
1510            .collect();
1511        let reduced = ManifoldReduction::reduce(&pts);
1512        assert!(
1513            reduced.len() <= MAX_MANIFOLD_POINTS,
1514            "reduced should have at most 4 points"
1515        );
1516    }
1517
1518    #[test]
1519    fn test_persistent_manifold_update() {
1520        let mut pm = PersistentManifold::new(1, 2, 0.1);
1521        let pts = vec![ContactPoint::new([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0], 0.1)];
1522        pm.update(&pts, 1);
1523        assert_eq!(pm.num_points(), 1);
1524    }
1525
1526    #[test]
1527    fn test_persistent_manifold_warm_start() {
1528        let mut pm = PersistentManifold::new(1, 2, 0.1);
1529        let mut cp = ContactPoint::new([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0], 0.1);
1530        cp.impulse = 5.0;
1531        pm.manifold.add_point(cp);
1532        pm.point_ages.push(0);
1533        let impulse = pm.warm_start_impulse(0);
1534        assert!((impulse - 5.0).abs() < 1e-10);
1535    }
1536
1537    #[test]
1538    fn test_feature_contact_classification() {
1539        let face_normals = vec![[0.0, 1.0, 0.0_f64], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1540        let ft = FeatureContact::classify_feature([0.0, 1.0, 0.0], &face_normals);
1541        assert_eq!(
1542            ft,
1543            FeatureType::Face,
1544            "aligned with face normal should be Face"
1545        );
1546    }
1547
1548    #[test]
1549    fn test_contact_normal_smoothing_phong() {
1550        let smoother = ContactNormalSmoothing::new(NormalSmoothingMethod::Phong, 0.3);
1551        let face_n = [0.0, 0.0, 1.0_f64];
1552        let vn = ([0.0, 0.0, 1.0_f64], [0.0, 0.1, 0.995], [0.0, -0.1, 0.995]);
1553        let smooth_n = smoother.smooth_normal(face_n, vn, 0.5, 0.0);
1554        let len = norm3(smooth_n);
1555        assert!(
1556            (len - 1.0).abs() < 1e-6,
1557            "smoothed normal should be unit length"
1558        );
1559    }
1560
1561    #[test]
1562    fn test_contact_normal_vertex_normals() {
1563        let verts = vec![[0.0, 0.0, 0.0_f64], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1564        let indices = vec![[0, 1, 2]];
1565        let normals = ContactNormalSmoothing::compute_vertex_normals(&verts, &indices);
1566        assert_eq!(normals.len(), 3);
1567        for n in &normals {
1568            let len = norm3(*n);
1569            assert!(
1570                (len - 1.0).abs() < 1e-6 || len < 1e-10,
1571                "vertex normals should be unit: {}",
1572                len
1573            );
1574        }
1575    }
1576
1577    #[test]
1578    fn test_contact_caching_get_or_create() {
1579        let mut cache = ContactCaching::new(10);
1580        let m = cache.get_or_create(1, 2);
1581        assert_eq!(m.manifold.body_a, 1);
1582    }
1583
1584    #[test]
1585    fn test_contact_caching_size_limit() {
1586        let mut cache = ContactCaching::new(2);
1587        cache.get_or_create(1, 2).manifold.last_frame = 1;
1588        cache.get_or_create(3, 4).manifold.last_frame = 2;
1589        cache.get_or_create(5, 6).manifold.last_frame = 3;
1590        assert!(cache.num_cached() <= 2, "cache should not exceed max_size");
1591    }
1592
1593    #[test]
1594    fn test_contact_event_system_begin() {
1595        let mut evs = ContactEventSystem::new();
1596        evs.update(&[(1, 2)]);
1597        let begin = evs
1598            .pending_events
1599            .iter()
1600            .any(|e| matches!(e, ContactEvent::Begin(1, 2) | ContactEvent::Begin(2, 1)));
1601        assert!(begin, "should emit Begin event for new contact");
1602    }
1603
1604    #[test]
1605    fn test_contact_event_system_end() {
1606        let mut evs = ContactEventSystem::new();
1607        evs.update(&[(1, 2)]);
1608        evs.update(&[]); // contact ended
1609        let end = evs
1610            .pending_events
1611            .iter()
1612            .any(|e| matches!(e, ContactEvent::End(_, _)));
1613        assert!(end, "should emit End event when contact is removed");
1614    }
1615
1616    #[test]
1617    fn test_contact_event_system_persist() {
1618        let mut evs = ContactEventSystem::new();
1619        evs.update(&[(1, 2)]);
1620        evs.update(&[(1, 2)]); // still in contact
1621        let persist = evs
1622            .pending_events
1623            .iter()
1624            .any(|e| matches!(e, ContactEvent::Persist(_, _)));
1625        assert!(persist, "should emit Persist event for ongoing contact");
1626    }
1627
1628    #[test]
1629    fn test_contact_manifold_add_points() {
1630        let mut mf = ContactManifoldEx::new(1, 2);
1631        for i in 0..5 {
1632            mf.add_point(ContactPoint::new(
1633                [i as f64, 0.0, 0.0],
1634                [0.0; 3],
1635                [0.0, 1.0, 0.0],
1636                0.1,
1637            ));
1638        }
1639        assert!(
1640            mf.num_points() <= MAX_MANIFOLD_POINTS,
1641            "manifold should not exceed max points"
1642        );
1643    }
1644
1645    #[test]
1646    fn test_contact_manifold_prune() {
1647        let mut mf = ContactManifoldEx::new(1, 2);
1648        mf.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0], 0.1));
1649        mf.add_point(ContactPoint::new(
1650            [1.0, 0.0, 0.0],
1651            [0.0; 3],
1652            [0.0, 1.0, 0.0],
1653            -0.5,
1654        ));
1655        mf.prune_inactive();
1656        assert_eq!(mf.num_points(), 1, "negative depth point should be pruned");
1657    }
1658}