Skip to main content

gizmo_physics_core/
gjk.rs

1use crate::collision::ContactPoint;
2use crate::components::{BoxShape, CapsuleShape, ColliderShape, SphereShape};
3use gizmo_math::Vec3;
4
5const EPA_TOLERANCE: f32 = 0.001;
6const EPA_MAX_ITERATIONS: usize = 32;
7
8pub struct Gjk;
9
10impl Gjk {
11    /// Test if two shapes are colliding using GJK
12    pub fn test_collision(
13        shape_a: &ColliderShape,
14        pos_a: Vec3,
15        rot_a: gizmo_math::Quat,
16        shape_b: &ColliderShape,
17        pos_b: Vec3,
18        rot_b: gizmo_math::Quat,
19    ) -> bool {
20        let support = |dir: Vec3| {
21            let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
22            let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
23            sa - sb
24        };
25
26        Self::gjk_with_simplex(support).is_some()
27    }
28
29    /// Get contact information using GJK + EPA
30    pub fn get_contact(
31        shape_a: &ColliderShape,
32        pos_a: Vec3,
33        rot_a: gizmo_math::Quat,
34        shape_b: &ColliderShape,
35        pos_b: Vec3,
36        rot_b: gizmo_math::Quat,
37    ) -> Option<ContactPoint> {
38        let support = |dir: Vec3| {
39            let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
40            let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
41            sa - sb
42        };
43
44        if let Some(simplex) = Self::gjk_with_simplex(support) {
45            if let Some(contact) = Self::epa(simplex, shape_a, pos_a, rot_a, shape_b, pos_b, rot_b)
46            {
47                Some(contact)
48            } else {
49                // EPA failed (likely degenerate simplex), but we KNOW they intersect.
50                // Return a basic contact point for triggers and solver fallback.
51                Some(ContactPoint {
52                    point: (pos_a + pos_b) * 0.5,
53                    normal: (pos_b - pos_a).try_normalize().unwrap_or(Vec3::Y),
54                    penetration: 0.01,
55                    ..Default::default()
56                })
57            }
58        } else {
59            None
60        }
61    }
62
63    /// GJK that returns the final simplex for EPA
64    fn gjk_with_simplex<F>(support: F) -> Option<Vec<Vec3>>
65    where
66        F: Fn(Vec3) -> Vec3,
67    {
68        let mut simplex = Vec::with_capacity(4);
69        let mut direction = Vec3::new(1.0, 0.0, 0.0);
70
71        // First point
72        simplex.push(support(direction));
73        direction = -simplex[0];
74
75        // FIX 3: More robust degenerate direction fallback — derive from existing simplex
76        // rather than always falling back to Vec3::X which can be parallel to the simplex
77        const MAX_ITERATIONS: usize = 32;
78        for _ in 0..MAX_ITERATIONS {
79            direction = direction.try_normalize().unwrap_or_else(|| {
80                // Derive a perpendicular direction from the current simplex
81                if simplex.len() >= 2 {
82                    let edge = simplex[simplex.len() - 1] - simplex[0];
83                    let perp = if edge.x.abs() <= edge.y.abs() && edge.x.abs() <= edge.z.abs() {
84                        Vec3::new(1.0, 0.0, 0.0)
85                    } else if edge.y.abs() <= edge.z.abs() {
86                        Vec3::new(0.0, 1.0, 0.0)
87                    } else {
88                        Vec3::new(0.0, 0.0, 1.0)
89                    };
90                    edge.cross(perp).try_normalize().unwrap_or(Vec3::X)
91                } else {
92                    Vec3::X
93                }
94            });
95
96            let a = support(direction);
97
98            if a.dot(direction) < 0.0 {
99                return None; // No collision
100            }
101
102            simplex.push(a);
103
104            if Self::handle_simplex(&mut simplex, &mut direction) {
105                return Some(simplex); // Collision detected
106            }
107        }
108
109        None
110    }
111
112    /// Compute distance and closest points using GJK (for non-intersecting shapes)
113    /// Returns (distance, normal_from_b_to_a)
114    pub fn distance<F>(support: F) -> Option<(f32, Vec3)>
115    where
116        F: Fn(Vec3) -> Vec3,
117    {
118        let mut simplex = Vec::with_capacity(4);
119        let mut direction = Vec3::X;
120
121        let p = support(direction);
122        simplex.push(p);
123
124        let mut closest_point = p;
125        let mut min_dist_sq = p.length_squared();
126
127        for _ in 0..32 {
128            if min_dist_sq < 1e-8 {
129                return Some((0.0, Vec3::X)); // Intersecting, use fallback normal
130            }
131
132            direction = -closest_point;
133            let a = support(direction.normalize());
134
135            // Convergence check: how much closer can we get in this direction?
136            let current_dist = min_dist_sq.sqrt();
137            let lower_bound = -a.dot(direction) / current_dist;
138            if current_dist - lower_bound < 0.0001 {
139                break;
140            }
141
142            simplex.push(a);
143            closest_point = Self::closest_point_on_simplex(&mut simplex);
144            min_dist_sq = closest_point.length_squared();
145        }
146
147        let dist = min_dist_sq.sqrt();
148        let normal = if dist > 1e-6 {
149            closest_point / dist
150        } else {
151            Vec3::X
152        };
153        Some((dist, normal))
154    }
155
156    fn closest_point_on_simplex(simplex: &mut Vec<Vec3>) -> Vec3 {
157        match simplex.len() {
158            1 => simplex[0],
159            2 => {
160                let b = simplex[0];
161                let a = simplex[1];
162                let ab = b - a;
163                let ao = -a;
164                let t = ao.dot(ab) / ab.length_squared().max(1e-8);
165                if t <= 0.0 {
166                    simplex.remove(0);
167                    a
168                } else if t >= 1.0 {
169                    simplex.remove(1);
170                    b
171                } else {
172                    a + ab * t
173                }
174            }
175            3 => {
176                let c = simplex[0];
177                let b = simplex[1];
178                let a = simplex[2];
179
180                let ab = b - a;
181                let ac = c - a;
182                let ap = -a;
183
184                let d1 = ab.dot(ap);
185                let d2 = ac.dot(ap);
186                if d1 <= 0.0 && d2 <= 0.0 {
187                    *simplex = vec![a];
188                    return a;
189                }
190
191                let bp = -b;
192                let d3 = ab.dot(bp);
193                let d4 = ac.dot(bp);
194                if d3 >= 0.0 && d4 <= d3 {
195                    *simplex = vec![b];
196                    return b;
197                }
198
199                let cp = -c;
200                let d5 = ab.dot(cp);
201                let d6 = ac.dot(cp);
202                if d6 >= 0.0 && d5 <= d6 {
203                    *simplex = vec![c];
204                    return c;
205                }
206
207                let vc = d1 * d4 - d3 * d2;
208                if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
209                    let v = d1 / (d1 - d3);
210                    *simplex = vec![b, a];
211                    return a + ab * v;
212                }
213
214                let vb = d5 * d2 - d1 * d6;
215                if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
216                    let w = d2 / (d2 - d6);
217                    *simplex = vec![c, a];
218                    return a + ac * w;
219                }
220
221                let va = d3 * d6 - d5 * d4;
222                if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
223                    let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
224                    *simplex = vec![c, b];
225                    return b + (c - b) * w;
226                }
227
228                let denom = 1.0 / (va + vb + vc);
229                let v = vb * denom;
230                let w = vc * denom;
231                a + ab * v + ac * w
232            }
233            4 => {
234                let d = simplex[0];
235                let c = simplex[1];
236                let b = simplex[2];
237                let a = simplex[3];
238
239                let abc = (b - a).cross(c - a);
240                let acd = (c - a).cross(d - a);
241                let adb = (d - a).cross(b - a);
242                let bdc = (c - b).cross(d - b);
243
244                if abc.dot(-a) > 0.0 {
245                    *simplex = vec![c, b, a];
246                    return Self::closest_point_on_simplex(simplex);
247                }
248                if acd.dot(-a) > 0.0 {
249                    *simplex = vec![d, c, a];
250                    return Self::closest_point_on_simplex(simplex);
251                }
252                if adb.dot(-a) > 0.0 {
253                    *simplex = vec![b, d, a];
254                    return Self::closest_point_on_simplex(simplex);
255                }
256                if bdc.dot(-b) > 0.0 {
257                    *simplex = vec![d, c, b];
258                    return Self::closest_point_on_simplex(simplex);
259                }
260
261                Vec3::ZERO
262            }
263            _ => Vec3::ZERO,
264        }
265    }
266
267    /// Exact TOI (Time of Impact) using Conservative Advancement
268    pub fn conservative_advancement(
269        shape_a: &ColliderShape,
270        mut pos_a: Vec3,
271        rot_a: gizmo_math::Quat,
272        vel_a: Vec3,
273        shape_b: &ColliderShape,
274        mut pos_b: Vec3,
275        rot_b: gizmo_math::Quat,
276        vel_b: Vec3,
277        max_t: f32,
278    ) -> Option<(f32, Vec3)> {
279        let mut t = 0.0;
280        let rel_vel = vel_a - vel_b;
281
282        if rel_vel.length_squared() < 1e-6 {
283            return None;
284        }
285
286        for _ in 0..32 {
287            let support = |dir: Vec3| {
288                let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
289                let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
290                sa - sb
291            };
292
293            if let Some((dist, normal)) = Self::distance(support) {
294                if dist < 0.001 {
295                    return Some((t, normal));
296                }
297
298                let closing_vel = -rel_vel.dot(normal);
299
300                if closing_vel <= 0.0 {
301                    return None;
302                }
303
304                let delta_t = dist / closing_vel;
305                t += delta_t;
306
307                if t > max_t {
308                    return None;
309                }
310
311                pos_a += vel_a * delta_t;
312                pos_b += vel_b * delta_t;
313            } else {
314                return None;
315            }
316        }
317
318        None
319    }
320
321    /// Exact CCD Sweep Test using Conservative Advancement
322    pub fn speculative_contact(
323        shape_a: &ColliderShape,
324        pos_a: Vec3,
325        rot_a: gizmo_math::Quat,
326        vel_a: Vec3,
327        shape_b: &ColliderShape,
328        pos_b: Vec3,
329        rot_b: gizmo_math::Quat,
330        vel_b: Vec3,
331        dt: f32,
332    ) -> Option<ContactPoint> {
333        if let Some((t, _ca_normal)) = Self::conservative_advancement(
334            shape_a, pos_a, rot_a, vel_a, shape_b, pos_b, rot_b, vel_b, dt,
335        ) {
336            let hit_pos_a = pos_a + vel_a * t;
337            let hit_pos_b = pos_b + vel_b * t;
338
339            // Derive normal from actual body positions at TOI — more reliable
340            // than the CA normal which can be a fallback Vec3::X when dist ≈ 0.
341            let normal_a_to_b = (hit_pos_b - hit_pos_a)
342                .try_normalize()
343                .unwrap_or(Vec3::X);
344
345            let contact_point = (hit_pos_a + hit_pos_b) * 0.5;
346
347            return Some(ContactPoint {
348                point: contact_point,
349                normal: normal_a_to_b,
350                penetration: 0.0, // speculative — bodies are just touching at TOI
351                local_point_a: contact_point - pos_a,
352                local_point_b: contact_point - pos_b,
353                normal_impulse: 0.0,
354                tangent_impulse: Vec3::ZERO,
355            });
356        }
357        None
358    }
359
360    fn handle_simplex(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
361        match simplex.len() {
362            2 => Self::line_case(simplex, direction),
363            3 => Self::triangle_case(simplex, direction),
364            4 => Self::tetrahedron_case(simplex, direction),
365            _ => false,
366        }
367    }
368
369    fn line_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
370        let a = simplex[1];
371        let b = simplex[0];
372
373        let ab = b - a;
374        let ao = -a;
375
376        if ab.dot(ao) > 0.0 {
377            let mut cross = ab.cross(ao);
378            if cross.length_squared() < 1e-6 {
379                cross = if ab.x.abs() > ab.y.abs() {
380                    Vec3::new(ab.y, -ab.x, 0.0)
381                } else {
382                    Vec3::new(0.0, ab.z, -ab.y)
383                };
384            }
385            *direction = cross.cross(ab);
386        } else {
387            simplex.remove(0);
388            *direction = ao;
389        }
390
391        false
392    }
393
394    fn triangle_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
395        let a = simplex[2];
396        let b = simplex[1];
397        let c = simplex[0];
398
399        let ab = b - a;
400        let ac = c - a;
401        let ao = -a;
402
403        let abc = ab.cross(ac);
404
405        if abc.cross(ac).dot(ao) > 0.0 {
406            if ac.dot(ao) > 0.0 {
407                *simplex = vec![c, a];
408                let mut cross = ac.cross(ao);
409                if cross.length_squared() < 1e-6 {
410                    cross = if ac.x.abs() > ac.y.abs() {
411                        Vec3::new(ac.y, -ac.x, 0.0)
412                    } else {
413                        Vec3::new(0.0, ac.z, -ac.y)
414                    };
415                }
416                *direction = cross.cross(ac);
417            } else {
418                *simplex = vec![b, a];
419                let mut cross = ab.cross(ao);
420                if cross.length_squared() < 1e-6 {
421                    cross = if ab.x.abs() > ab.y.abs() {
422                        Vec3::new(ab.y, -ab.x, 0.0)
423                    } else {
424                        Vec3::new(0.0, ab.z, -ab.y)
425                    };
426                }
427                *direction = cross.cross(ab);
428            }
429        } else if ab.cross(abc).dot(ao) > 0.0 {
430            *simplex = vec![b, a];
431            let mut cross = ab.cross(ao);
432            if cross.length_squared() < 1e-6 {
433                cross = if ab.x.abs() > ab.y.abs() {
434                    Vec3::new(ab.y, -ab.x, 0.0)
435                } else {
436                    Vec3::new(0.0, ab.z, -ab.y)
437                };
438            }
439            *direction = cross.cross(ab);
440        } else {
441            if abc.dot(ao) > 0.0 {
442                *direction = abc;
443            } else {
444                simplex.swap(0, 1);
445                *direction = -abc;
446            }
447        }
448
449        false
450    }
451
452    fn tetrahedron_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
453        let a = simplex[3];
454        let b = simplex[2];
455        let c = simplex[1];
456        let d = simplex[0];
457
458        let ab = b - a;
459        let ac = c - a;
460        let ad = d - a;
461        let ao = -a;
462
463        let abc = ab.cross(ac);
464        let acd = ac.cross(ad);
465        let adb = ad.cross(ab);
466
467        if abc.dot(ao) > 0.0 {
468            simplex.remove(0);
469            return Self::triangle_case(simplex, direction);
470        }
471
472        if acd.dot(ao) > 0.0 {
473            simplex.remove(2);
474            simplex.swap(0, 1);
475            return Self::triangle_case(simplex, direction);
476        }
477
478        if adb.dot(ao) > 0.0 {
479            simplex.remove(1);
480            return Self::triangle_case(simplex, direction);
481        }
482
483        true
484    }
485
486    /// EPA (Expanding Polytope Algorithm) for contact information
487    fn epa(
488        mut simplex: Vec<Vec3>,
489        shape_a: &ColliderShape,
490        pos_a: Vec3,
491        rot_a: gizmo_math::Quat,
492        shape_b: &ColliderShape,
493        pos_b: Vec3,
494        rot_b: gizmo_math::Quat,
495    ) -> Option<ContactPoint> {
496        let support = |dir: Vec3| {
497            let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
498            let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
499            sa - sb
500        };
501
502        let mut faces: Vec<(usize, usize, usize)> = Vec::new();
503        let mut edges = Vec::new();
504
505        if simplex.len() == 4 {
506            // FIX 4: Ensure consistent outward-facing winding order for all initial faces.
507            // compute_face_normal already does a dot-product check, but we enforce it
508            // here so that newly-added faces during expansion also stay consistent.
509            let initial_faces = [(0, 1, 2), (0, 3, 1), (0, 2, 3), (1, 3, 2)];
510            for (a, b, c) in initial_faces {
511                let n = Self::compute_face_normal(&simplex, a, b, c);
512                // Guarantee outward orientation: n · v_a > 0
513                if n.dot(simplex[a]) >= 0.0 {
514                    faces.push((a, b, c));
515                } else {
516                    faces.push((a, c, b)); // flip winding
517                }
518            }
519        } else {
520            return None;
521        }
522
523        for _ in 0..EPA_MAX_ITERATIONS {
524            let (_closest_face_idx, normal, distance) = Self::find_closest_face(&simplex, &faces)?;
525
526            let support_point = support(normal);
527            let support_distance = support_point.dot(normal);
528
529            if support_distance - distance < EPA_TOLERANCE {
530                break;
531            }
532
533            let new_point_idx = simplex.len();
534            simplex.push(support_point);
535
536            edges.clear();
537            let mut i = 0;
538            while i < faces.len() {
539                let (a, b, c) = faces[i];
540                let face_normal = Self::compute_face_normal(&simplex, a, b, c);
541                let to_point = simplex[new_point_idx] - simplex[a];
542
543                if face_normal.dot(to_point) > 0.0 {
544                    Self::add_edge(&mut edges, a, b);
545                    Self::add_edge(&mut edges, b, c);
546                    Self::add_edge(&mut edges, c, a);
547                    faces.swap_remove(i);
548                } else {
549                    i += 1;
550                }
551            }
552
553            // FIX 4: When stitching new faces, enforce outward winding order
554            for (e1, e2) in &edges {
555                let a = *e1;
556                let b = *e2;
557                let c = new_point_idx;
558                let n = Self::compute_face_normal(&simplex, a, b, c);
559                if n.dot(simplex[a]) >= 0.0 {
560                    faces.push((a, b, c));
561                } else {
562                    faces.push((b, a, c)); // flip winding
563                }
564            }
565        }
566
567        let (closest_idx, normal, penetration) = Self::find_closest_face(&simplex, &faces)?;
568
569        // FIX 1 + FIX 2: Correct support directions and barycentric contact point.
570        //
571        // normal points from Minkowski surface outward (i.e., from B toward A in world space).
572        // shape_a's extreme point in +normal direction = the deepest point of A along the contact.
573        // shape_b's extreme point in -normal direction = the deepest point of B along the contact.
574        let pt_a = Self::support_point(shape_a, pos_a, rot_a, normal); // was -normal — FIXED
575        let pt_b = Self::support_point(shape_b, pos_b, rot_b, -normal); // correct, unchanged
576
577        // FIX 2: Barycentric interpolation on the closest EPA face for a more accurate
578        // contact point, especially for box corner/edge collisions.
579        let (fa, fb, fc) = faces[closest_idx];
580        let contact_point = Self::barycentric_contact_point(
581            &simplex, fa, fb, fc, normal, shape_a, pos_a, rot_a, shape_b, pos_b, rot_b,
582        )
583        .unwrap_or_else(|| (pt_a + pt_b) * 0.5);
584
585        Some(ContactPoint {
586            point: contact_point,
587            normal,
588            penetration,
589            local_point_a: contact_point - pos_a,
590            local_point_b: contact_point - pos_b,
591            normal_impulse: 0.0,
592            tangent_impulse: Vec3::ZERO,
593        })
594    }
595
596    /// Compute the contact point via barycentric coordinates of the closest EPA face.
597    /// Each Minkowski vertex v_i = support_a(d_i) - support_b(-d_i).
598    /// The contact point on A is: sum(w_i * support_a(d_i)), and similarly for B.
599    /// We approximate by projecting the origin onto the closest face and using those weights.
600    fn barycentric_contact_point(
601        simplex: &[Vec3],
602        a: usize,
603        b: usize,
604        c: usize,
605        _normal: Vec3,
606        shape_a: &ColliderShape,
607        pos_a: Vec3,
608        rot_a: gizmo_math::Quat,
609        shape_b: &ColliderShape,
610        pos_b: Vec3,
611        rot_b: gizmo_math::Quat,
612    ) -> Option<Vec3> {
613        let va = simplex[a];
614        let vb = simplex[b];
615        let vc = simplex[c];
616
617        // Project origin onto the triangle plane and compute barycentric weights
618        let (u, v, w) = Self::barycentric_coords(va, vb, vc, Vec3::ZERO)?;
619
620        // Recover the original support directions for each Minkowski vertex.
621        // This is an approximation: we use the vertex positions as direction hints.
622        // For exact results one would cache the per-vertex support directions during GJK/EPA.
623        let dir_a = va.try_normalize().unwrap_or(Vec3::X);
624        let dir_b = vb.try_normalize().unwrap_or(Vec3::X);
625        let dir_c = vc.try_normalize().unwrap_or(Vec3::X);
626
627        let pt_a = u * Self::support_point(shape_a, pos_a, rot_a, dir_a)
628            + v * Self::support_point(shape_a, pos_a, rot_a, dir_b)
629            + w * Self::support_point(shape_a, pos_a, rot_a, dir_c);
630        let pt_b = u * Self::support_point(shape_b, pos_b, rot_b, -dir_a)
631            + v * Self::support_point(shape_b, pos_b, rot_b, -dir_b)
632            + w * Self::support_point(shape_b, pos_b, rot_b, -dir_c);
633
634        Some((pt_a + pt_b) * 0.5)
635    }
636
637    /// Barycentric coordinates of point p projected onto triangle (a, b, c).
638    /// Returns None if the triangle is degenerate.
639    fn barycentric_coords(a: Vec3, b: Vec3, c: Vec3, p: Vec3) -> Option<(f32, f32, f32)> {
640        let ab = b - a;
641        let ac = c - a;
642
643        let d3 = ab.dot(b - a); // = ab·ab
644        let d4 = ac.dot(b - a);
645        let d5 = ab.dot(c - a);
646        let d6 = ac.dot(c - a); // = ac·ac
647
648        let denom = d3 * d6 - d4 * d5;
649        if denom.abs() < 1e-8 {
650            return None; // Degenerate triangle
651        }
652
653        // Correct standard formula using Cramer's rule
654        let ab = b - a;
655        let ac = c - a;
656        let ap = p - a;
657
658        let d00 = ab.dot(ab);
659        let d01 = ab.dot(ac);
660        let d11 = ac.dot(ac);
661        let d20 = ap.dot(ab);
662        let d21 = ap.dot(ac);
663
664        let denom = d00 * d11 - d01 * d01;
665        if denom.abs() < 1e-8 {
666            return None;
667        }
668
669        let v = (d11 * d20 - d01 * d21) / denom;
670        let w = (d00 * d21 - d01 * d20) / denom;
671        let u = 1.0 - v - w;
672
673        Some((u, v, w))
674    }
675
676    fn find_closest_face(
677        simplex: &[Vec3],
678        faces: &[(usize, usize, usize)],
679    ) -> Option<(usize, Vec3, f32)> {
680        let mut min_distance = f32::INFINITY;
681        let mut closest_idx = 0;
682        let mut closest_normal = Vec3::ZERO;
683
684        for (i, &(a, b, c)) in faces.iter().enumerate() {
685            let normal = Self::compute_face_normal(simplex, a, b, c);
686            let distance = normal.dot(simplex[a]);
687
688            if distance < min_distance {
689                min_distance = distance;
690                closest_idx = i;
691                closest_normal = normal;
692            }
693        }
694
695        if min_distance == f32::INFINITY {
696            None
697        } else {
698            Some((closest_idx, closest_normal, min_distance))
699        }
700    }
701
702    fn compute_face_normal(simplex: &[Vec3], a: usize, b: usize, c: usize) -> Vec3 {
703        let ab = simplex[b] - simplex[a];
704        let ac = simplex[c] - simplex[a];
705        let normal_raw = ab.cross(ac);
706        // Ensure outward orientation (away from origin)
707        if normal_raw.dot(simplex[a]) < 0.0 {
708            -normal_raw.try_normalize().unwrap_or(Vec3::X)
709        } else {
710            normal_raw.try_normalize().unwrap_or(Vec3::X)
711        }
712    }
713
714    fn add_edge(edges: &mut Vec<(usize, usize)>, a: usize, b: usize) {
715        let reverse = (b, a);
716        let forward = (a, b);
717
718        if let Some(pos) = edges.iter().position(|&e| e == reverse) {
719            edges.swap_remove(pos);
720        } else if let Some(pos) = edges.iter().position(|&e| e == forward) {
721            edges.swap_remove(pos);
722        } else {
723            edges.push((a, b));
724        }
725    }
726
727    /// Get support point for a shape in a given direction
728    pub fn support_point(
729        shape: &ColliderShape,
730        pos: Vec3,
731        rot: gizmo_math::Quat,
732        dir: Vec3,
733    ) -> Vec3 {
734        let local_dir = rot.inverse() * dir;
735
736        let local_support = match shape {
737            ColliderShape::Sphere(s) => Self::sphere_support(s, local_dir),
738            ColliderShape::Box(b) => Self::box_support(b, local_dir),
739            ColliderShape::Capsule(c) => Self::capsule_support(c, local_dir),
740            ColliderShape::Plane(_) => {
741                debug_assert!(false, "Plane shapes must use separate collision detection");
742                Vec3::ZERO
743            }
744            ColliderShape::TriMesh(tm) => {
745                let mut best_dot = f32::NEG_INFINITY;
746                let mut best_pt = Vec3::ZERO;
747
748                if !tm.bvh.nodes.is_empty() {
749                    let mut stack = Vec::with_capacity(64);
750                    stack.push(0);
751
752                    let abs_dir = gizmo_math::Vec3A::new(
753                        local_dir.x.abs(),
754                        local_dir.y.abs(),
755                        local_dir.z.abs(),
756                    );
757                    let dir_a = gizmo_math::Vec3A::new(local_dir.x, local_dir.y, local_dir.z);
758
759                    while let Some(node_idx) = stack.pop() {
760                        let node = &tm.bvh.nodes[node_idx];
761
762                        let center = node.aabb.center();
763                        let half_extents = node.aabb.half_extents();
764
765                        let max_node_dot = center.dot(dir_a)
766                            + half_extents.x * abs_dir.x
767                            + half_extents.y * abs_dir.y
768                            + half_extents.z * abs_dir.z;
769
770                        if max_node_dot < best_dot {
771                            continue;
772                        }
773
774                        if node.is_leaf() {
775                            let start = (node.first_tri_index * 3) as usize;
776                            let end = start + (node.tri_count * 3) as usize;
777                            for i in start..end {
778                                let v = tm.vertices[tm.indices[i] as usize];
779                                let d = v.dot(local_dir);
780                                if d > best_dot {
781                                    best_dot = d;
782                                    best_pt = v;
783                                }
784                            }
785                        } else {
786                            if node.left_child >= 0 {
787                                stack.push(node.left_child as usize);
788                            }
789                            if node.right_child >= 0 {
790                                stack.push(node.right_child as usize);
791                            }
792                        }
793                    }
794                } else {
795                    for v in tm.vertices.iter() {
796                        let d = v.dot(local_dir);
797                        if d > best_dot {
798                            best_dot = d;
799                            best_pt = *v;
800                        }
801                    }
802                }
803                best_pt
804            }
805            ColliderShape::ConvexHull(ch) => {
806                let mut best_dot = f32::NEG_INFINITY;
807                let mut best_pt = Vec3::ZERO;
808                for v in ch.vertices.iter() {
809                    let d = v.dot(local_dir);
810                    if d > best_dot {
811                        best_dot = d;
812                        best_pt = *v;
813                    }
814                }
815                best_pt
816            }
817            crate::components::ColliderShape::Compound(_) => {
818                debug_assert!(
819                    false,
820                    "Compound shapes must use separate collision detection"
821                );
822                Vec3::ZERO
823            }
824        };
825
826        pos + rot * local_support
827    }
828
829    fn sphere_support(sphere: &SphereShape, dir: Vec3) -> Vec3 {
830        dir.try_normalize().unwrap_or(Vec3::X) * sphere.radius
831    }
832
833    fn box_support(box_shape: &BoxShape, dir: Vec3) -> Vec3 {
834        Vec3::new(
835            if dir.x > 0.0 {
836                box_shape.half_extents.x
837            } else {
838                -box_shape.half_extents.x
839            },
840            if dir.y > 0.0 {
841                box_shape.half_extents.y
842            } else {
843                -box_shape.half_extents.y
844            },
845            if dir.z > 0.0 {
846                box_shape.half_extents.z
847            } else {
848                -box_shape.half_extents.z
849            },
850        )
851    }
852
853    fn capsule_support(capsule: &CapsuleShape, dir: Vec3) -> Vec3 {
854        let dir_normalized = dir.try_normalize().unwrap_or(Vec3::X);
855        let sphere_center = if dir_normalized.y > 0.0 {
856            Vec3::new(0.0, capsule.half_height, 0.0)
857        } else {
858            Vec3::new(0.0, -capsule.half_height, 0.0)
859        };
860        sphere_center + dir_normalized * capsule.radius
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use gizmo_math::{Quat, Vec3};
868
869    #[test]
870    fn test_sphere_vs_sphere_collision() {
871        let shape = ColliderShape::Sphere(SphereShape { radius: 1.0 });
872
873        assert!(Gjk::test_collision(
874            &shape,
875            Vec3::ZERO,
876            Quat::IDENTITY,
877            &shape,
878            Vec3::new(1.5, 0.0, 0.0),
879            Quat::IDENTITY
880        ));
881        assert!(!Gjk::test_collision(
882            &shape,
883            Vec3::ZERO,
884            Quat::IDENTITY,
885            &shape,
886            Vec3::new(2.5, 0.0, 0.0),
887            Quat::IDENTITY
888        ));
889    }
890
891    #[test]
892    fn test_box_vs_box_collision() {
893        let shape = ColliderShape::Box(BoxShape {
894            half_extents: Vec3::new(1.0, 1.0, 1.0),
895        });
896
897        assert!(Gjk::test_collision(
898            &shape,
899            Vec3::ZERO,
900            Quat::IDENTITY,
901            &shape,
902            Vec3::new(1.5, 0.0, 0.0),
903            Quat::IDENTITY
904        ));
905        assert!(!Gjk::test_collision(
906            &shape,
907            Vec3::ZERO,
908            Quat::IDENTITY,
909            &shape,
910            Vec3::new(2.5, 0.0, 0.0),
911            Quat::IDENTITY
912        ));
913    }
914
915    #[test]
916    fn test_epa_contact_generation() {
917        let shape_a = ColliderShape::Box(BoxShape {
918            half_extents: Vec3::new(1.0, 1.0, 1.0),
919        });
920        let shape_b = ColliderShape::Box(BoxShape {
921            half_extents: Vec3::new(1.0, 1.0, 1.0),
922        });
923
924        let contact = Gjk::get_contact(
925            &shape_a,
926            Vec3::ZERO,
927            Quat::IDENTITY,
928            &shape_b,
929            Vec3::new(1.5, 0.0, 0.0),
930            Quat::IDENTITY,
931        );
932
933        assert!(contact.is_some(), "EPA failed to generate contact");
934        let contact = contact.unwrap();
935
936        assert!(
937            (contact.penetration - 0.5).abs() < 0.001,
938            "Penetration depth is wrong: {}",
939            contact.penetration
940        );
941        assert!(
942            (contact.normal.x.abs() - 1.0).abs() < 0.001,
943            "Normal is wrong: {:?}",
944            contact.normal
945        );
946    }
947
948    #[test]
949    fn test_speculative_contact_approaching() {
950        // Two spheres approaching each other — should produce a contact
951        let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
952
953        let contact = Gjk::speculative_contact(
954            &shape,
955            Vec3::new(-5.0, 0.0, 0.0),
956            Quat::IDENTITY,
957            Vec3::new(10.0, 0.0, 0.0),
958            &shape,
959            Vec3::new(5.0, 0.0, 0.0),
960            Quat::IDENTITY,
961            Vec3::new(-10.0, 0.0, 0.0),
962            1.0,
963        );
964
965        assert!(
966            contact.is_some(),
967            "Speculative contact missed approaching spheres"
968        );
969    }
970
971    #[test]
972    fn test_speculative_contact_separating() {
973        // Two spheres moving apart — should NOT produce a contact
974        let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
975
976        let contact = Gjk::speculative_contact(
977            &shape,
978            Vec3::new(-5.0, 0.0, 0.0),
979            Quat::IDENTITY,
980            Vec3::new(-10.0, 0.0, 0.0),
981            &shape,
982            Vec3::new(5.0, 0.0, 0.0),
983            Quat::IDENTITY,
984            Vec3::new(10.0, 0.0, 0.0),
985            1.0,
986        );
987
988        assert!(
989            contact.is_none(),
990            "Speculative contact incorrectly fired for separating shapes"
991        );
992    }
993}