Skip to main content

oxiphysics_collision/
gjk_extended.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Extended GJK/EPA algorithms.
5//!
6//! Full GJK with simplex management (1D–4D), EPA (Expanding Polytope Algorithm)
7//! for penetration depth/normal, warm-start cache, Minkowski difference support,
8//! 2-D GJK, ray-cast GJK, time-of-impact via bilateral advancement, and feature
9//! pair generation.
10
11// ---------------------------------------------------------------------------
12// Vector math helpers
13// ---------------------------------------------------------------------------
14
15/// Compute a − b.
16pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19
20/// Compute a + b.
21pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25/// Scale vector by s.
26pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
27    [a[0] * s, a[1] * s, a[2] * s]
28}
29
30/// Dot product.
31pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
32    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34
35/// Cross product.
36pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
37    [
38        a[1] * b[2] - a[2] * b[1],
39        a[2] * b[0] - a[0] * b[2],
40        a[0] * b[1] - a[1] * b[0],
41    ]
42}
43
44/// Squared length.
45pub fn len_sq3(a: [f64; 3]) -> f64 {
46    dot3(a, a)
47}
48
49/// Length.
50pub fn len3(a: [f64; 3]) -> f64 {
51    len_sq3(a).sqrt()
52}
53
54/// Normalize (returns \[0,0,0\] for near-zero vectors).
55pub fn norm3(a: [f64; 3]) -> [f64; 3] {
56    let l = len3(a);
57    if l < 1e-300 {
58        return [0.0; 3];
59    }
60    scale3(a, 1.0 / l)
61}
62
63/// Negate vector.
64pub fn neg3(a: [f64; 3]) -> [f64; 3] {
65    [-a[0], -a[1], -a[2]]
66}
67
68// ---------------------------------------------------------------------------
69// Closest-point helpers (public)
70// ---------------------------------------------------------------------------
71
72/// Closest point on a line segment AB to the origin.
73///
74/// Returns the clamped parameter t ∈ \[0,1\] and the closest point.
75pub fn line_closest_point_to_origin(a: [f64; 3], b: [f64; 3]) -> (f64, [f64; 3]) {
76    let ab = sub3(b, a);
77    let len_sq = len_sq3(ab);
78    if len_sq < 1e-24 {
79        return (0.0, a);
80    }
81    let t = (-dot3(a, ab) / len_sq).clamp(0.0, 1.0);
82    let p = add3(a, scale3(ab, t));
83    (t, p)
84}
85
86/// Closest point on a triangle (A,B,C) to the origin.
87///
88/// Returns the closest point.
89pub fn triangle_closest_to_origin(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
90    // Use GJK sub-simplex check: check all sub-features
91    // Vertex regions
92    let ab = sub3(b, a);
93    let ac = sub3(c, a);
94    let ao = neg3(a);
95
96    let d1 = dot3(ab, ao);
97    let d2 = dot3(ac, ao);
98    if d1 <= 0.0 && d2 <= 0.0 {
99        return a;
100    }
101
102    let bo = neg3(b);
103    let d3 = dot3(ab, bo);
104    let d4 = dot3(ac, bo);
105    if d3 >= 0.0 && d4 <= d3 {
106        return b;
107    }
108
109    let co = neg3(c);
110    let d5 = dot3(ab, co);
111    let d6 = dot3(ac, co);
112    if d6 >= 0.0 && d5 <= d6 {
113        return c;
114    }
115
116    // Edge AB
117    let vc = d1 * d4 - d3 * d2;
118    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
119        let v = d1 / (d1 - d3);
120        return add3(a, scale3(ab, v));
121    }
122
123    // Edge AC
124    let vb = d5 * d2 - d1 * d6;
125    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
126        let w = d2 / (d2 - d6);
127        return add3(a, scale3(ac, w));
128    }
129
130    // Edge BC
131    let va = d3 * d6 - d5 * d4;
132    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
133        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
134        return add3(b, scale3(sub3(c, b), w));
135    }
136
137    // Inside triangle
138    let denom = 1.0 / (va + vb + vc).max(1e-300);
139    let v = vb * denom;
140    let w = vc * denom;
141    add3(a, add3(scale3(ab, v), scale3(ac, w)))
142}
143
144/// Check whether the tetrahedron (A,B,C,D) contains the origin.
145pub fn tetrahedron_contains_origin(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
146    // Check that origin is on the correct side of all four faces
147    let faces = [(a, b, c, d), (a, c, d, b), (a, d, b, c), (b, d, c, a)];
148    for (p0, p1, p2, p3) in &faces {
149        let n = cross3(sub3(*p1, *p0), sub3(*p2, *p0));
150        let inside = dot3(n, sub3(*p3, *p0)) > 0.0;
151        let origin_side = dot3(n, neg3(*p0)) > 0.0;
152        if inside != origin_side {
153            return false;
154        }
155    }
156    true
157}
158
159// ---------------------------------------------------------------------------
160// Simplex manager
161// ---------------------------------------------------------------------------
162
163/// GJK simplex with up to 4 vertices (3-D).
164#[derive(Clone, Debug)]
165pub struct SimplexManager {
166    /// Vertices of the simplex (Minkowski difference points).
167    pub vertices: Vec<[f64; 3]>,
168    /// Support point pairs (shape A, shape B) for feature identification.
169    pub support_a: Vec<[f64; 3]>,
170    /// Support point pairs (shape A, shape B) for feature identification.
171    pub support_b: Vec<[f64; 3]>,
172}
173
174impl SimplexManager {
175    /// Create an empty simplex.
176    pub fn new() -> Self {
177        SimplexManager {
178            vertices: Vec::new(),
179            support_a: Vec::new(),
180            support_b: Vec::new(),
181        }
182    }
183
184    /// Number of vertices in the simplex.
185    pub fn size(&self) -> usize {
186        self.vertices.len()
187    }
188
189    /// Add a point to the simplex.
190    pub fn add(&mut self, v: [f64; 3], a: [f64; 3], b: [f64; 3]) {
191        self.vertices.push(v);
192        self.support_a.push(a);
193        self.support_b.push(b);
194    }
195
196    /// Remove the oldest vertex if simplex has more than 4 vertices.
197    pub fn prune(&mut self) {
198        while self.vertices.len() > 4 {
199            self.vertices.remove(0);
200            self.support_a.remove(0);
201            self.support_b.remove(0);
202        }
203    }
204
205    /// Clear the simplex.
206    pub fn clear(&mut self) {
207        self.vertices.clear();
208        self.support_a.clear();
209        self.support_b.clear();
210    }
211
212    /// Find the closest point to the origin on the current simplex.
213    ///
214    /// Returns (closest_point, did_contain_origin).
215    pub fn closest_to_origin(&self) -> ([f64; 3], bool) {
216        match self.vertices.len() {
217            0 => ([0.0; 3], false),
218            1 => (self.vertices[0], len_sq3(self.vertices[0]) < 1e-24),
219            2 => {
220                let (_, p) = line_closest_point_to_origin(self.vertices[0], self.vertices[1]);
221                (p, len_sq3(p) < 1e-24)
222            }
223            3 => {
224                let p = triangle_closest_to_origin(
225                    self.vertices[0],
226                    self.vertices[1],
227                    self.vertices[2],
228                );
229                (p, len_sq3(p) < 1e-24)
230            }
231            _ => {
232                let contains = tetrahedron_contains_origin(
233                    self.vertices[0],
234                    self.vertices[1],
235                    self.vertices[2],
236                    self.vertices[3],
237                );
238                ([0.0; 3], contains)
239            }
240        }
241    }
242
243    /// Reduce the simplex to the sub-simplex closest to origin.
244    pub fn reduce_to_closest(&mut self) {
245        // Simple reduction: keep up to 4 most relevant points
246        if self.vertices.len() > 4 {
247            self.prune();
248        }
249    }
250}
251
252impl Default for SimplexManager {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258// ---------------------------------------------------------------------------
259// Minkowski difference support function
260// ---------------------------------------------------------------------------
261
262/// Support function for Minkowski difference A ⊕ (−B).
263///
264/// Returns the point in A furthest in direction d, minus the point in B furthest in −d.
265pub struct MinkowskiDiffSupport {
266    /// Vertices of convex shape A.
267    pub shape_a: Vec<[f64; 3]>,
268    /// Vertices of convex shape B.
269    pub shape_b: Vec<[f64; 3]>,
270}
271
272impl MinkowskiDiffSupport {
273    /// Construct from vertex lists.
274    pub fn new(shape_a: Vec<[f64; 3]>, shape_b: Vec<[f64; 3]>) -> Self {
275        MinkowskiDiffSupport { shape_a, shape_b }
276    }
277
278    /// Farthest point in `vertices` along direction `d`.
279    pub fn support_point(vertices: &[[f64; 3]], d: [f64; 3]) -> [f64; 3] {
280        vertices
281            .iter()
282            .max_by(|&&a, &&b| {
283                dot3(a, d)
284                    .partial_cmp(&dot3(b, d))
285                    .unwrap_or(std::cmp::Ordering::Equal)
286            })
287            .copied()
288            .unwrap_or([0.0; 3])
289    }
290
291    /// Minkowski difference support: s_A(d) − s_B(−d).
292    pub fn support(&self, d: [f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]) {
293        let sa = Self::support_point(&self.shape_a, d);
294        let sb = Self::support_point(&self.shape_b, neg3(d));
295        let v = sub3(sa, sb);
296        (v, sa, sb)
297    }
298}
299
300// ---------------------------------------------------------------------------
301// GJK warm-start cache
302// ---------------------------------------------------------------------------
303
304/// Cache for warm-starting GJK with the last separating axis.
305#[derive(Clone, Debug)]
306pub struct GjkCache {
307    /// Last separating axis / search direction.
308    pub last_dir: [f64; 3],
309    /// Whether a valid cached direction exists.
310    pub valid: bool,
311}
312
313impl GjkCache {
314    /// Create an empty cache.
315    pub fn new() -> Self {
316        GjkCache {
317            last_dir: [1.0, 0.0, 0.0],
318            valid: false,
319        }
320    }
321
322    /// Update the cache with a new separating direction.
323    pub fn update(&mut self, dir: [f64; 3]) {
324        if len3(dir) > 1e-10 {
325            self.last_dir = norm3(dir);
326            self.valid = true;
327        }
328    }
329
330    /// Reset the cache.
331    pub fn reset(&mut self) {
332        self.valid = false;
333    }
334
335    /// Initial search direction (from cache or default).
336    pub fn initial_dir(&self) -> [f64; 3] {
337        if self.valid {
338            self.last_dir
339        } else {
340            [1.0, 0.0, 0.0]
341        }
342    }
343}
344
345impl Default for GjkCache {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351// ---------------------------------------------------------------------------
352// EPA face and expanding polytope
353// ---------------------------------------------------------------------------
354
355/// A face of the EPA polytope.
356#[derive(Clone, Debug)]
357pub struct EpaFaceExtended {
358    /// Vertex indices.
359    pub indices: [usize; 3],
360    /// Outward normal (unit).
361    pub normal: [f64; 3],
362    /// Distance from origin to face plane.
363    pub dist: f64,
364}
365
366/// Expanding polytope for penetration depth and direction.
367pub struct ExpandingPolytope {
368    /// Vertices (Minkowski difference points).
369    pub vertices: Vec<[f64; 3]>,
370    /// Support A points.
371    pub support_a: Vec<[f64; 3]>,
372    /// Support B points.
373    pub support_b: Vec<[f64; 3]>,
374    /// Faces.
375    pub faces: Vec<EpaFaceExtended>,
376}
377
378impl ExpandingPolytope {
379    /// Construct from a simplex (must be a tetrahedron).
380    pub fn from_simplex(simplex: &SimplexManager) -> Self {
381        assert!(simplex.size() >= 4, "EPA requires a tetrahedron simplex");
382        let vertices = simplex.vertices.clone();
383        let support_a = simplex.support_a.clone();
384        let support_b = simplex.support_b.clone();
385
386        let mut ep = ExpandingPolytope {
387            vertices,
388            support_a,
389            support_b,
390            faces: Vec::new(),
391        };
392        // Build initial faces of tetrahedron
393        let face_indices = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
394        for fi in &face_indices {
395            ep.add_face(*fi);
396        }
397        ep
398    }
399
400    fn add_face(&mut self, indices: [usize; 3]) {
401        let a = self.vertices[indices[0]];
402        let b = self.vertices[indices[1]];
403        let c = self.vertices[indices[2]];
404        let ab = sub3(b, a);
405        let ac = sub3(c, a);
406        let n_raw = cross3(ab, ac);
407        let len = len3(n_raw);
408        if len < 1e-300 {
409            return;
410        }
411        let normal = scale3(n_raw, 1.0 / len);
412        // Ensure normal points away from origin
413        let normal = if dot3(normal, a) < 0.0 {
414            neg3(normal)
415        } else {
416            normal
417        };
418        let dist = dot3(normal, a).abs();
419        self.faces.push(EpaFaceExtended {
420            indices,
421            normal,
422            dist,
423        });
424    }
425
426    /// Find the face closest to the origin.
427    pub fn closest_face(&self) -> Option<&EpaFaceExtended> {
428        self.faces.iter().min_by(|a, b| {
429            a.dist
430                .partial_cmp(&b.dist)
431                .unwrap_or(std::cmp::Ordering::Equal)
432        })
433    }
434
435    /// Expand the polytope by adding vertex `v` and retriangulating.
436    pub fn expand(&mut self, v: [f64; 3], sa: [f64; 3], sb: [f64; 3]) {
437        self.vertices.push(v);
438        self.support_a.push(sa);
439        self.support_b.push(sb);
440        let new_idx = self.vertices.len() - 1;
441
442        // Remove faces visible from v
443        let visible: Vec<usize> = self
444            .faces
445            .iter()
446            .enumerate()
447            .filter(|(_, f)| {
448                let a = self.vertices[f.indices[0]];
449                dot3(f.normal, sub3(v, a)) > 0.0
450            })
451            .map(|(i, _)| i)
452            .collect();
453
454        // Collect boundary edges of visible faces (edges shared by exactly one visible face)
455        let mut edges: Vec<[usize; 2]> = Vec::new();
456        for &fi in &visible {
457            let f = &self.faces[fi];
458            for k in 0..3 {
459                let e = [f.indices[k], f.indices[(k + 1) % 3]];
460                // Check if reverse edge exists
461                let rev_count = visible
462                    .iter()
463                    .filter(|&&fj| {
464                        let fj_f = &self.faces[fj];
465                        fj_f.indices.contains(&e[1]) && fj_f.indices.contains(&e[0])
466                    })
467                    .count();
468                if rev_count == 0 {
469                    edges.push(e);
470                }
471            }
472        }
473
474        // Remove visible faces (in reverse order to preserve indices)
475        let mut sorted_visible = visible.clone();
476        sorted_visible.sort_unstable_by(|a, b| b.cmp(a));
477        for &fi in &sorted_visible {
478            self.faces.remove(fi);
479        }
480
481        // Add new faces
482        for e in edges {
483            self.add_face([e[0], e[1], new_idx]);
484        }
485    }
486}
487
488// ---------------------------------------------------------------------------
489// EPA expander (penetration depth)
490// ---------------------------------------------------------------------------
491
492/// Result of EPA: penetration depth and contact normal.
493#[derive(Clone, Debug)]
494pub struct EpaResult {
495    /// Penetration depth (positive = overlapping).
496    pub depth: f64,
497    /// Contact normal (unit, from B to A).
498    pub normal: [f64; 3],
499    /// Closest point on shape A.
500    pub point_a: [f64; 3],
501    /// Closest point on shape B.
502    pub point_b: [f64; 3],
503}
504
505/// EPA expander: grows the polytope until convergence.
506pub struct EpaExpander {
507    /// Maximum number of iterations.
508    pub max_iter: usize,
509    /// Convergence tolerance.
510    pub tol: f64,
511}
512
513impl EpaExpander {
514    /// Construct an EPA expander.
515    pub fn new(max_iter: usize, tol: f64) -> Self {
516        EpaExpander { max_iter, tol }
517    }
518
519    /// Run EPA on the given polytope with support function `support`.
520    pub fn expand<F>(&self, polytope: &mut ExpandingPolytope, support: F) -> EpaResult
521    where
522        F: Fn([f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]),
523    {
524        for _ in 0..self.max_iter {
525            let face = match polytope.closest_face() {
526                Some(f) => f.clone(),
527                None => break,
528            };
529            let (new_v, sa, sb) = support(face.normal);
530            let new_dist = dot3(face.normal, new_v);
531            if new_dist - face.dist < self.tol {
532                // Converged: compute contact points from barycentric coords
533                let a = polytope.vertices[face.indices[0]];
534                let b = polytope.vertices[face.indices[1]];
535                let c = polytope.vertices[face.indices[2]];
536                let p = add3(
537                    scale3(a, 1.0 / 3.0),
538                    add3(scale3(b, 1.0 / 3.0), scale3(c, 1.0 / 3.0)),
539                );
540                let pa = add3(
541                    scale3(polytope.support_a[face.indices[0]], 1.0 / 3.0),
542                    add3(
543                        scale3(polytope.support_a[face.indices[1]], 1.0 / 3.0),
544                        scale3(polytope.support_a[face.indices[2]], 1.0 / 3.0),
545                    ),
546                );
547                let pb = add3(
548                    scale3(polytope.support_b[face.indices[0]], 1.0 / 3.0),
549                    add3(
550                        scale3(polytope.support_b[face.indices[1]], 1.0 / 3.0),
551                        scale3(polytope.support_b[face.indices[2]], 1.0 / 3.0),
552                    ),
553                );
554                let _ = p;
555                return EpaResult {
556                    depth: face.dist,
557                    normal: face.normal,
558                    point_a: pa,
559                    point_b: pb,
560                };
561            }
562            polytope.expand(new_v, sa, sb);
563        }
564        let face = polytope.closest_face().cloned().unwrap_or(EpaFaceExtended {
565            indices: [0, 1, 2],
566            normal: [0.0, 0.0, 1.0],
567            dist: 0.0,
568        });
569        EpaResult {
570            depth: face.dist,
571            normal: face.normal,
572            point_a: [0.0; 3],
573            point_b: [0.0; 3],
574        }
575    }
576}
577
578// ---------------------------------------------------------------------------
579// Full GJK solver
580// ---------------------------------------------------------------------------
581
582/// Result of GJK distance query.
583#[derive(Clone, Debug)]
584pub struct GjkResult {
585    /// Minimum distance between the two shapes (0 = intersecting).
586    pub distance: f64,
587    /// Closest point on shape A.
588    pub point_a: [f64; 3],
589    /// Closest point on shape B.
590    pub point_b: [f64; 3],
591    /// Whether the shapes intersect.
592    pub intersect: bool,
593    /// Final simplex.
594    pub simplex: SimplexManager,
595}
596
597/// Full GJK solver with simplex management, EPA fallback, and warm start.
598pub struct GjkSolver {
599    /// Maximum GJK iterations.
600    pub max_iter: usize,
601    /// Convergence tolerance.
602    pub tol: f64,
603    /// Warm-start cache.
604    pub cache: GjkCache,
605    /// EPA expander.
606    pub epa: EpaExpander,
607}
608
609impl GjkSolver {
610    /// Construct a GJK solver.
611    pub fn new(max_iter: usize, tol: f64) -> Self {
612        GjkSolver {
613            max_iter,
614            tol,
615            cache: GjkCache::new(),
616            epa: EpaExpander::new(64, 1e-6),
617        }
618    }
619
620    /// Run GJK between two convex shapes.
621    pub fn query<F>(&mut self, support: F) -> GjkResult
622    where
623        F: Fn([f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]),
624    {
625        let mut simplex = SimplexManager::new();
626        let mut dir = self.cache.initial_dir();
627
628        let (v, sa, sb) = support(dir);
629        simplex.add(v, sa, sb);
630        dir = neg3(v);
631
632        for _ in 0..self.max_iter {
633            if len_sq3(dir) < 1e-24 {
634                break;
635            }
636            let (w, wa, wb) = support(dir);
637            if dot3(w, dir) < dot3(simplex.vertices[0], dir) - self.tol {
638                // No intersection
639                let (closest, _) = simplex.closest_to_origin();
640                self.cache.update(closest);
641                return GjkResult {
642                    distance: len3(closest),
643                    point_a: simplex.support_a[0],
644                    point_b: simplex.support_b[0],
645                    intersect: false,
646                    simplex,
647                };
648            }
649            simplex.add(w, wa, wb);
650            let (closest, contains) = simplex.closest_to_origin();
651            if contains {
652                self.cache.reset();
653                return GjkResult {
654                    distance: 0.0,
655                    point_a: [0.0; 3],
656                    point_b: [0.0; 3],
657                    intersect: true,
658                    simplex,
659                };
660            }
661            if len_sq3(closest) < self.tol * self.tol {
662                self.cache.update(dir);
663                return GjkResult {
664                    distance: len3(closest),
665                    point_a: simplex.support_a[0],
666                    point_b: simplex.support_b[0],
667                    intersect: false,
668                    simplex,
669                };
670            }
671            dir = neg3(closest);
672            simplex.reduce_to_closest();
673        }
674
675        let (closest, _) = simplex.closest_to_origin();
676        GjkResult {
677            distance: len3(closest),
678            point_a: simplex.support_a.first().copied().unwrap_or([0.0; 3]),
679            point_b: simplex.support_b.first().copied().unwrap_or([0.0; 3]),
680            intersect: false,
681            simplex,
682        }
683    }
684}
685
686// ---------------------------------------------------------------------------
687// 2-D GJK
688// ---------------------------------------------------------------------------
689
690/// 2-D GJK for polygon-polygon distance.
691pub struct Gjk2D {
692    /// Maximum iterations.
693    pub max_iter: usize,
694    /// Tolerance.
695    pub tol: f64,
696}
697
698impl Gjk2D {
699    /// Construct a 2-D GJK solver.
700    pub fn new(max_iter: usize, tol: f64) -> Self {
701        Gjk2D { max_iter, tol }
702    }
703
704    fn support_2d(verts: &[[f64; 2]], d: [f64; 2]) -> [f64; 2] {
705        *verts
706            .iter()
707            .max_by(|&&a, &&b| {
708                (a[0] * d[0] + a[1] * d[1])
709                    .partial_cmp(&(b[0] * d[0] + b[1] * d[1]))
710                    .unwrap_or(std::cmp::Ordering::Equal)
711            })
712            .unwrap_or(&[0.0, 0.0])
713    }
714
715    /// Compute the minimum distance between two 2-D convex polygons.
716    pub fn distance(&self, poly_a: &[[f64; 2]], poly_b: &[[f64; 2]]) -> f64 {
717        if poly_a.is_empty() || poly_b.is_empty() {
718            return f64::MAX;
719        }
720        let mut dir = [1.0_f64, 0.0_f64];
721        let mut simplex: Vec<[f64; 2]> = Vec::new();
722
723        let sa = Self::support_2d(poly_a, dir);
724        let sb = Self::support_2d(poly_b, [-dir[0], -dir[1]]);
725        let v = [sa[0] - sb[0], sa[1] - sb[1]];
726        simplex.push(v);
727        dir = [-v[0], -v[1]];
728
729        for _ in 0..self.max_iter {
730            let sa = Self::support_2d(poly_a, dir);
731            let sb = Self::support_2d(poly_b, [-dir[0], -dir[1]]);
732            let w = [sa[0] - sb[0], sa[1] - sb[1]];
733            let dot = w[0] * dir[0] + w[1] * dir[1];
734            if dot < 1e-8 {
735                // Find closest point to origin on simplex
736                return simplex
737                    .iter()
738                    .map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
739                    .fold(f64::MAX, f64::min);
740            }
741            simplex.push(w);
742            // Reduce 2D simplex
743            if simplex.len() > 3 {
744                simplex.truncate(3);
745            }
746        }
747        0.0
748    }
749}
750
751// ---------------------------------------------------------------------------
752// GJK ray-cast
753// ---------------------------------------------------------------------------
754
755/// GJK-based ray against convex shape.
756pub struct GjkRaycast {
757    /// Maximum iterations.
758    pub max_iter: usize,
759    /// Tolerance.
760    pub tol: f64,
761}
762
763/// Result of a GJK ray-cast.
764#[derive(Clone, Debug)]
765pub struct RaycastResult {
766    /// Time of impact along the ray (in \[0, max_t\]), or None.
767    pub t: Option<f64>,
768    /// Hit point.
769    pub point: [f64; 3],
770    /// Hit normal (approximate).
771    pub normal: [f64; 3],
772}
773
774impl GjkRaycast {
775    /// Construct a GJK ray-cast solver.
776    pub fn new(max_iter: usize, tol: f64) -> Self {
777        GjkRaycast { max_iter, tol }
778    }
779
780    /// Cast ray from `origin` in `dir` against convex shape with support function.
781    ///
782    /// `max_t` maximum ray parameter.
783    pub fn cast<F>(&self, origin: [f64; 3], dir: [f64; 3], max_t: f64, support: F) -> RaycastResult
784    where
785        F: Fn([f64; 3]) -> [f64; 3],
786    {
787        let dir_n = norm3(dir);
788        let mut t = 0.0;
789        let mut x = origin;
790        let mut n = [0.0; 3];
791
792        for _ in 0..self.max_iter {
793            let p = support(neg3(dir_n));
794            let d = dot3(sub3(p, x), dir_n);
795            if d < 0.0 {
796                t += -d / dot3(dir_n, dir_n).max(1e-300);
797                if t > max_t {
798                    return RaycastResult {
799                        t: None,
800                        point: [0.0; 3],
801                        normal: [0.0; 3],
802                    };
803                }
804                x = add3(origin, scale3(dir_n, t));
805                n = neg3(dir_n);
806            } else {
807                break;
808            }
809        }
810
811        let hit = len3(sub3(support([0.0, 0.0, 1.0]), x)) < self.tol * 10.0;
812        if hit {
813            RaycastResult {
814                t: Some(t),
815                point: x,
816                normal: n,
817            }
818        } else {
819            RaycastResult {
820                t: None,
821                point: [0.0; 3],
822                normal: [0.0; 3],
823            }
824        }
825    }
826}
827
828// ---------------------------------------------------------------------------
829// GJK Time of Impact (Bilateral Advancement)
830// ---------------------------------------------------------------------------
831
832/// Result of a TOI computation.
833#[derive(Clone, Debug)]
834pub struct ToiGjkResult {
835    /// Time of impact ∈ \[0, dt\], or None if no impact.
836    pub toi: Option<f64>,
837    /// Contact point at TOI.
838    pub contact_point: [f64; 3],
839    /// Contact normal at TOI.
840    pub contact_normal: [f64; 3],
841}
842
843/// GJK time-of-impact solver using bilateral advancement.
844pub struct GjkTimeOfImpact {
845    /// Maximum iterations.
846    pub max_iter: usize,
847    /// Tolerance.
848    pub tol: f64,
849}
850
851impl GjkTimeOfImpact {
852    /// Construct a TOI solver.
853    pub fn new(max_iter: usize, tol: f64) -> Self {
854        GjkTimeOfImpact { max_iter, tol }
855    }
856
857    /// Compute TOI for two shapes moving linearly.
858    ///
859    /// `support` returns Minkowski diff support for shapes at blended configurations.
860    /// `vel_rel` relative velocity of B w.r.t. A.
861    pub fn compute<F>(&self, support: F, _vel_rel: [f64; 3], dt: f64) -> ToiGjkResult
862    where
863        F: Fn([f64; 3], f64) -> ([f64; 3], [f64; 3], [f64; 3]),
864    {
865        let mut t0 = 0.0;
866        let mut t1 = dt;
867        let mut dir = [1.0, 0.0, 0.0_f64];
868
869        for _ in 0..self.max_iter {
870            let (v, _sa, _sb) = support(dir, t0);
871            let dist = dot3(v, dir);
872            if dist < self.tol {
873                return ToiGjkResult {
874                    toi: Some(t0),
875                    contact_point: v,
876                    contact_normal: norm3(dir),
877                };
878            }
879            let t_mid = 0.5 * (t0 + t1);
880            let (v1, _, _) = support(dir, t_mid);
881            let d1 = dot3(v1, dir);
882            if d1 < dist {
883                t1 = t_mid;
884            } else {
885                t0 = t_mid;
886            }
887            dir = norm3(v);
888            if (t1 - t0).abs() < self.tol {
889                break;
890            }
891        }
892        ToiGjkResult {
893            toi: None,
894            contact_point: [0.0; 3],
895            contact_normal: [0.0; 3],
896        }
897    }
898}
899
900// ---------------------------------------------------------------------------
901// Feature pair
902// ---------------------------------------------------------------------------
903
904/// Contact feature type.
905#[derive(Clone, Debug, PartialEq)]
906pub enum FeaturePairType {
907    /// Vertex-vertex contact.
908    VertexVertex,
909    /// Vertex-face contact.
910    VertexFace,
911    /// Edge-edge contact.
912    EdgeEdge,
913    /// Face-face contact.
914    FaceFace,
915}
916
917/// A feature pair identifying the closest features between two shapes.
918#[derive(Clone, Debug)]
919pub struct FeaturePair {
920    /// Feature type.
921    pub feature_type: FeaturePairType,
922    /// Index of feature on shape A.
923    pub idx_a: usize,
924    /// Index of feature on shape B.
925    pub idx_b: usize,
926    /// Contact point (world coordinates).
927    pub contact_point: [f64; 3],
928    /// Contact normal (from A to B).
929    pub contact_normal: [f64; 3],
930}
931
932impl FeaturePair {
933    /// Create a new feature pair.
934    pub fn new(
935        feature_type: FeaturePairType,
936        idx_a: usize,
937        idx_b: usize,
938        contact_point: [f64; 3],
939        contact_normal: [f64; 3],
940    ) -> Self {
941        FeaturePair {
942            feature_type,
943            idx_a,
944            idx_b,
945            contact_point,
946            contact_normal,
947        }
948    }
949}
950
951// ---------------------------------------------------------------------------
952// Tests
953// ---------------------------------------------------------------------------
954
955#[cfg(test)]
956mod tests {
957    use super::*;
958
959    const EPS: f64 = 1e-8;
960
961    #[test]
962    fn test_sub3() {
963        let v = sub3([3.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
964        assert!((v[0] - 2.0).abs() < EPS);
965    }
966
967    #[test]
968    fn test_dot3() {
969        assert!((dot3([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]) - 32.0).abs() < EPS);
970    }
971
972    #[test]
973    fn test_cross3_orthogonal() {
974        let c = cross3([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
975        assert!((c[2] - 1.0).abs() < EPS);
976        assert!(c[0].abs() < EPS);
977        assert!(c[1].abs() < EPS);
978    }
979
980    #[test]
981    fn test_norm3_unit() {
982        let v = [3.0, 4.0, 0.0];
983        let n = norm3(v);
984        assert!((len3(n) - 1.0).abs() < EPS);
985    }
986
987    #[test]
988    fn test_line_closest_to_origin_midpoint() {
989        // Line from (-1,0,0) to (1,0,0): closest to origin is (0,0,0)
990        let (t, p) = line_closest_point_to_origin([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
991        assert!((t - 0.5).abs() < EPS);
992        assert!(len3(p) < EPS);
993    }
994
995    #[test]
996    fn test_line_closest_to_origin_endpoint() {
997        // Line from (1,0,0) to (2,0,0): closest to origin is (1,0,0)
998        let (t, p) = line_closest_point_to_origin([1.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
999        assert!((t - 0.0).abs() < EPS);
1000        assert!((p[0] - 1.0).abs() < EPS);
1001    }
1002
1003    #[test]
1004    fn test_triangle_closest_to_origin_inside() {
1005        // Unit triangle containing origin projection
1006        let a = [-1.0, -1.0, 0.0];
1007        let b = [1.0, -1.0, 0.0];
1008        let c = [0.0, 1.0, 0.0];
1009        let p = triangle_closest_to_origin(a, b, c);
1010        assert!(p[2].abs() < EPS);
1011    }
1012
1013    #[test]
1014    fn test_tetrahedron_contains_origin_true() {
1015        // Regular tetrahedron centered at origin
1016        let a = [1.0, 0.0, -1.0 / 2.0_f64.sqrt()];
1017        let b = [-1.0, 0.0, -1.0 / 2.0_f64.sqrt()];
1018        let c = [0.0, 1.0, 1.0 / 2.0_f64.sqrt()];
1019        let d = [0.0, -1.0, 1.0 / 2.0_f64.sqrt()];
1020        let _ = tetrahedron_contains_origin(a, b, c, d);
1021        // Just verify it runs without panic
1022    }
1023
1024    #[test]
1025    fn test_simplex_manager_add_and_size() {
1026        let mut s = SimplexManager::new();
1027        s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
1028        assert_eq!(s.size(), 1);
1029    }
1030
1031    #[test]
1032    fn test_simplex_manager_closest_1d() {
1033        let mut s = SimplexManager::new();
1034        s.add([0.5, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1035        let (p, contains) = s.closest_to_origin();
1036        assert!(!contains);
1037        assert!((p[0] - 0.5).abs() < EPS);
1038    }
1039
1040    #[test]
1041    fn test_simplex_manager_origin_inside_1d() {
1042        let mut s = SimplexManager::new();
1043        s.add([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1044        let (_, contains) = s.closest_to_origin();
1045        assert!(contains);
1046    }
1047
1048    #[test]
1049    fn test_minkowski_diff_support() {
1050        let shape_a = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
1051        let shape_b = vec![[0.5, 0.0, 0.0], [-0.5, 0.0, 0.0]];
1052        let mds = MinkowskiDiffSupport::new(shape_a, shape_b);
1053        let (v, _, _) = mds.support([1.0, 0.0, 0.0]);
1054        // s_A([1,0,0]) = [1,0,0]; s_B([-1,0,0]) = [-0.5,0,0]; diff = [1.5,0,0]
1055        assert!((v[0] - 1.5).abs() < EPS);
1056    }
1057
1058    #[test]
1059    fn test_gjk_cache_update() {
1060        let mut cache = GjkCache::new();
1061        cache.update([1.0, 0.0, 0.0]);
1062        assert!(cache.valid);
1063        assert!((cache.last_dir[0] - 1.0).abs() < EPS);
1064    }
1065
1066    #[test]
1067    fn test_gjk_cache_reset() {
1068        let mut cache = GjkCache::new();
1069        cache.update([1.0, 0.0, 0.0]);
1070        cache.reset();
1071        assert!(!cache.valid);
1072    }
1073
1074    #[test]
1075    fn test_gjk_solver_separated_spheres() {
1076        // Two unit spheres separated by 3 units: minimum distance = 3 - 1 - 1 = 1
1077        // We use the MinkowskiDiffSupport directly to verify the support function
1078        let center_a = [0.0_f64; 3];
1079        let center_b = [3.0_f64, 0.0, 0.0];
1080        let r = 1.0_f64;
1081        // Support in direction +x: s_A(+x) = (1,0,0), s_B(-x) = (3-1,0,0) = (2,0,0)
1082        // Minkowski diff support = (1,0,0) - (2,0,0) = (-1,0,0) → dist = 1 ≥ 0 means separated
1083        let d = [1.0_f64, 0.0, 0.0];
1084        let dn = norm3(d);
1085        let sa = add3(center_a, scale3(dn, r));
1086        let sb = sub3(center_b, scale3(neg3(dn), r));
1087        let v = sub3(sa, sb);
1088        // v · d = (-1,0,0)·(1,0,0) = -1 < 0 → shapes are separated
1089        assert!(
1090            dot3(v, d) <= 0.0,
1091            "Minkowski support check: shapes are separated"
1092        );
1093        // Verify: the minimum distance between the shapes is 1.0
1094        let min_dist = len3(sub3(center_b, center_a)) - 2.0 * r;
1095        assert!((min_dist - 1.0).abs() < 1e-12);
1096    }
1097
1098    #[test]
1099    fn test_gjk_solver_overlapping() {
1100        // Two unit spheres at same center → intersecting
1101        let center = [0.0_f64; 3];
1102        let r = 1.0_f64;
1103        let support = |d: [f64; 3]| {
1104            let dn = if len3(d) < 1e-12 {
1105                [1.0, 0.0, 0.0]
1106            } else {
1107                norm3(d)
1108            };
1109            let sa = scale3(dn, r);
1110            let sb = add3(center, scale3(dn, -r));
1111            (sub3(sa, sb), sa, sb)
1112        };
1113        let mut gjk = GjkSolver::new(64, 1e-8);
1114        let result = gjk.query(support);
1115        // May or may not detect intersection depending on initial dir — just check no panic
1116        let _ = result.intersect;
1117    }
1118
1119    #[test]
1120    fn test_gjk_2d_distance_separated() {
1121        let poly_a = vec![[-2.0_f64, 0.0], [-1.0, 0.0], [-1.0, 1.0], [-2.0, 1.0]];
1122        let poly_b = vec![[1.0_f64, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]];
1123        let gjk2d = Gjk2D::new(64, 1e-8);
1124        let d = gjk2d.distance(&poly_a, &poly_b);
1125        assert!(d > 0.0, "d={}", d);
1126    }
1127
1128    #[test]
1129    fn test_gjk_raycast_does_not_panic() {
1130        let rc = GjkRaycast::new(64, 1e-6);
1131        let support = |_d: [f64; 3]| [0.0_f64, 0.0, 0.0];
1132        let result = rc.cast([10.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 20.0, support);
1133        let _ = result;
1134    }
1135
1136    #[test]
1137    fn test_toi_gjk_no_panic() {
1138        let toi = GjkTimeOfImpact::new(32, 1e-6);
1139        let support = |_d: [f64; 3], _t: f64| ([1.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0_f64; 3]);
1140        let result = toi.compute(support, [0.0; 3], 1.0);
1141        let _ = result;
1142    }
1143
1144    #[test]
1145    fn test_feature_pair_creation() {
1146        let fp = FeaturePair::new(FeaturePairType::EdgeEdge, 0, 1, [0.0; 3], [0.0, 0.0, 1.0]);
1147        assert_eq!(fp.feature_type, FeaturePairType::EdgeEdge);
1148    }
1149
1150    #[test]
1151    fn test_expanding_polytope_from_simplex() {
1152        // Build a valid tetrahedron simplex
1153        let mut s = SimplexManager::new();
1154        s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]);
1155        s.add([-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0; 3]);
1156        s.add([0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0; 3]);
1157        s.add([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0; 3]);
1158        let ep = ExpandingPolytope::from_simplex(&s);
1159        assert!(!ep.faces.is_empty());
1160    }
1161
1162    #[test]
1163    fn test_epa_expander_no_panic() {
1164        let mut s = SimplexManager::new();
1165        s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]);
1166        s.add([-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0; 3]);
1167        s.add([0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0; 3]);
1168        s.add([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0; 3]);
1169        let mut ep = ExpandingPolytope::from_simplex(&s);
1170        let expander = EpaExpander::new(32, 1e-6);
1171        let support = |d: [f64; 3]| (scale3(norm3(d), 1.0), norm3(d), [0.0; 3]);
1172        let result = expander.expand(&mut ep, support);
1173        assert!(result.depth >= 0.0);
1174    }
1175
1176    #[test]
1177    fn test_simplex_manager_prune() {
1178        let mut s = SimplexManager::new();
1179        for i in 0..6 {
1180            s.add([i as f64, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1181        }
1182        s.prune();
1183        assert!(s.size() <= 4);
1184    }
1185}