Skip to main content

oxiphysics_core/
collision.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Collision detection primitives: GJK/EPA narrowphase, SAT, sphere/capsule
6//! tests, BVH broadphase, and contact manifold generation.
7//!
8//! All geometry is represented with `[f64; 3]` arrays to avoid nalgebra
9//! dependencies inside this module.
10
11#![allow(dead_code)]
12#![allow(non_snake_case)]
13
14use crate::math::Real;
15
16// ---------------------------------------------------------------------------
17// Vec3 helper (local, no nalgebra)
18// ---------------------------------------------------------------------------
19
20/// Add two 3-vectors.
21#[inline]
22fn v3_add(a: [Real; 3], b: [Real; 3]) -> [Real; 3] {
23    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
24}
25
26/// Subtract two 3-vectors.
27#[inline]
28fn v3_sub(a: [Real; 3], b: [Real; 3]) -> [Real; 3] {
29    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
30}
31
32/// Scale a 3-vector.
33#[inline]
34fn v3_scale(a: [Real; 3], s: Real) -> [Real; 3] {
35    [a[0] * s, a[1] * s, a[2] * s]
36}
37
38/// Dot product.
39#[inline]
40fn v3_dot(a: [Real; 3], b: [Real; 3]) -> Real {
41    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
42}
43
44/// Cross product.
45#[inline]
46fn v3_cross(a: [Real; 3], b: [Real; 3]) -> [Real; 3] {
47    [
48        a[1] * b[2] - a[2] * b[1],
49        a[2] * b[0] - a[0] * b[2],
50        a[0] * b[1] - a[1] * b[0],
51    ]
52}
53
54/// Squared length of a 3-vector.
55#[inline]
56fn v3_len_sq(a: [Real; 3]) -> Real {
57    v3_dot(a, a)
58}
59
60/// Length of a 3-vector.
61#[inline]
62fn v3_len(a: [Real; 3]) -> Real {
63    v3_len_sq(a).sqrt()
64}
65
66/// Normalize a 3-vector.  Returns `[0,0,0]` for zero-length input.
67#[inline]
68fn v3_normalize(a: [Real; 3]) -> [Real; 3] {
69    let len = v3_len(a);
70    if len > 1e-15 {
71        v3_scale(a, 1.0 / len)
72    } else {
73        [0.0; 3]
74    }
75}
76
77/// Negate a 3-vector.
78#[inline]
79fn v3_neg(a: [Real; 3]) -> [Real; 3] {
80    [-a[0], -a[1], -a[2]]
81}
82
83// ---------------------------------------------------------------------------
84// Convex Shape (support function interface)
85// ---------------------------------------------------------------------------
86
87/// Trait for convex shapes defined by a support function.
88pub trait ConvexShape {
89    /// Return the support point of the shape in direction `dir`.
90    fn support(&self, dir: [Real; 3]) -> [Real; 3];
91}
92
93// ---------------------------------------------------------------------------
94// Primitive shapes
95// ---------------------------------------------------------------------------
96
97/// A sphere centred at `center` with `radius`.
98#[derive(Debug, Clone)]
99pub struct Sphere {
100    /// Centre position in world space.
101    pub center: [Real; 3],
102    /// Radius.
103    pub radius: Real,
104}
105
106impl Sphere {
107    /// Create a new sphere.
108    pub fn new(center: [Real; 3], radius: Real) -> Self {
109        Self { center, radius }
110    }
111}
112
113impl ConvexShape for Sphere {
114    fn support(&self, dir: [Real; 3]) -> [Real; 3] {
115        let n = v3_normalize(dir);
116        v3_add(self.center, v3_scale(n, self.radius))
117    }
118}
119
120/// An axis-aligned box centred at `center` with `half_extents`.
121#[derive(Debug, Clone)]
122pub struct Box3 {
123    /// Centre position in world space.
124    pub center: [Real; 3],
125    /// Half-extents along each axis.
126    pub half_extents: [Real; 3],
127}
128
129impl Box3 {
130    /// Create a new axis-aligned box.
131    pub fn new(center: [Real; 3], half_extents: [Real; 3]) -> Self {
132        Self {
133            center,
134            half_extents,
135        }
136    }
137}
138
139impl ConvexShape for Box3 {
140    fn support(&self, dir: [Real; 3]) -> [Real; 3] {
141        [
142            self.center[0] + self.half_extents[0] * dir[0].signum(),
143            self.center[1] + self.half_extents[1] * dir[1].signum(),
144            self.center[2] + self.half_extents[2] * dir[2].signum(),
145        ]
146    }
147}
148
149/// A capsule: the Minkowski sum of a line segment and a sphere.
150#[derive(Debug, Clone)]
151pub struct Capsule {
152    /// First endpoint of the inner segment.
153    pub a: [Real; 3],
154    /// Second endpoint of the inner segment.
155    pub b: [Real; 3],
156    /// Radius.
157    pub radius: Real,
158}
159
160impl Capsule {
161    /// Create a new capsule.
162    pub fn new(a: [Real; 3], b: [Real; 3], radius: Real) -> Self {
163        Self { a, b, radius }
164    }
165}
166
167impl ConvexShape for Capsule {
168    fn support(&self, dir: [Real; 3]) -> [Real; 3] {
169        // Choose whichever endpoint is further in `dir`.
170        let da = v3_dot(self.a, dir);
171        let db = v3_dot(self.b, dir);
172        let base = if da >= db { self.a } else { self.b };
173        v3_add(base, v3_scale(v3_normalize(dir), self.radius))
174    }
175}
176
177// ---------------------------------------------------------------------------
178// Sphere-Sphere test
179// ---------------------------------------------------------------------------
180
181/// Result of a sphere–sphere intersection test.
182#[derive(Debug, Clone)]
183pub struct SphereSphereContact {
184    /// Whether the spheres overlap.
185    pub overlapping: bool,
186    /// Contact normal from A to B (normalised).
187    pub normal: [Real; 3],
188    /// Penetration depth (negative = separated; positive = penetrating).
189    pub depth: Real,
190    /// Contact point (midpoint of overlap, or closest point).
191    pub point: [Real; 3],
192}
193
194/// Test two spheres for intersection and compute the contact manifold.
195pub fn sphere_sphere(a: &Sphere, b: &Sphere) -> SphereSphereContact {
196    let d = v3_sub(b.center, a.center);
197    let dist = v3_len(d);
198    let sum_r = a.radius + b.radius;
199    let depth = sum_r - dist;
200    let normal = if dist > 1e-12 {
201        v3_scale(d, 1.0 / dist)
202    } else {
203        [0.0, 1.0, 0.0] // degenerate: coincident centres
204    };
205    let point = v3_add(a.center, v3_scale(normal, a.radius - depth * 0.5));
206    SphereSphereContact {
207        overlapping: depth > 0.0,
208        normal,
209        depth,
210        point,
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Sphere-Capsule test
216// ---------------------------------------------------------------------------
217
218/// Signed distance from point `p` to the line segment (a→b), and the closest
219/// point on the segment.
220pub fn point_segment_closest(p: [Real; 3], a: [Real; 3], b: [Real; 3]) -> ([Real; 3], Real) {
221    let ab = v3_sub(b, a);
222    let ap = v3_sub(p, a);
223    let t = (v3_dot(ap, ab) / v3_len_sq(ab).max(1e-30)).clamp(0.0, 1.0);
224    let closest = v3_add(a, v3_scale(ab, t));
225    let dist = v3_len(v3_sub(p, closest));
226    (closest, dist)
227}
228
229/// Test a sphere and capsule for intersection.
230/// Returns `(overlapping, depth, normal, point)`.
231pub fn sphere_capsule(sphere: &Sphere, cap: &Capsule) -> (bool, Real, [Real; 3], [Real; 3]) {
232    let (closest, dist) = point_segment_closest(sphere.center, cap.a, cap.b);
233    let depth = (sphere.radius + cap.radius) - dist;
234    let d = v3_sub(sphere.center, closest);
235    let normal = if dist > 1e-12 {
236        v3_scale(d, 1.0 / dist)
237    } else {
238        [0.0, 1.0, 0.0]
239    };
240    let point = v3_add(closest, v3_scale(normal, cap.radius - depth * 0.5));
241    (depth > 0.0, depth, normal, point)
242}
243
244// ---------------------------------------------------------------------------
245// AABB-AABB overlap
246// ---------------------------------------------------------------------------
247
248/// Axis-aligned bounding box, stored as `(min, max)`.
249#[derive(Debug, Clone, Copy)]
250pub struct AabbRaw {
251    /// Minimum corner.
252    pub min: [Real; 3],
253    /// Maximum corner.
254    pub max: [Real; 3],
255}
256
257impl AabbRaw {
258    /// Create a new AABB.
259    pub fn new(min: [Real; 3], max: [Real; 3]) -> Self {
260        Self { min, max }
261    }
262
263    /// Create from centre and half-extents.
264    pub fn from_center_half(center: [Real; 3], half: [Real; 3]) -> Self {
265        Self {
266            min: [
267                center[0] - half[0],
268                center[1] - half[1],
269                center[2] - half[2],
270            ],
271            max: [
272                center[0] + half[0],
273                center[1] + half[1],
274                center[2] + half[2],
275            ],
276        }
277    }
278
279    /// Test overlap with another AABB.
280    pub fn overlaps(&self, other: &AabbRaw) -> bool {
281        self.min[0] <= other.max[0]
282            && self.max[0] >= other.min[0]
283            && self.min[1] <= other.max[1]
284            && self.max[1] >= other.min[1]
285            && self.min[2] <= other.max[2]
286            && self.max[2] >= other.min[2]
287    }
288
289    /// Merge two AABBs.
290    pub fn merge(&self, other: &AabbRaw) -> Self {
291        Self {
292            min: [
293                self.min[0].min(other.min[0]),
294                self.min[1].min(other.min[1]),
295                self.min[2].min(other.min[2]),
296            ],
297            max: [
298                self.max[0].max(other.max[0]),
299                self.max[1].max(other.max[1]),
300                self.max[2].max(other.max[2]),
301            ],
302        }
303    }
304
305    /// Surface area (used for BVH cost heuristic).
306    pub fn surface_area(&self) -> Real {
307        let dx = self.max[0] - self.min[0];
308        let dy = self.max[1] - self.min[1];
309        let dz = self.max[2] - self.min[2];
310        2.0 * (dx * dy + dy * dz + dz * dx)
311    }
312
313    /// Centre of the AABB.
314    pub fn center(&self) -> [Real; 3] {
315        [
316            (self.min[0] + self.max[0]) * 0.5,
317            (self.min[1] + self.max[1]) * 0.5,
318            (self.min[2] + self.max[2]) * 0.5,
319        ]
320    }
321}
322
323// ---------------------------------------------------------------------------
324// BVH (bounding volume hierarchy) — simple top-down build
325// ---------------------------------------------------------------------------
326
327/// A leaf entry in the BVH.
328#[derive(Debug, Clone)]
329pub struct BvhLeaf {
330    /// Unique id of the object.
331    pub id: u64,
332    /// Tight AABB.
333    pub aabb: AabbRaw,
334}
335
336/// A node in the BVH tree.
337#[derive(Debug, Clone)]
338enum BvhNode {
339    Leaf {
340        id: u64,
341        aabb: AabbRaw,
342    },
343    Internal {
344        aabb: AabbRaw,
345        left: Box<BvhNode>,
346        right: Box<BvhNode>,
347    },
348}
349
350impl BvhNode {
351    fn aabb(&self) -> &AabbRaw {
352        match self {
353            BvhNode::Leaf { aabb, .. } => aabb,
354            BvhNode::Internal { aabb, .. } => aabb,
355        }
356    }
357
358    fn query_overlap(&self, query: &AabbRaw, out: &mut Vec<u64>) {
359        if !self.aabb().overlaps(query) {
360            return;
361        }
362        match self {
363            BvhNode::Leaf { id, .. } => out.push(*id),
364            BvhNode::Internal { left, right, .. } => {
365                left.query_overlap(query, out);
366                right.query_overlap(query, out);
367            }
368        }
369    }
370
371    fn all_pairs<'a>(&'a self, other: &'a BvhNode, out: &mut Vec<(u64, u64)>) {
372        if !self.aabb().overlaps(other.aabb()) {
373            return;
374        }
375        match (self, other) {
376            (BvhNode::Leaf { id: id_a, .. }, BvhNode::Leaf { id: id_b, .. }) => {
377                if id_a != id_b {
378                    out.push((*id_a.min(id_b), *id_a.max(id_b)));
379                }
380            }
381            (BvhNode::Internal { left, right, .. }, _) => {
382                left.all_pairs(other, out);
383                right.all_pairs(other, out);
384            }
385            (_, BvhNode::Internal { left, right, .. }) => {
386                self.all_pairs(left, out);
387                self.all_pairs(right, out);
388            }
389        }
390    }
391}
392
393/// A simple axis-aligned BVH built with a median-split strategy.
394pub struct Bvh {
395    root: Option<BvhNode>,
396}
397
398impl Bvh {
399    /// Build a new BVH from the given leaf list.
400    pub fn build(leaves: Vec<BvhLeaf>) -> Self {
401        if leaves.is_empty() {
402            return Self { root: None };
403        }
404        Self {
405            root: Some(Self::build_recursive(leaves)),
406        }
407    }
408
409    fn build_recursive(mut leaves: Vec<BvhLeaf>) -> BvhNode {
410        if leaves.len() == 1 {
411            let leaf = leaves.remove(0);
412            return BvhNode::Leaf {
413                id: leaf.id,
414                aabb: leaf.aabb,
415            };
416        }
417
418        // Compute encompassing AABB.
419        let mut aabb = leaves[0].aabb;
420        for l in &leaves[1..] {
421            aabb = aabb.merge(&l.aabb);
422        }
423
424        // Split along the longest axis.
425        let dx = aabb.max[0] - aabb.min[0];
426        let dy = aabb.max[1] - aabb.min[1];
427        let dz = aabb.max[2] - aabb.min[2];
428        let axis = if dx >= dy && dx >= dz {
429            0
430        } else if dy >= dz {
431            1
432        } else {
433            2
434        };
435
436        leaves.sort_by(|a, b| {
437            a.aabb.center()[axis]
438                .partial_cmp(&b.aabb.center()[axis])
439                .unwrap_or(std::cmp::Ordering::Equal)
440        });
441
442        let mid = leaves.len() / 2;
443        let right_leaves = leaves.split_off(mid);
444        let left_leaves = leaves;
445
446        let left = Box::new(Self::build_recursive(left_leaves));
447        let right = Box::new(Self::build_recursive(right_leaves));
448        BvhNode::Internal { aabb, left, right }
449    }
450
451    /// Return all ids whose AABB overlaps the given query AABB.
452    pub fn query(&self, aabb: &AabbRaw) -> Vec<u64> {
453        let mut result = Vec::new();
454        if let Some(root) = &self.root {
455            root.query_overlap(aabb, &mut result);
456        }
457        result
458    }
459
460    /// Return all overlapping pairs in the BVH (self-test).
461    pub fn overlapping_pairs(&self) -> Vec<(u64, u64)> {
462        let mut pairs = Vec::new();
463        if let Some(root) = &self.root {
464            root.all_pairs(root, &mut pairs);
465        }
466        // Deduplicate.
467        pairs.sort_unstable();
468        pairs.dedup();
469        pairs
470    }
471}
472
473// ---------------------------------------------------------------------------
474// GJK – Gilbert-Johnson-Keerthi distance algorithm
475// ---------------------------------------------------------------------------
476
477/// Minkowski difference support function for two convex shapes.
478fn gjk_support(a: &dyn ConvexShape, b: &dyn ConvexShape, dir: [Real; 3]) -> [Real; 3] {
479    v3_sub(a.support(dir), b.support(v3_neg(dir)))
480}
481
482/// Result of a GJK query.
483#[derive(Debug, Clone)]
484pub struct GjkResult {
485    /// Whether the shapes intersect (minimum distance is 0).
486    pub intersecting: bool,
487    /// Closest distance between the shapes (0 if intersecting).
488    pub distance: Real,
489    /// Closest point on shape A (only valid when not intersecting).
490    pub closest_a: [Real; 3],
491    /// Closest point on shape B (only valid when not intersecting).
492    pub closest_b: [Real; 3],
493}
494
495/// Run the GJK algorithm to determine if two convex shapes intersect and,
496/// if not, compute their minimum distance.
497///
498/// Returns `GjkResult` with the intersection flag and minimum distance.
499pub fn gjk(a: &dyn ConvexShape, b: &dyn ConvexShape) -> GjkResult {
500    const MAX_ITER: usize = 64;
501    const EPS: Real = 1e-10;
502
503    let mut simplex: Vec<[Real; 3]> = Vec::with_capacity(4);
504    let mut dir = [1.0, 0.0, 0.0_f64];
505
506    let first = gjk_support(a, b, dir);
507    simplex.push(first);
508    dir = v3_neg(first);
509
510    for _ in 0..MAX_ITER {
511        let len_sq = v3_len_sq(dir);
512        if len_sq < EPS {
513            // Direction degenerate — intersection at origin.
514            return GjkResult {
515                intersecting: true,
516                distance: 0.0,
517                closest_a: [0.0; 3],
518                closest_b: [0.0; 3],
519            };
520        }
521
522        let support = gjk_support(a, b, dir);
523
524        // If the new support does not pass the origin, shapes do not intersect.
525        if v3_dot(support, dir) < 0.0 {
526            let dist = v3_len_sq(dir).sqrt();
527            return GjkResult {
528                intersecting: false,
529                distance: dist,
530                closest_a: a.support(dir),
531                closest_b: b.support(v3_neg(dir)),
532            };
533        }
534
535        simplex.push(support);
536
537        if gjk_do_simplex(&mut simplex, &mut dir) {
538            return GjkResult {
539                intersecting: true,
540                distance: 0.0,
541                closest_a: [0.0; 3],
542                closest_b: [0.0; 3],
543            };
544        }
545    }
546
547    // Fallback: treat as intersecting.
548    GjkResult {
549        intersecting: true,
550        distance: 0.0,
551        closest_a: [0.0; 3],
552        closest_b: [0.0; 3],
553    }
554}
555
556/// Update the GJK simplex and direction; returns `true` if the origin is inside.
557fn gjk_do_simplex(simplex: &mut Vec<[Real; 3]>, dir: &mut [Real; 3]) -> bool {
558    match simplex.len() {
559        2 => gjk_line_case(simplex, dir),
560        3 => gjk_triangle_case(simplex, dir),
561        4 => gjk_tetrahedron_case(simplex, dir),
562        _ => false,
563    }
564}
565
566fn gjk_line_case(simplex: &mut Vec<[Real; 3]>, dir: &mut [Real; 3]) -> bool {
567    let b = simplex[0];
568    let a = simplex[1];
569    let ab = v3_sub(b, a);
570    let ao = v3_neg(a);
571    if v3_dot(ab, ao) > 0.0 {
572        // Origin is between A and B.
573        *dir = v3_sub(v3_scale(ab, v3_dot(ab, ao) / v3_dot(ab, ab)), ao);
574    } else {
575        simplex.clear();
576        simplex.push(a);
577        *dir = ao;
578    }
579    false
580}
581
582fn gjk_triangle_case(simplex: &mut Vec<[Real; 3]>, dir: &mut [Real; 3]) -> bool {
583    let c = simplex[0];
584    let b = simplex[1];
585    let a = simplex[2];
586    let ab = v3_sub(b, a);
587    let ac = v3_sub(c, a);
588    let ao = v3_neg(a);
589    let abc = v3_cross(ab, ac);
590
591    // Check if origin is outside edge AC.
592    if v3_dot(v3_cross(abc, ac), ao) > 0.0 {
593        if v3_dot(ac, ao) > 0.0 {
594            simplex.clear();
595            simplex.push(c);
596            simplex.push(a);
597            *dir = v3_sub(v3_scale(ac, v3_dot(ac, ao) / v3_dot(ac, ac)), ao);
598        } else {
599            simplex.clear();
600            simplex.push(b);
601            simplex.push(a);
602            return gjk_line_case(simplex, dir);
603        }
604    } else if v3_dot(v3_cross(ab, abc), ao) > 0.0 {
605        simplex.clear();
606        simplex.push(b);
607        simplex.push(a);
608        return gjk_line_case(simplex, dir);
609    } else {
610        // Origin is above or below the triangle.
611        if v3_dot(abc, ao) > 0.0 {
612            *dir = abc;
613        } else {
614            simplex.swap(0, 1); // flip winding
615            *dir = v3_neg(abc);
616        }
617    }
618    false
619}
620
621fn gjk_tetrahedron_case(simplex: &mut Vec<[Real; 3]>, dir: &mut [Real; 3]) -> bool {
622    let d = simplex[0];
623    let c = simplex[1];
624    let b = simplex[2];
625    let a = simplex[3];
626    let ab = v3_sub(b, a);
627    let ac = v3_sub(c, a);
628    let ad = v3_sub(d, a);
629    let ao = v3_neg(a);
630
631    let abc = v3_cross(ab, ac);
632    let acd = v3_cross(ac, ad);
633    let adb = v3_cross(ad, ab);
634
635    if v3_dot(abc, ao) > 0.0 {
636        simplex.clear();
637        simplex.push(c);
638        simplex.push(b);
639        simplex.push(a);
640        return gjk_triangle_case(simplex, dir);
641    }
642    if v3_dot(acd, ao) > 0.0 {
643        simplex.clear();
644        simplex.push(d);
645        simplex.push(c);
646        simplex.push(a);
647        return gjk_triangle_case(simplex, dir);
648    }
649    if v3_dot(adb, ao) > 0.0 {
650        simplex.clear();
651        simplex.push(b);
652        simplex.push(d);
653        simplex.push(a);
654        return gjk_triangle_case(simplex, dir);
655    }
656    // Origin is inside the tetrahedron.
657    true
658}
659
660// ---------------------------------------------------------------------------
661// SAT (Separating Axis Theorem) – OBB vs OBB
662// ---------------------------------------------------------------------------
663
664/// An oriented bounding box.
665#[derive(Debug, Clone)]
666pub struct Obb {
667    /// Centre position.
668    pub center: [Real; 3],
669    /// Three normalised local axes (columns of the rotation matrix).
670    pub axes: [[Real; 3]; 3],
671    /// Half-extents along each axis.
672    pub half_extents: [Real; 3],
673}
674
675impl Obb {
676    /// Create a new OBB.
677    pub fn new(center: [Real; 3], axes: [[Real; 3]; 3], half_extents: [Real; 3]) -> Self {
678        Self {
679            center,
680            axes,
681            half_extents,
682        }
683    }
684
685    /// Create an axis-aligned OBB (identity rotation).
686    pub fn axis_aligned(center: [Real; 3], half_extents: [Real; 3]) -> Self {
687        Self {
688            center,
689            axes: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
690            half_extents,
691        }
692    }
693
694    /// Project the OBB onto axis `ax` and return (min, max).
695    fn project(&self, ax: [Real; 3]) -> (Real, Real) {
696        let c = v3_dot(self.center, ax);
697        let r = self
698            .axes
699            .iter()
700            .zip(self.half_extents.iter())
701            .map(|(&a, &h)| v3_dot(a, ax).abs() * h)
702            .sum::<Real>();
703        (c - r, c + r)
704    }
705
706    /// Return the OBB's 8 corner vertices in world space.
707    pub fn vertices(&self) -> [[Real; 3]; 8] {
708        let mut verts = [[0.0; 3]; 8];
709        let signs = [
710            [-1.0, -1.0, -1.0],
711            [1.0, -1.0, -1.0],
712            [-1.0, 1.0, -1.0],
713            [1.0, 1.0, -1.0],
714            [-1.0, -1.0, 1.0],
715            [1.0, -1.0, 1.0],
716            [-1.0, 1.0, 1.0],
717            [1.0, 1.0, 1.0],
718        ];
719        for (i, s) in signs.iter().enumerate() {
720            let mut v = self.center;
721            for k in 0..3 {
722                v = v3_add(v, v3_scale(self.axes[k], s[k] * self.half_extents[k]));
723            }
724            verts[i] = v;
725        }
726        verts
727    }
728}
729
730/// Test two OBBs using the Separating Axis Theorem.
731/// Returns `None` if separated, or `Some(depth, axis)` with penetration info.
732pub fn obb_obb_sat(a: &Obb, b: &Obb) -> Option<(Real, [Real; 3])> {
733    let mut min_depth = Real::MAX;
734    let mut min_axis = [0.0; 3];
735
736    // Helper: test a single axis.
737    let mut test_axis = |ax: [Real; 3]| -> bool {
738        let len_sq = v3_len_sq(ax);
739        if len_sq < 1e-10 {
740            return true; // degenerate axis; skip
741        }
742        let ax = v3_scale(ax, 1.0 / len_sq.sqrt());
743        let (a_min, a_max) = a.project(ax);
744        let (b_min, b_max) = b.project(ax);
745        if a_max < b_min || b_max < a_min {
746            return false; // separated
747        }
748        let depth = (a_max.min(b_max) - a_min.max(b_min))
749            .min(a_max - a_min)
750            .min(b_max - b_min);
751        if depth < min_depth {
752            min_depth = depth;
753            min_axis = ax;
754        }
755        true
756    };
757
758    // 3 face axes of A
759    for i in 0..3 {
760        if !test_axis(a.axes[i]) {
761            return None;
762        }
763    }
764    // 3 face axes of B
765    for i in 0..3 {
766        if !test_axis(b.axes[i]) {
767            return None;
768        }
769    }
770    // 9 cross-product edge axes
771    for i in 0..3 {
772        for j in 0..3 {
773            let ax = v3_cross(a.axes[i], b.axes[j]);
774            if !test_axis(ax) {
775                return None;
776            }
777        }
778    }
779
780    Some((min_depth, min_axis))
781}
782
783// ---------------------------------------------------------------------------
784// Ray cast
785// ---------------------------------------------------------------------------
786
787/// Result of a ray–AABB intersection test.
788#[derive(Debug, Clone)]
789pub struct RayAabbHit {
790    /// Parameter `t_min` along the ray at entry.
791    pub t_min: Real,
792    /// Parameter `t_max` along the ray at exit.
793    pub t_max: Real,
794}
795
796/// Intersect a ray `(origin, dir)` with an AABB.
797///
798/// Returns `Some(hit)` if the ray hits the box; the ray is infinite.
799pub fn ray_aabb(origin: [Real; 3], dir: [Real; 3], aabb: &AabbRaw) -> Option<RayAabbHit> {
800    let mut t_min = Real::NEG_INFINITY;
801    let mut t_max = Real::INFINITY;
802
803    for i in 0..3 {
804        if dir[i].abs() < 1e-12 {
805            // Ray is parallel to slab.
806            if origin[i] < aabb.min[i] || origin[i] > aabb.max[i] {
807                return None;
808            }
809        } else {
810            let inv_d = 1.0 / dir[i];
811            let t1 = (aabb.min[i] - origin[i]) * inv_d;
812            let t2 = (aabb.max[i] - origin[i]) * inv_d;
813            let (ta, tb) = if t1 < t2 { (t1, t2) } else { (t2, t1) };
814            t_min = t_min.max(ta);
815            t_max = t_max.min(tb);
816            if t_min > t_max {
817                return None;
818            }
819        }
820    }
821    Some(RayAabbHit { t_min, t_max })
822}
823
824/// Intersect a ray with a sphere.  Returns parameter `t` at first hit or `None`.
825pub fn ray_sphere(origin: [Real; 3], dir: [Real; 3], sphere: &Sphere) -> Option<Real> {
826    let oc = v3_sub(origin, sphere.center);
827    let a = v3_dot(dir, dir);
828    let b = 2.0 * v3_dot(oc, dir);
829    let c = v3_dot(oc, oc) - sphere.radius * sphere.radius;
830    let discriminant = b * b - 4.0 * a * c;
831    if discriminant < 0.0 {
832        return None;
833    }
834    let sqrt_d = discriminant.sqrt();
835    let t0 = (-b - sqrt_d) / (2.0 * a);
836    let t1 = (-b + sqrt_d) / (2.0 * a);
837    // Return the nearest positive t.
838    if t0 > 0.0 {
839        Some(t0)
840    } else if t1 > 0.0 {
841        Some(t1)
842    } else {
843        None
844    }
845}
846
847// ---------------------------------------------------------------------------
848// Contact manifold
849// ---------------------------------------------------------------------------
850
851/// A collision contact point.
852#[derive(Debug, Clone)]
853pub struct Contact {
854    /// Contact point in world space.
855    pub point: [Real; 3],
856    /// Contact normal (from B to A, normalised).
857    pub normal: [Real; 3],
858    /// Penetration depth.
859    pub depth: Real,
860    /// Id of the first object.
861    pub id_a: u64,
862    /// Id of the second object.
863    pub id_b: u64,
864}
865
866/// A contact manifold: up to 4 persistent contact points between two bodies.
867#[derive(Debug, Clone, Default)]
868pub struct ContactManifold {
869    /// Up to 4 contact points.
870    pub contacts: Vec<Contact>,
871}
872
873impl ContactManifold {
874    /// Create an empty manifold.
875    pub fn new() -> Self {
876        Self::default()
877    }
878
879    /// Add a contact, replacing the least significant one if there are already 4.
880    pub fn add_contact(&mut self, contact: Contact) {
881        if self.contacts.len() < 4 {
882            self.contacts.push(contact);
883        } else {
884            // Replace the contact with the smallest depth.
885            if let Some(min_idx) = self
886                .contacts
887                .iter()
888                .enumerate()
889                .min_by(|(_, a), (_, b)| {
890                    a.depth
891                        .partial_cmp(&b.depth)
892                        .unwrap_or(std::cmp::Ordering::Equal)
893                })
894                .map(|(i, _)| i)
895            {
896                let new_depth = contact.depth;
897                if new_depth > self.contacts[min_idx].depth {
898                    self.contacts[min_idx] = contact;
899                }
900            }
901        }
902    }
903
904    /// Return the average contact normal.
905    pub fn average_normal(&self) -> [Real; 3] {
906        if self.contacts.is_empty() {
907            return [0.0, 1.0, 0.0];
908        }
909        let mut n = [0.0; 3];
910        for c in &self.contacts {
911            n[0] += c.normal[0];
912            n[1] += c.normal[1];
913            n[2] += c.normal[2];
914        }
915        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
916        if len > 1e-12 {
917            [n[0] / len, n[1] / len, n[2] / len]
918        } else {
919            [0.0, 1.0, 0.0]
920        }
921    }
922
923    /// Maximum penetration depth across all contacts.
924    pub fn max_depth(&self) -> Real {
925        self.contacts
926            .iter()
927            .map(|c| c.depth)
928            .fold(Real::NEG_INFINITY, Real::max)
929    }
930
931    /// Whether there are any contacts.
932    pub fn is_empty(&self) -> bool {
933        self.contacts.is_empty()
934    }
935}
936
937// ---------------------------------------------------------------------------
938// EPA – Expanding Polytope Algorithm (penetration depth)
939// ---------------------------------------------------------------------------
940
941/// Result of an EPA penetration-depth query.
942#[derive(Debug, Clone)]
943pub struct EpaResult {
944    /// Penetration depth (positive when overlapping).
945    pub depth: Real,
946    /// Collision normal (from B towards A, normalised).
947    pub normal: [Real; 3],
948    /// Contact point estimate.
949    pub point: [Real; 3],
950}
951
952/// Run EPA to compute the penetration depth between two overlapping convex shapes.
953///
954/// Requires that GJK has confirmed the shapes are intersecting.
955/// Returns `None` if the shapes are not intersecting.
956pub fn epa(a: &dyn ConvexShape, b: &dyn ConvexShape) -> Option<EpaResult> {
957    // First confirm intersection with GJK.
958    let gjk_r = gjk(a, b);
959    if !gjk_r.intersecting {
960        return None;
961    }
962
963    const MAX_ITER: usize = 64;
964    const EPS: Real = 1e-8;
965
966    // Seed the polytope with a tetrahedron constructed from support points.
967    let dirs = [
968        [1.0, 0.0, 0.0_f64],
969        [-1.0, 0.0, 0.0],
970        [0.0, 1.0, 0.0],
971        [0.0, -1.0, 0.0],
972        [0.0, 0.0, 1.0],
973        [0.0, 0.0, -1.0],
974    ];
975
976    let mut polytope: Vec<[Real; 3]> = dirs.iter().map(|&d| gjk_support(a, b, d)).collect();
977
978    // Build initial faces (pairs of triangles from the seed points).
979    // Simple approach: build from all unique triangles in the convex hull of polytope.
980    // For EPA we iteratively expand closest face.
981
982    let mut faces: Vec<[usize; 3]> = vec![
983        [0, 1, 2],
984        [0, 2, 3],
985        [0, 3, 4],
986        [0, 4, 1],
987        [5, 2, 1],
988        [5, 3, 2],
989        [5, 4, 3],
990        [5, 1, 4],
991    ];
992
993    for _ in 0..MAX_ITER {
994        // Find the face closest to the origin.
995        let mut min_dist = Real::MAX;
996        let mut min_idx = 0;
997        let mut min_normal = [0.0_f64; 3];
998
999        for (fi, face) in faces.iter().enumerate() {
1000            let a_pt = polytope[face[0]];
1001            let b_pt = polytope[face[1]];
1002            let c_pt = polytope[face[2]];
1003            let ab = v3_sub(b_pt, a_pt);
1004            let ac = v3_sub(c_pt, a_pt);
1005            let n = v3_normalize(v3_cross(ab, ac));
1006            let d = v3_dot(n, a_pt).abs();
1007            if d < min_dist {
1008                min_dist = d;
1009                min_idx = fi;
1010                min_normal = n;
1011            }
1012        }
1013
1014        // Support in the direction of the closest face normal.
1015        let support = gjk_support(a, b, min_normal);
1016        let new_dist = v3_dot(min_normal, support);
1017
1018        if (new_dist - min_dist).abs() < EPS {
1019            // Converged.
1020            let contact = v3_scale(min_normal, min_dist);
1021            return Some(EpaResult {
1022                depth: min_dist,
1023                normal: min_normal,
1024                point: contact,
1025            });
1026        }
1027
1028        // Expand polytope: remove visible faces and add new faces with the new vertex.
1029        let new_idx = polytope.len();
1030        polytope.push(support);
1031
1032        let mut new_faces: Vec<[usize; 3]> = Vec::new();
1033        let mut edges: Vec<(usize, usize)> = Vec::new();
1034
1035        for (fi, &face) in faces.iter().enumerate() {
1036            if fi == min_idx {
1037                // Record edges of the removed face (horizon).
1038                edges.push((face[0], face[1]));
1039                edges.push((face[1], face[2]));
1040                edges.push((face[2], face[0]));
1041            } else {
1042                new_faces.push(face);
1043            }
1044        }
1045
1046        // Add new faces connecting the new vertex to the horizon edges.
1047        for (ea, eb) in edges {
1048            new_faces.push([ea, eb, new_idx]);
1049        }
1050        faces = new_faces;
1051    }
1052
1053    // Fallback after max iterations.
1054    let normal = [0.0, 1.0, 0.0];
1055    Some(EpaResult {
1056        depth: 0.0,
1057        normal,
1058        point: [0.0; 3],
1059    })
1060}
1061
1062// ---------------------------------------------------------------------------
1063// Minkowski sum support function
1064// ---------------------------------------------------------------------------
1065
1066/// Support function of the Minkowski sum of two shapes.
1067///
1068/// `support_mink(A⊕B, d) = support_A(d) + support_B(d)`.
1069pub fn minkowski_sum_support(
1070    a: &dyn ConvexShape,
1071    b: &dyn ConvexShape,
1072    dir: [Real; 3],
1073) -> [Real; 3] {
1074    v3_add(a.support(dir), b.support(dir))
1075}
1076
1077// ---------------------------------------------------------------------------
1078// Shape casting (linear motion CCD)
1079// ---------------------------------------------------------------------------
1080
1081/// Cast a moving sphere along `velocity` direction (magnitude = max travel distance)
1082/// against a static sphere.  Returns the first time of contact `t ∈ [0, max_t]`
1083/// or `None` if no contact.
1084pub fn shape_cast_sphere_vs_sphere(
1085    moving: &Sphere,
1086    velocity: [Real; 3],
1087    target: &Sphere,
1088    max_t: Real,
1089) -> Option<Real> {
1090    // Relative motion: relative velocity = velocity (target is static).
1091    // Reduce to: find t such that |moving.center + t*vel - target.center| = r_a + r_b.
1092    let oc = v3_sub(moving.center, target.center);
1093    let combined_r = moving.radius + target.radius;
1094
1095    let a = v3_dot(velocity, velocity);
1096    let b = 2.0 * v3_dot(oc, velocity);
1097    let c = v3_dot(oc, oc) - combined_r * combined_r;
1098
1099    if a < 1e-15 {
1100        // No motion.
1101        return if c <= 0.0 { Some(0.0) } else { None };
1102    }
1103
1104    let disc = b * b - 4.0 * a * c;
1105    if disc < 0.0 {
1106        return None;
1107    }
1108
1109    let sqrt_d = disc.sqrt();
1110    let t0 = (-b - sqrt_d) / (2.0 * a);
1111    let t1 = (-b + sqrt_d) / (2.0 * a);
1112
1113    // Return smallest non-negative t within [0, max_t].
1114    for t in [t0, t1] {
1115        if t >= 0.0 && t <= max_t {
1116            return Some(t);
1117        }
1118    }
1119    // Already overlapping (c <= 0) → t = 0.
1120    if c <= 0.0 { Some(0.0) } else { None }
1121}
1122
1123/// Cast a sphere along `velocity` against an AABB.  Returns `t ∈ [0, max_t]` or `None`.
1124///
1125/// Implemented as a ray cast against the Minkowski-expanded AABB.
1126pub fn shape_cast_sphere_vs_aabb(
1127    sphere: &Sphere,
1128    velocity: [Real; 3],
1129    aabb: &AabbRaw,
1130    max_t: Real,
1131) -> Option<Real> {
1132    // Expand AABB by sphere radius.
1133    let r = sphere.radius;
1134    let expanded = AabbRaw::new(
1135        [aabb.min[0] - r, aabb.min[1] - r, aabb.min[2] - r],
1136        [aabb.max[0] + r, aabb.max[1] + r, aabb.max[2] + r],
1137    );
1138    let hit = ray_aabb(sphere.center, velocity, &expanded)?;
1139    if hit.t_min < 0.0 || hit.t_min > max_t {
1140        return None;
1141    }
1142    Some(hit.t_min.max(0.0))
1143}
1144
1145// ---------------------------------------------------------------------------
1146// Triangle convex shape
1147// ---------------------------------------------------------------------------
1148
1149/// A triangle as a convex shape (degenerate 2-D polyhedron).
1150#[derive(Debug, Clone)]
1151pub struct Triangle {
1152    /// Vertices of the triangle.
1153    pub vertices: [[Real; 3]; 3],
1154}
1155
1156impl Triangle {
1157    /// Create a new triangle.
1158    pub fn new(v0: [Real; 3], v1: [Real; 3], v2: [Real; 3]) -> Self {
1159        Self {
1160            vertices: [v0, v1, v2],
1161        }
1162    }
1163
1164    /// Outward normal of the triangle (not normalised).
1165    pub fn normal(&self) -> [Real; 3] {
1166        let e1 = v3_sub(self.vertices[1], self.vertices[0]);
1167        let e2 = v3_sub(self.vertices[2], self.vertices[0]);
1168        v3_cross(e1, e2)
1169    }
1170}
1171
1172impl ConvexShape for Triangle {
1173    fn support(&self, dir: [Real; 3]) -> [Real; 3] {
1174        self.vertices
1175            .iter()
1176            .copied()
1177            .max_by(|&a, &b| {
1178                v3_dot(a, dir)
1179                    .partial_cmp(&v3_dot(b, dir))
1180                    .unwrap_or(std::cmp::Ordering::Equal)
1181            })
1182            .unwrap_or([0.0; 3])
1183    }
1184}
1185
1186// ---------------------------------------------------------------------------
1187// Convex hull shape (point cloud)
1188// ---------------------------------------------------------------------------
1189
1190/// A convex shape defined by a set of points.
1191///
1192/// The support function iterates all points; for small point sets this is fine.
1193#[derive(Debug, Clone)]
1194pub struct ConvexPointCloud {
1195    /// Points defining the convex hull.
1196    pub points: Vec<[Real; 3]>,
1197}
1198
1199impl ConvexPointCloud {
1200    /// Create a new convex point cloud.
1201    pub fn new(points: Vec<[Real; 3]>) -> Self {
1202        Self { points }
1203    }
1204}
1205
1206impl ConvexShape for ConvexPointCloud {
1207    fn support(&self, dir: [Real; 3]) -> [Real; 3] {
1208        self.points
1209            .iter()
1210            .copied()
1211            .max_by(|&a, &b| {
1212                v3_dot(a, dir)
1213                    .partial_cmp(&v3_dot(b, dir))
1214                    .unwrap_or(std::cmp::Ordering::Equal)
1215            })
1216            .unwrap_or([0.0; 3])
1217    }
1218}
1219
1220// ---------------------------------------------------------------------------
1221// Tests
1222// ---------------------------------------------------------------------------
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227
1228    // ── Vector helpers ────────────────────────────────────────────────────────
1229
1230    #[test]
1231    fn test_v3_dot_orthogonal() {
1232        assert_eq!(v3_dot([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), 0.0);
1233    }
1234
1235    #[test]
1236    fn test_v3_cross_basis() {
1237        let c = v3_cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1238        assert!((c[2] - 1.0).abs() < 1e-12);
1239        assert!(c[0].abs() < 1e-12 && c[1].abs() < 1e-12);
1240    }
1241
1242    #[test]
1243    fn test_v3_normalize_unit() {
1244        let n = v3_normalize([3.0, 4.0, 0.0]);
1245        let len = v3_len(n);
1246        assert!((len - 1.0).abs() < 1e-12, "len={}", len);
1247    }
1248
1249    #[test]
1250    fn test_v3_normalize_zero_safe() {
1251        let n = v3_normalize([0.0; 3]);
1252        assert_eq!(n, [0.0; 3]);
1253    }
1254
1255    // ── Sphere-Sphere ─────────────────────────────────────────────────────────
1256
1257    #[test]
1258    fn test_sphere_sphere_overlap() {
1259        let a = Sphere::new([0.0; 3], 1.0);
1260        let b = Sphere::new([1.5, 0.0, 0.0], 1.0);
1261        let r = sphere_sphere(&a, &b);
1262        assert!(r.overlapping, "spheres should overlap");
1263        assert!(r.depth > 0.0, "depth should be positive");
1264    }
1265
1266    #[test]
1267    fn test_sphere_sphere_no_overlap() {
1268        let a = Sphere::new([0.0; 3], 1.0);
1269        let b = Sphere::new([5.0, 0.0, 0.0], 1.0);
1270        let r = sphere_sphere(&a, &b);
1271        assert!(!r.overlapping);
1272        assert!(r.depth < 0.0);
1273    }
1274
1275    #[test]
1276    fn test_sphere_sphere_touching() {
1277        let a = Sphere::new([0.0; 3], 1.0);
1278        let b = Sphere::new([2.0, 0.0, 0.0], 1.0);
1279        let r = sphere_sphere(&a, &b);
1280        assert!((r.depth).abs() < 1e-10, "depth should be ~0 at touching");
1281    }
1282
1283    // ── Sphere-Capsule ────────────────────────────────────────────────────────
1284
1285    #[test]
1286    fn test_sphere_capsule_overlap() {
1287        let s = Sphere::new([0.0; 3], 0.5);
1288        let c = Capsule::new([0.0, 2.0, 0.0], [0.0, 4.0, 0.0], 0.5);
1289        let (overlap, depth, _, _) = sphere_capsule(&s, &c);
1290        assert!(!overlap, "should not overlap, depth={}", depth);
1291    }
1292
1293    #[test]
1294    fn test_sphere_capsule_nearby() {
1295        let s = Sphere::new([0.0, 1.0, 0.0], 0.6);
1296        let c = Capsule::new([0.0, 0.0, 0.0], [0.0, 3.0, 0.0], 0.6);
1297        let (overlap, depth, _, _) = sphere_capsule(&s, &c);
1298        assert!(overlap, "should overlap, depth={}", depth);
1299    }
1300
1301    // ── AABB ─────────────────────────────────────────────────────────────────
1302
1303    #[test]
1304    fn test_aabb_overlap() {
1305        let a = AabbRaw::new([-1.0; 3], [1.0; 3]);
1306        let b = AabbRaw::new([0.5, 0.5, 0.5], [2.0, 2.0, 2.0]);
1307        assert!(a.overlaps(&b));
1308    }
1309
1310    #[test]
1311    fn test_aabb_no_overlap() {
1312        let a = AabbRaw::new([-1.0; 3], [1.0; 3]);
1313        let b = AabbRaw::new([2.0; 3], [3.0; 3]);
1314        assert!(!a.overlaps(&b));
1315    }
1316
1317    #[test]
1318    fn test_aabb_merge_contains_both() {
1319        let a = AabbRaw::new([-2.0; 3], [0.0; 3]);
1320        let b = AabbRaw::new([1.0; 3], [3.0; 3]);
1321        let m = a.merge(&b);
1322        assert!(m.min[0] <= -2.0 && m.max[0] >= 3.0);
1323    }
1324
1325    // ── BVH ──────────────────────────────────────────────────────────────────
1326
1327    #[test]
1328    fn test_bvh_query_single_match() {
1329        let leaves = vec![
1330            BvhLeaf {
1331                id: 1,
1332                aabb: AabbRaw::new([0.0; 3], [1.0; 3]),
1333            },
1334            BvhLeaf {
1335                id: 2,
1336                aabb: AabbRaw::new([5.0; 3], [6.0; 3]),
1337            },
1338        ];
1339        let bvh = Bvh::build(leaves);
1340        let query = AabbRaw::new([0.0; 3], [0.5; 3]);
1341        let hits = bvh.query(&query);
1342        assert_eq!(hits, vec![1]);
1343    }
1344
1345    #[test]
1346    fn test_bvh_query_no_match() {
1347        let leaves = vec![BvhLeaf {
1348            id: 1,
1349            aabb: AabbRaw::new([10.0; 3], [11.0; 3]),
1350        }];
1351        let bvh = Bvh::build(leaves);
1352        let query = AabbRaw::new([0.0; 3], [1.0; 3]);
1353        assert!(bvh.query(&query).is_empty());
1354    }
1355
1356    #[test]
1357    fn test_bvh_empty() {
1358        let bvh = Bvh::build(vec![]);
1359        let query = AabbRaw::new([0.0; 3], [1.0; 3]);
1360        assert!(bvh.query(&query).is_empty());
1361    }
1362
1363    // ── OBB SAT ───────────────────────────────────────────────────────────────
1364
1365    #[test]
1366    fn test_obb_sat_overlap() {
1367        let a = Obb::axis_aligned([0.0; 3], [1.0; 3]);
1368        let b = Obb::axis_aligned([1.5, 0.0, 0.0], [1.0; 3]);
1369        assert!(obb_obb_sat(&a, &b).is_some(), "overlapping OBBs");
1370    }
1371
1372    #[test]
1373    fn test_obb_sat_separated() {
1374        let a = Obb::axis_aligned([0.0; 3], [1.0; 3]);
1375        let b = Obb::axis_aligned([5.0, 0.0, 0.0], [1.0; 3]);
1376        assert!(obb_obb_sat(&a, &b).is_none(), "separated OBBs");
1377    }
1378
1379    // ── Ray tests ─────────────────────────────────────────────────────────────
1380
1381    #[test]
1382    fn test_ray_aabb_hit() {
1383        let origin = [-5.0, 0.0, 0.0];
1384        let dir = [1.0, 0.0, 0.0];
1385        let aabb = AabbRaw::new([-1.0; 3], [1.0; 3]);
1386        let hit = ray_aabb(origin, dir, &aabb);
1387        assert!(hit.is_some());
1388        let h = hit.unwrap();
1389        assert!(h.t_min < h.t_max);
1390    }
1391
1392    #[test]
1393    fn test_ray_aabb_miss() {
1394        let origin = [0.0, 5.0, 0.0];
1395        let dir = [1.0, 0.0, 0.0];
1396        let aabb = AabbRaw::new([-1.0; 3], [1.0; 3]);
1397        assert!(ray_aabb(origin, dir, &aabb).is_none());
1398    }
1399
1400    #[test]
1401    fn test_ray_sphere_hit() {
1402        let s = Sphere::new([0.0; 3], 1.0);
1403        let t = ray_sphere([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], &s);
1404        assert!(t.is_some());
1405        assert!((t.unwrap() - 4.0).abs() < 1e-10);
1406    }
1407
1408    #[test]
1409    fn test_ray_sphere_miss() {
1410        let s = Sphere::new([0.0; 3], 1.0);
1411        let t = ray_sphere([0.0, 5.0, 0.0], [1.0, 0.0, 0.0], &s);
1412        assert!(t.is_none());
1413    }
1414
1415    // ── GJK ──────────────────────────────────────────────────────────────────
1416
1417    #[test]
1418    fn test_gjk_spheres_intersecting() {
1419        let a = Sphere::new([0.0; 3], 1.5);
1420        let b = Sphere::new([1.0, 0.0, 0.0], 1.5);
1421        let r = gjk(&a, &b);
1422        assert!(r.intersecting);
1423    }
1424
1425    #[test]
1426    fn test_gjk_spheres_separated() {
1427        let a = Sphere::new([0.0; 3], 0.5);
1428        let b = Sphere::new([10.0, 0.0, 0.0], 0.5);
1429        let r = gjk(&a, &b);
1430        assert!(!r.intersecting);
1431        assert!(r.distance > 0.0);
1432    }
1433
1434    // ── ContactManifold ───────────────────────────────────────────────────────
1435
1436    #[test]
1437    fn test_manifold_add_and_max_depth() {
1438        let mut m = ContactManifold::new();
1439        m.add_contact(Contact {
1440            point: [0.0; 3],
1441            normal: [0.0, 1.0, 0.0],
1442            depth: 0.1,
1443            id_a: 1,
1444            id_b: 2,
1445        });
1446        m.add_contact(Contact {
1447            point: [1.0, 0.0, 0.0],
1448            normal: [0.0, 1.0, 0.0],
1449            depth: 0.3,
1450            id_a: 1,
1451            id_b: 2,
1452        });
1453        assert_eq!(m.contacts.len(), 2);
1454        assert!((m.max_depth() - 0.3).abs() < 1e-12);
1455    }
1456
1457    #[test]
1458    fn test_manifold_average_normal_unit() {
1459        let mut m = ContactManifold::new();
1460        for _ in 0..3 {
1461            m.add_contact(Contact {
1462                point: [0.0; 3],
1463                normal: [0.0, 1.0, 0.0],
1464                depth: 0.1,
1465                id_a: 1,
1466                id_b: 2,
1467            });
1468        }
1469        let n = m.average_normal();
1470        let len = v3_len(n);
1471        assert!((len - 1.0).abs() < 1e-12);
1472    }
1473
1474    // ── EPA tests ─────────────────────────────────────────────────────────────
1475
1476    #[test]
1477    fn test_epa_two_overlapping_spheres() {
1478        let a = Sphere::new([0.0; 3], 1.5);
1479        let b = Sphere::new([1.0, 0.0, 0.0], 1.5);
1480        let result = epa(&a, &b);
1481        assert!(
1482            result.is_some(),
1483            "EPA should return penetration for overlapping spheres"
1484        );
1485        let ep = result.unwrap();
1486        assert!(ep.depth >= 0.0, "penetration depth should be non-negative");
1487    }
1488
1489    #[test]
1490    fn test_epa_concentric_spheres() {
1491        let a = Sphere::new([0.0; 3], 2.0);
1492        let b = Sphere::new([0.0; 3], 1.0);
1493        let result = epa(&a, &b);
1494        // Concentric spheres: deep penetration
1495        assert!(result.is_some());
1496    }
1497
1498    #[test]
1499    fn test_epa_box_sphere_overlap() {
1500        let b = Box3::new([0.0; 3], [1.0; 3]);
1501        let s = Sphere::new([0.5, 0.0, 0.0], 0.8);
1502        let result = epa(&b, &s);
1503        assert!(
1504            result.is_some(),
1505            "overlapping box and sphere should produce EPA result"
1506        );
1507    }
1508
1509    // ── Support function framework ────────────────────────────────────────────
1510
1511    #[test]
1512    fn test_support_sphere_axis_aligned() {
1513        let s = Sphere::new([1.0, 2.0, 3.0], 2.0);
1514        let sp_x = s.support([1.0, 0.0, 0.0]);
1515        assert!(
1516            (sp_x[0] - 3.0).abs() < 1e-10,
1517            "x support = center.x + radius"
1518        );
1519        let sp_y = s.support([0.0, 1.0, 0.0]);
1520        assert!(
1521            (sp_y[1] - 4.0).abs() < 1e-10,
1522            "y support = center.y + radius"
1523        );
1524    }
1525
1526    #[test]
1527    fn test_support_box_positive_axes() {
1528        let b = Box3::new([0.0; 3], [2.0, 3.0, 4.0]);
1529        let sp = b.support([1.0, 1.0, 1.0]);
1530        assert!((sp[0] - 2.0).abs() < 1e-10);
1531        assert!((sp[1] - 3.0).abs() < 1e-10);
1532        assert!((sp[2] - 4.0).abs() < 1e-10);
1533    }
1534
1535    #[test]
1536    fn test_support_box_negative_axes() {
1537        let b = Box3::new([0.0; 3], [1.0; 3]);
1538        let sp = b.support([-1.0, -1.0, -1.0]);
1539        assert!((sp[0] + 1.0).abs() < 1e-10);
1540        assert!((sp[1] + 1.0).abs() < 1e-10);
1541        assert!((sp[2] + 1.0).abs() < 1e-10);
1542    }
1543
1544    #[test]
1545    fn test_support_capsule_chooses_correct_endpoint() {
1546        let cap = Capsule::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1547        let sp_up = cap.support([0.0, 1.0, 0.0]);
1548        // Should be near top endpoint + radius in Y
1549        assert!(sp_up[1] > 1.0, "y support above top endpoint");
1550        let sp_down = cap.support([0.0, -1.0, 0.0]);
1551        assert!(sp_down[1] < -1.0, "y support below bottom endpoint");
1552    }
1553
1554    // ── Minkowski sum ─────────────────────────────────────────────────────────
1555
1556    #[test]
1557    fn test_minkowski_sum_sphere_sphere_support() {
1558        let a = Sphere::new([0.0; 3], 1.0);
1559        let b = Sphere::new([0.0; 3], 2.0);
1560        let dir = [1.0, 0.0, 0.0];
1561        // Support of sum = support of a + support of b in same direction
1562        let sp = minkowski_sum_support(&a, &b, dir);
1563        let expected = a.support(dir)[0] + b.support(dir)[0];
1564        assert!((sp[0] - expected).abs() < 1e-10);
1565    }
1566
1567    #[test]
1568    fn test_minkowski_sum_sphere_box() {
1569        let s = Sphere::new([0.0; 3], 0.5);
1570        let b = Box3::new([0.0; 3], [1.0, 1.0, 1.0]);
1571        let dir = [1.0, 0.0, 0.0];
1572        let sp = minkowski_sum_support(&s, &b, dir);
1573        // Support ≥ box support alone (sphere only adds positive amount)
1574        assert!(sp[0] >= b.support(dir)[0]);
1575    }
1576
1577    // ── Shape casting / continuous collision ──────────────────────────────────
1578
1579    #[test]
1580    fn test_shape_cast_sphere_hits_sphere() {
1581        let moving = Sphere::new([0.0; 3], 0.5);
1582        let target = Sphere::new([5.0, 0.0, 0.0], 0.5);
1583        let velocity = [1.0, 0.0, 0.0];
1584        let t = shape_cast_sphere_vs_sphere(&moving, velocity, &target, 10.0);
1585        assert!(t.is_some(), "moving sphere should hit static sphere");
1586        let tv = t.unwrap();
1587        assert!((0.0..=10.0).contains(&tv));
1588    }
1589
1590    #[test]
1591    fn test_shape_cast_sphere_misses_sphere() {
1592        let moving = Sphere::new([0.0; 3], 0.5);
1593        let target = Sphere::new([0.0, 100.0, 0.0], 0.5);
1594        let velocity = [1.0, 0.0, 0.0]; // moving in X, target far in Y
1595        let t = shape_cast_sphere_vs_sphere(&moving, velocity, &target, 10.0);
1596        assert!(
1597            t.is_none(),
1598            "sphere moving in X should miss sphere far in Y"
1599        );
1600    }
1601
1602    #[test]
1603    fn test_shape_cast_already_overlapping() {
1604        let moving = Sphere::new([0.0; 3], 1.0);
1605        let target = Sphere::new([0.5, 0.0, 0.0], 1.0);
1606        let velocity = [1.0, 0.0, 0.0];
1607        let t = shape_cast_sphere_vs_sphere(&moving, velocity, &target, 10.0);
1608        // Already overlapping → t = 0 or very small
1609        if let Some(tv) = t {
1610            assert!(tv >= 0.0);
1611        }
1612    }
1613
1614    // ── GJK with capsule/box ──────────────────────────────────────────────────
1615
1616    #[test]
1617    fn test_gjk_box_sphere_intersecting() {
1618        let b = Box3::new([0.0; 3], [1.0; 3]);
1619        let s = Sphere::new([1.5, 0.0, 0.0], 1.0);
1620        let r = gjk(&b, &s);
1621        assert!(r.intersecting, "box and sphere should intersect");
1622    }
1623
1624    #[test]
1625    fn test_gjk_box_sphere_separated() {
1626        let b = Box3::new([0.0; 3], [1.0; 3]);
1627        let s = Sphere::new([10.0, 0.0, 0.0], 0.5);
1628        let r = gjk(&b, &s);
1629        assert!(!r.intersecting, "box and sphere should be separated");
1630        assert!(r.distance > 0.0);
1631    }
1632
1633    #[test]
1634    fn test_gjk_capsule_sphere_overlapping() {
1635        let cap = Capsule::new([0.0, -2.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1636        let s = Sphere::new([0.0, 0.0, 0.0], 0.5);
1637        let r = gjk(&cap, &s);
1638        assert!(r.intersecting, "capsule and sphere should overlap");
1639    }
1640
1641    #[test]
1642    fn test_gjk_capsule_sphere_separated() {
1643        let cap = Capsule::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.3);
1644        let s = Sphere::new([5.0, 0.0, 0.0], 0.3);
1645        let r = gjk(&cap, &s);
1646        assert!(!r.intersecting);
1647    }
1648
1649    // ── AabbRaw additional tests ───────────────────────────────────────────────
1650
1651    #[test]
1652    fn test_aabb_surface_area_unit_cube() {
1653        let a = AabbRaw::new([0.0; 3], [1.0; 3]);
1654        // Unit cube: SA = 6
1655        assert!((a.surface_area() - 6.0).abs() < 1e-12);
1656    }
1657
1658    #[test]
1659    fn test_aabb_center() {
1660        let a = AabbRaw::new([-2.0; 3], [4.0; 3]);
1661        let c = a.center();
1662        assert!((c[0] - 1.0).abs() < 1e-12);
1663    }
1664
1665    #[test]
1666    fn test_aabb_from_center_half() {
1667        let a = AabbRaw::from_center_half([1.0, 2.0, 3.0], [0.5; 3]);
1668        assert!((a.min[0] - 0.5).abs() < 1e-12);
1669        assert!((a.max[1] - 2.5).abs() < 1e-12);
1670    }
1671
1672    // ── OBB additional tests ───────────────────────────────────────────────────
1673
1674    #[test]
1675    fn test_obb_vertices_count() {
1676        let obb = Obb::axis_aligned([0.0; 3], [1.0; 3]);
1677        let verts = obb.vertices();
1678        assert_eq!(verts.len(), 8);
1679    }
1680
1681    #[test]
1682    fn test_obb_axis_aligned_vertices_correct() {
1683        let obb = Obb::axis_aligned([0.0; 3], [1.0; 3]);
1684        let verts = obb.vertices();
1685        // Every vertex should have coordinates in {-1, 1}³
1686        for v in &verts {
1687            assert!(v[0].abs() <= 1.0 + 1e-10);
1688            assert!(v[1].abs() <= 1.0 + 1e-10);
1689            assert!(v[2].abs() <= 1.0 + 1e-10);
1690        }
1691    }
1692
1693    #[test]
1694    fn test_obb_sat_touching() {
1695        // Two OBBs touching exactly at their faces
1696        let a = Obb::axis_aligned([0.0; 3], [1.0; 3]);
1697        let b = Obb::axis_aligned([2.0, 0.0, 0.0], [1.0; 3]);
1698        // Distance between centers = 2, sum of half-extents in X = 2 → touching
1699        let result = obb_obb_sat(&a, &b);
1700        // Either touching (some depth ~ 0) or just separated — both valid
1701        let _ = result;
1702    }
1703
1704    // ── BVH additional tests ───────────────────────────────────────────────────
1705
1706    #[test]
1707    fn test_bvh_all_overlapping_pairs() {
1708        let leaves = vec![
1709            BvhLeaf {
1710                id: 1,
1711                aabb: AabbRaw::new([0.0; 3], [2.0; 3]),
1712            },
1713            BvhLeaf {
1714                id: 2,
1715                aabb: AabbRaw::new([1.0; 3], [3.0; 3]),
1716            },
1717            BvhLeaf {
1718                id: 3,
1719                aabb: AabbRaw::new([10.0; 3], [11.0; 3]),
1720            },
1721        ];
1722        let bvh = Bvh::build(leaves);
1723        let pairs = bvh.overlapping_pairs();
1724        // Pair (1,2) overlaps; (1,3) and (2,3) don't
1725        assert!(
1726            pairs.contains(&(1, 2)) || pairs.contains(&(2, 1)),
1727            "pair (1,2) should be in overlapping pairs: {:?}",
1728            pairs
1729        );
1730    }
1731
1732    #[test]
1733    fn test_bvh_single_leaf() {
1734        let leaves = vec![BvhLeaf {
1735            id: 42,
1736            aabb: AabbRaw::new([0.0; 3], [1.0; 3]),
1737        }];
1738        let bvh = Bvh::build(leaves);
1739        let hits = bvh.query(&AabbRaw::new([0.0; 3], [0.5; 3]));
1740        assert_eq!(hits, vec![42]);
1741    }
1742
1743    // ── ContactManifold additional tests ──────────────────────────────────────
1744
1745    #[test]
1746    fn test_manifold_overflow_replaces_shallowest() {
1747        let mut m = ContactManifold::new();
1748        for i in 0..5 {
1749            m.add_contact(Contact {
1750                point: [i as f64, 0.0, 0.0],
1751                normal: [0.0, 1.0, 0.0],
1752                depth: (i as f64 + 1.0) * 0.1,
1753                id_a: 1,
1754                id_b: 2,
1755            });
1756        }
1757        // Should have at most 4 contacts
1758        assert!(m.contacts.len() <= 4);
1759    }
1760
1761    #[test]
1762    fn test_manifold_is_empty_after_new() {
1763        let m = ContactManifold::new();
1764        assert!(m.is_empty());
1765    }
1766
1767    #[test]
1768    fn test_manifold_max_depth_empty() {
1769        let m = ContactManifold::new();
1770        // max_depth on empty manifold should be NEG_INFINITY
1771        assert_eq!(m.max_depth(), f64::NEG_INFINITY);
1772    }
1773
1774    // ── point_segment_closest ─────────────────────────────────────────────────
1775
1776    #[test]
1777    fn test_point_segment_closest_midpoint() {
1778        let (cp, dist) = point_segment_closest([0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1779        assert!(cp[0].abs() < 1e-12, "closest x should be 0");
1780        assert!((dist - 1.0).abs() < 1e-10);
1781    }
1782
1783    #[test]
1784    fn test_point_segment_closest_beyond_end() {
1785        let (cp, dist) = point_segment_closest([5.0, 0.0, 0.0], [0.0; 3], [1.0, 0.0, 0.0]);
1786        assert!((cp[0] - 1.0).abs() < 1e-12, "closest x should clamp to 1");
1787        assert!((dist - 4.0).abs() < 1e-10);
1788    }
1789
1790    // ── ray_sphere additional ─────────────────────────────────────────────────
1791
1792    #[test]
1793    fn test_ray_sphere_from_inside() {
1794        let s = Sphere::new([0.0; 3], 2.0);
1795        // Ray from origin → should hit from inside (t > 0)
1796        let t = ray_sphere([0.0; 3], [1.0, 0.0, 0.0], &s);
1797        assert!(t.is_some());
1798        assert!(t.unwrap() > 0.0);
1799    }
1800
1801    #[test]
1802    fn test_ray_aabb_along_each_axis() {
1803        let aabb = AabbRaw::new([-1.0; 3], [1.0; 3]);
1804        for axis in 0..3 {
1805            // Start on the negative axis side, aligned with the center
1806            let mut origin = [0.0_f64; 3];
1807            let mut dir = [0.0_f64; 3];
1808            origin[axis] = -5.0;
1809            dir[axis] = 1.0;
1810            let hit = ray_aabb(origin, dir, &aabb);
1811            assert!(hit.is_some(), "ray along axis {axis} should hit unit AABB");
1812        }
1813    }
1814}