Skip to main content

parry2d/query/gjk/
gjk.rs

1//! The Gilbert-Johnson-Keerthi distance algorithm.
2//!
3//! # What is GJK?
4//!
5//! The **Gilbert-Johnson-Keerthi (GJK)** algorithm is a fundamental geometric algorithm used
6//! to compute the distance between two convex shapes. It's one of the most important algorithms
7//! in collision detection and is used extensively in physics engines, robotics, and computer graphics.
8//!
9//! ## How GJK Works (Simplified)
10//!
11//! GJK works by operating on the **Minkowski difference** (also called Configuration Space Obstacle or CSO)
12//! of two shapes. Instead of directly comparing the shapes, GJK:
13//!
14//! 1. Constructs a simplex (triangle in 2D, tetrahedron in 3D) within the Minkowski difference
15//! 2. Iteratively refines this simplex to find the point closest to the origin
16//! 3. The distance from the origin to this closest point equals the distance between the shapes
17//!
18//! If the origin is **inside** the Minkowski difference, the shapes are intersecting.
19//! If the origin is **outside**, the distance to the closest point gives the separation distance.
20//!
21//! ## When is GJK Used?
22//!
23//! GJK is used whenever you need to:
24//! - **Check if two convex shapes intersect** (collision detection)
25//! - **Find the minimum distance** between two convex shapes
26//! - **Compute closest points** on two shapes
27//! - **Cast a shape along a direction** to find the time of impact (continuous collision detection)
28//!
29//! ## Key Advantages of GJK
30//!
31//! - Works with **any convex shape** that can provide a support function
32//! - Does **not require the full geometry** of the shapes (only support points)
33//! - Very **fast convergence** in most practical cases
34//! - Forms the basis for many collision detection systems
35//!
36//! ## Limitations
37//!
38//! - Only works with **convex shapes** (use convex decomposition for concave shapes)
39//! - When shapes are penetrating, GJK can only detect intersection but not penetration depth
40//!   (use EPA - Expanding Polytope Algorithm - for penetration depth)
41//!
42//! # Main Functions in This Module
43//!
44//! - [`closest_points`] - The core GJK algorithm for finding distance and closest points
45//! - [`project_origin`] - Projects the origin onto a shape's boundary
46//! - [`cast_local_ray`] - Casts a ray against a shape (used for raycasting)
47//! - [`directional_distance`] - Computes how far a shape can move before touching another
48//!
49//! # Example
50//!
51//! See individual function documentation for usage examples.
52
53use crate::math::ComplexField;
54
55use crate::query::gjk::{ConstantOrigin, CsoPoint, VoronoiSimplex};
56use crate::shape::SupportMap;
57// use query::Proximity;
58use crate::math::{Pose, Real, Vector, DIM};
59use crate::query::{self, Ray};
60
61use num::{Bounded, Zero};
62
63/// Results of the GJK algorithm.
64///
65/// This enum represents the different outcomes when running the GJK algorithm to find
66/// the distance between two shapes. The result depends on whether the shapes are intersecting,
67/// how far apart they are, and what information was requested.
68///
69/// # Understanding the Results
70///
71/// - **Intersection**: The shapes are overlapping. The origin lies inside the Minkowski difference.
72/// - **ClosestPoints**: The exact closest points on both shapes were computed, along with the
73///   separation direction.
74/// - **Proximity**: The shapes are close but not intersecting. Only an approximate separation
75///   direction is provided (used when exact distance computation is not needed).
76/// - **NoIntersection**: The shapes are too far apart (beyond the specified `max_dist` threshold).
77///
78/// # Coordinate Spaces
79///
80/// All points and vectors in this result are expressed in the **local-space of the first shape**
81/// (the shape passed as `g1` to the GJK functions). This is important when working with
82/// transformed shapes.
83#[derive(Clone, Debug, PartialEq)]
84pub enum GJKResult {
85    /// The shapes are intersecting (overlapping).
86    ///
87    /// This means the origin is inside the Minkowski difference of the two shapes.
88    /// GJK cannot compute penetration depth; use the EPA (Expanding Polytope Algorithm)
89    /// for that purpose.
90    Intersection,
91
92    /// The closest points on both shapes were found.
93    ///
94    /// # Fields
95    ///
96    /// - First `Vector`: The closest point on the first shape (in local-space of shape 1)
97    /// - Second `Vector`: The closest point on the second shape (in local-space of shape 1)
98    /// - `Vector`: The unit direction vector from shape 1 to shape 2 (separation axis)
99    ///
100    /// This variant is returned when `exact_dist` is `true` in the GJK algorithm and the
101    /// shapes are not intersecting.
102    ClosestPoints(Vector, Vector, Vector),
103
104    /// The shapes are in proximity (close but not intersecting).
105    ///
106    /// # Fields
107    ///
108    /// - `Vector`: An approximate separation axis (unit direction from shape 1 to shape 2)
109    ///
110    /// This variant is returned when `exact_dist` is `false` and the algorithm determines
111    /// the shapes are close but not intersecting. It's faster than computing exact closest
112    /// points when you only need to know if shapes are nearby.
113    Proximity(Vector),
114
115    /// The shapes are too far apart.
116    ///
117    /// # Fields
118    ///
119    /// - `Vector`: A separation axis (unit direction from shape 1 to shape 2)
120    ///
121    /// This variant is returned when the minimum distance between the shapes exceeds
122    /// the `max_dist` parameter passed to the GJK algorithm.
123    NoIntersection(Vector),
124}
125
126/// The absolute tolerance used by the GJK algorithm.
127///
128/// This function returns the epsilon (tolerance) value that GJK uses to determine when
129/// it has converged to a solution. The tolerance affects:
130///
131/// - When two points are considered "close enough" to be the same
132/// - When the algorithm decides it has found the minimum distance
133/// - Numerical stability in edge cases
134///
135/// The returned value is 10 times the default machine epsilon for the current floating-point
136/// precision (f32 or f64). This provides a balance between accuracy and robustness.
137///
138/// # Returns
139///
140/// The absolute tolerance value (10 * DEFAULT_EPSILON)
141pub fn eps_tol() -> Real {
142    let _eps = crate::math::DEFAULT_EPSILON;
143    _eps * 10.0
144}
145
146/// Projects the origin onto the boundary of the given shape.
147///
148/// This function finds the point on the shape's surface that is closest to the origin (0, 0)
149/// in 2D or (0, 0, 0) in 3D. This is useful for distance queries and collision detection
150/// when you need to know the closest point on a shape.
151///
152/// # Important: Origin Must Be Outside
153///
154/// **The origin is assumed to be outside of the shape.** If the origin is inside the shape,
155/// this function returns `None`. For penetrating cases, use the EPA (Expanding Polytope Algorithm)
156/// instead.
157///
158/// # Parameters
159///
160/// - `m`: The position and orientation (isometry) of the shape in world space
161/// - `g`: The shape to project onto (must implement `SupportMap`)
162/// - `simplex`: A reusable simplex structure for the GJK algorithm. Initialize with
163///   `VoronoiSimplex::new()` before first use.
164///
165/// # Returns
166///
167/// - `Some(Vector)`: The closest point on the shape's boundary, in the shape's **local space**
168/// - `None`: If the origin is inside the shape
169///
170/// # Example
171///
172/// ```rust
173/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
174/// use parry3d::shape::Ball;
175/// use parry3d::query::gjk::{project_origin, VoronoiSimplex};
176/// use parry3d::math::Pose;
177///
178/// // Create a ball at position (5, 0, 0)
179/// let ball = Ball::new(1.0);
180/// let position = Pose::translation(5.0, 0.0, 0.0);
181///
182/// // Project the origin onto the ball
183/// let mut simplex = VoronoiSimplex::new();
184/// if let Some(closest_point) = project_origin(&position, &ball, &mut simplex) {
185///     println!("Closest point on ball: {:?}", closest_point);
186///     // The point will be approximately (-1, 0, 0) in local space
187///     // which is the left side of the ball facing the origin
188/// }
189/// # }
190/// ```
191///
192/// # Performance Note
193///
194/// The `simplex` parameter can be reused across multiple calls to avoid allocations.
195/// This is particularly beneficial when performing many projection queries.
196pub fn project_origin<G: ?Sized + SupportMap>(
197    m: &Pose,
198    g: &G,
199    simplex: &mut VoronoiSimplex,
200) -> Option<Vector> {
201    match closest_points(
202        &m.inverse(),
203        g,
204        &ConstantOrigin,
205        Real::max_value(),
206        true,
207        simplex,
208    ) {
209        GJKResult::Intersection => None,
210        GJKResult::ClosestPoints(p, _, _) => Some(p),
211        _ => unreachable!(),
212    }
213}
214
215/*
216 * Separating Axis GJK
217 */
218/// Computes the closest points between two shapes using the GJK algorithm.
219///
220/// This is the **core function** of the GJK implementation in Parry. It can compute:
221/// - Whether two shapes are intersecting
222/// - The distance between two separated shapes
223/// - The closest points on both shapes
224/// - An approximate separation axis when exact distance isn't needed
225///
226/// # How It Works
227///
228/// The algorithm operates on the Minkowski difference (CSO) of the two shapes and iteratively
229/// builds a simplex that approximates the point closest to the origin. The algorithm terminates
230/// when:
231/// - The shapes are proven to intersect (origin is inside the CSO)
232/// - The minimum distance is found within the tolerance
233/// - The shapes are proven to be farther than `max_dist` apart
234///
235/// # Parameters
236///
237/// - `pos12`: The relative position of shape 2 with respect to shape 1. This is the isometry
238///   that transforms from shape 1's space to shape 2's space.
239/// - `g1`: The first shape (must implement `SupportMap`)
240/// - `g2`: The second shape (must implement `SupportMap`)
241/// - `max_dist`: Maximum distance to check. If shapes are farther than this, the algorithm
242///   returns `GJKResult::NoIntersection` early. Use `Real::max_value()` to disable this check.
243/// - `exact_dist`: Whether to compute exact closest points:
244///   - `true`: Computes exact distance and returns `GJKResult::ClosestPoints`
245///   - `false`: May return `GJKResult::Proximity` with only an approximate separation axis,
246///     which is faster when you only need to know if shapes are nearby
247/// - `simplex`: A reusable simplex structure. Initialize with `VoronoiSimplex::new()` before
248///   first use. Can be reused across calls for better performance.
249///
250/// # Returns
251///
252/// Returns a [`GJKResult`] which can be:
253/// - `Intersection`: The shapes are overlapping
254/// - `ClosestPoints(p1, p2, normal)`: The closest points on each shape (when `exact_dist` is true)
255/// - `Proximity(axis)`: An approximate separation axis (when `exact_dist` is false)
256/// - `NoIntersection(axis)`: The shapes are farther than `max_dist` apart
257///
258/// # Example: Basic Distance Query
259///
260/// ```rust
261/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
262/// use parry3d::shape::{Ball, Cuboid};
263/// use parry3d::query::gjk::{closest_points, VoronoiSimplex};
264/// use parry3d::math::{Pose, Vector};
265///
266/// // Create two shapes
267/// let ball = Ball::new(1.0);
268/// let cuboid = Cuboid::new(Vector::new(1.0, 1.0, 1.0));
269///
270/// // Position them in space
271/// let pos1 = Pose::translation(0.0, 0.0, 0.0);
272/// let pos2 = Pose::translation(5.0, 0.0, 0.0);
273///
274/// // Compute relative position
275/// let pos12 = pos1.inv_mul(&pos2);
276///
277/// // Run GJK
278/// let mut simplex = VoronoiSimplex::new();
279/// let result = closest_points(
280///     &pos12,
281///     &ball,
282///     &cuboid,
283///     f32::MAX,  // No distance limit
284///     true,      // Compute exact distance
285///     &mut simplex,
286/// );
287///
288/// match result {
289///     parry3d::query::gjk::GJKResult::ClosestPoints(p1, p2, normal) => {
290///         println!("Closest point on ball: {:?}", p1);
291///         println!("Closest point on cuboid: {:?}", p2);
292///         println!("Separation direction: {:?}", normal);
293///         let distance = (p2 - p1).length();
294///         println!("Distance: {}", distance);
295///     }
296///     parry3d::query::gjk::GJKResult::Intersection => {
297///         println!("Shapes are intersecting!");
298///     }
299///     _ => {}
300/// }
301/// # }
302/// ```
303///
304/// # Example: Fast Proximity Check
305///
306/// ```rust
307/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
308/// use parry3d::shape::Ball;
309/// use parry3d::query::gjk::{closest_points, VoronoiSimplex};
310/// use parry3d::math::Pose;
311///
312/// let ball1 = Ball::new(1.0);
313/// let ball2 = Ball::new(1.0);
314/// let pos12 = Pose::translation(3.0, 0.0, 0.0);
315///
316/// let mut simplex = VoronoiSimplex::new();
317/// let result = closest_points(
318///     &pos12,
319///     &ball1,
320///     &ball2,
321///     5.0,       // Only check up to distance 5.0
322///     false,     // Don't compute exact distance
323///     &mut simplex,
324/// );
325///
326/// match result {
327///     parry3d::query::gjk::GJKResult::Proximity(_axis) => {
328///         println!("Shapes are close but not intersecting");
329///     }
330///     parry3d::query::gjk::GJKResult::Intersection => {
331///         println!("Shapes are intersecting");
332///     }
333///     parry3d::query::gjk::GJKResult::NoIntersection(_) => {
334///         println!("Shapes are too far apart (> 5.0 units)");
335///     }
336///     _ => {}
337/// }
338/// # }
339/// ```
340///
341/// # Performance Tips
342///
343/// 1. Reuse the `simplex` across multiple queries to avoid allocations
344/// 2. Set `exact_dist` to `false` when you only need proximity information
345/// 3. Use a reasonable `max_dist` to allow early termination
346/// 4. GJK converges fastest when shapes are well-separated
347///
348/// # Notes
349///
350/// - All returned points and vectors are in the local-space of shape 1
351/// - The algorithm typically converges in 5-10 iterations for well-separated shapes
352/// - Maximum iteration count is 100 to prevent infinite loops
353pub fn closest_points<G1, G2>(
354    pos12: &Pose,
355    g1: &G1,
356    g2: &G2,
357    max_dist: Real,
358    exact_dist: bool,
359    simplex: &mut VoronoiSimplex,
360) -> GJKResult
361where
362    G1: ?Sized + SupportMap,
363    G2: ?Sized + SupportMap,
364{
365    let _eps = crate::math::DEFAULT_EPSILON;
366    let _eps_tol: Real = eps_tol();
367    let _eps_rel: Real = <Real as ComplexField>::sqrt(_eps_tol);
368
369    // TODO: reset the simplex if it is empty?
370    let mut proj = simplex.project_origin_and_reduce();
371
372    let mut old_dir;
373
374    if let Some(proj_dir) = proj.try_normalize() {
375        old_dir = -proj_dir;
376    } else {
377        return GJKResult::Intersection;
378    }
379
380    let mut max_bound = Real::max_value();
381    let mut dir;
382    let mut niter = 0;
383    // Tightest lower bound on the distance from the origin to the CSO: `min_bound`
384    // maximized over the directions probed. Its sign classifies the origin: `> 0`
385    // means separation, `~ 0` puts it on the CSO boundary (touching),
386    // `< 0` bounds the penetration depth.
387    let mut best_min_bound = -Real::max_value();
388    // Largest CSO support magnitude seen, used to scale the tolerance below.
389    let mut support_scale: Real = 0.0;
390    let mut nb_perturbs = 0usize;
391    const MAX_PERTURBATIONS: usize = 2 * DIM;
392
393    loop {
394        let old_max_bound = max_bound;
395
396        let neg_proj_coords = -proj;
397        let (normalized, dist) = neg_proj_coords.normalize_and_length();
398        if dist > _eps_tol {
399            dir = normalized;
400            max_bound = dist;
401        } else {
402            // The origin is on the simplex.
403            return GJKResult::Intersection;
404        }
405
406        if max_bound >= old_max_bound {
407            if best_min_bound > 0.0 {
408                // Separation certified: the converged witnesses are the answer.
409                if exact_dist {
410                    let (p1, p2) = result(simplex, true);
411                    return GJKResult::ClosestPoints(p1, p2, old_dir); // upper bounds inconsistencies
412                } else {
413                    return GJKResult::Proximity(old_dir);
414                }
415            } else if exact_dist && best_min_bound >= -_eps_rel * support_scale {
416                // Origin on the CSO boundary: the pair is touching, so the witnesses
417                // describe the contact.
418                let (p1, p2) = result(simplex, true);
419                return GJKResult::ClosestPoints(p1, p2, old_dir);
420            } else if nb_perturbs < MAX_PERTURBATIONS {
421                // Try slight perturbations to avoid getting stuck into a numerical ambiguity.
422                nb_perturbs += 1;
423                dir = perturbed_dir(dir, nb_perturbs);
424            } else {
425                // The CSO reaches past the origin in every direction probed.
426                return GJKResult::Intersection;
427            }
428        }
429
430        let cso_point = CsoPoint::from_shapes(pos12, g1, g2, dir);
431        let min_bound = -dir.dot(cso_point.point);
432
433        assert!(min_bound.is_finite());
434
435        best_min_bound = best_min_bound.max(min_bound);
436        support_scale = support_scale.max(cso_point.point.length());
437
438        if min_bound > max_dist {
439            return GJKResult::NoIntersection(dir);
440        } else if !exact_dist && min_bound > 0.0 && max_bound <= max_dist {
441            return GJKResult::Proximity(old_dir);
442        } else if max_bound - min_bound <= _eps_rel * max_bound {
443            if exact_dist {
444                let (p1, p2) = result(simplex, false);
445                return GJKResult::ClosestPoints(p1, p2, dir); // the distance found has a good enough precision
446            } else {
447                return GJKResult::Proximity(dir);
448            }
449        }
450
451        if !simplex.add_point(cso_point) {
452            // See the stagnation branch above for how `best_min_bound` classifies a stall.
453            if best_min_bound > 0.0 {
454                if exact_dist {
455                    let (p1, p2) = result(simplex, false);
456                    return GJKResult::ClosestPoints(p1, p2, dir);
457                } else {
458                    return GJKResult::Proximity(dir);
459                }
460            } else if exact_dist && best_min_bound >= -_eps_rel * support_scale {
461                let (p1, p2) = result(simplex, false);
462                return GJKResult::ClosestPoints(p1, p2, dir);
463            } else if nb_perturbs < MAX_PERTURBATIONS {
464                // The duplicate support point cannot enrich the simplex; jump back to
465                // the top of the loop where the stagnation branch will perturb `dir`.
466                continue;
467            } else {
468                return GJKResult::Intersection;
469            }
470        }
471
472        old_dir = dir;
473        proj = simplex.project_origin_and_reduce();
474
475        if simplex.dimension() == DIM {
476            if min_bound >= _eps_tol {
477                if exact_dist {
478                    let (p1, p2) = result(simplex, true);
479                    return GJKResult::ClosestPoints(p1, p2, old_dir);
480                } else {
481                    // NOTE: previous implementation used old_proj here.
482                    return GJKResult::Proximity(old_dir);
483                }
484            } else {
485                return GJKResult::Intersection; // Vector inside of the cso.
486            }
487        }
488        niter += 1;
489
490        if niter == 100 {
491            return GJKResult::NoIntersection(Vector::X);
492        }
493    }
494}
495
496/// Casts a ray against a shape using the GJK algorithm.
497///
498/// This function performs raycasting by testing a ray against a shape to find if and where
499/// the ray intersects the shape. It uses a specialized version of GJK that works with rays.
500///
501/// # What is Raycasting?
502///
503/// Raycasting shoots a ray (infinite line starting from a point in a direction) and finds
504/// where it first hits a shape. This is essential for:
505/// - Mouse picking in 3D scenes
506/// - Line-of-sight checks
507/// - Projectile collision detection
508/// - Laser/scanner simulations
509///
510/// # Parameters
511///
512/// - `shape`: The shape to cast the ray against (must implement `SupportMap`)
513/// - `simplex`: A reusable simplex structure. Initialize with `VoronoiSimplex::new()`.
514/// - `ray`: The ray to cast, containing an origin point and direction vector
515/// - `max_time_of_impact`: Maximum distance along the ray to check. The ray will be treated
516///   as a line segment of length `max_time_of_impact * ray.dir.length()`.
517///
518/// # Returns
519///
520/// - `Some((toi, normal))`: If the ray hits the shape
521///   - `toi`: Time of impact - multiply by `ray.dir.length()` to get the actual distance
522///   - `normal`: Surface normal at the hit point
523/// - `None`: If the ray doesn't hit the shape within the maximum distance
524///
525/// # Example
526///
527/// ```rust
528/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
529/// use parry3d::shape::Ball;
530/// use parry3d::query::{Ray, gjk::{cast_local_ray, VoronoiSimplex}};
531/// use parry3d::math::Vector;
532///
533/// // Create a ball at the origin
534/// let ball = Ball::new(1.0);
535///
536/// // Create a ray starting at (0, 0, -5) pointing toward +Z
537/// let ray = Ray::new(
538///     Vector::new(0.0, 0.0, -5.0),
539///     Vector::new(0.0, 0.0, 1.0)
540/// );
541///
542/// let mut simplex = VoronoiSimplex::new();
543/// if let Some((toi, normal)) = cast_local_ray(&ball, &mut simplex, &ray, f32::MAX) {
544///     let hit_point = ray.point_at(toi);
545///     println!("Ray hit at: {:?}", hit_point);
546///     println!("Surface normal: {:?}", normal);
547///     println!("Distance: {}", toi);
548/// } else {
549///     println!("Ray missed the shape");
550/// }
551/// # }
552/// ```
553///
554/// # Notes
555///
556/// - The ray is specified in the local-space of the shape
557/// - The returned normal points outward from the shape
558/// - For normalized ray directions, `toi` equals the distance to the hit point
559/// - This function is typically called by higher-level raycasting APIs
560pub fn cast_local_ray<G: ?Sized + SupportMap>(
561    shape: &G,
562    simplex: &mut VoronoiSimplex,
563    ray: &Ray,
564    max_time_of_impact: Real,
565) -> Option<(Real, Vector)> {
566    let g2 = ConstantOrigin;
567    minkowski_ray_cast(
568        &Pose::IDENTITY,
569        shape,
570        &g2,
571        ray,
572        max_time_of_impact,
573        simplex,
574    )
575}
576
577/// Computes how far a shape can move in a direction before touching another shape.
578///
579/// This function answers the question: "If I move shape 1 along this direction, how far
580/// can it travel before it touches shape 2?" This is useful for:
581/// - Continuous collision detection (CCD)
582/// - Movement planning and obstacle avoidance
583/// - Computing time-of-impact for moving objects
584/// - Safe navigation distances
585///
586/// # How It Works
587///
588/// The function casts shape 1 along the given direction vector and finds the first point
589/// where it would contact shape 2. It returns:
590/// - The distance that can be traveled
591/// - The contact normal at the point of first contact
592/// - The witness points (closest points) on both shapes at contact
593///
594/// # Parameters
595///
596/// - `pos12`: The relative position of shape 2 with respect to shape 1 (isometry from
597///   shape 1's space to shape 2's space)
598/// - `g1`: The first shape being moved (must implement `SupportMap`)
599/// - `g2`: The second shape (static target, must implement `SupportMap`)
600/// - `dir`: The direction vector to move shape 1 in (in local-space of shape 1)
601/// - `simplex`: A reusable simplex structure. Initialize with `VoronoiSimplex::new()`.
602///
603/// # Returns
604///
605/// - `Some((distance, normal, witness1, witness2))`: If contact would occur
606///   - `distance`: How far shape 1 can travel before touching shape 2
607///   - `normal`: The contact normal at the point of first contact
608///   - `witness1`: The contact point on shape 1 (in local-space of shape 1)
609///   - `witness2`: The contact point on shape 2 (in local-space of shape 1)
610/// - `None`: If no contact would occur (shapes don't intersect along this direction)
611///
612/// # Example
613///
614/// ```rust
615/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
616/// use parry3d::shape::Ball;
617/// use parry3d::query::gjk::{directional_distance, VoronoiSimplex};
618/// use parry3d::math::{Pose, Vector};
619///
620/// // Two balls: one at origin, one at (10, 0, 0)
621/// let ball1 = Ball::new(1.0);
622/// let ball2 = Ball::new(1.0);
623/// let pos12 = Pose::translation(10.0, 0.0, 0.0);
624///
625/// // Move ball1 toward ball2 along the +X axis
626/// let direction = Vector::new(1.0, 0.0, 0.0);
627///
628/// let mut simplex = VoronoiSimplex::new();
629/// if let Some((distance, normal, w1, w2)) = directional_distance(
630///     &pos12,
631///     &ball1,
632///     &ball2,
633///     direction,
634///     &mut simplex
635/// ) {
636///     println!("Ball1 can move {} units before contact", distance);
637///     println!("Contact normal: {:?}", normal);
638///     println!("Contact point on ball1: {:?}", w1);
639///     println!("Contact point on ball2: {:?}", w2);
640///     // Expected: distance ≈ 8.0 (10.0 - 1.0 - 1.0)
641/// }
642/// # }
643/// ```
644///
645/// # Use Cases
646///
647/// **1. Continuous Collision Detection:**
648/// ```ignore
649/// let movement_dir = velocity * time_step;
650/// if let Some((toi, normal, _, _)) = directional_distance(...) {
651///     if toi < 1.0 {
652///         // Collision will occur during this timestep
653///         let collision_time = toi * time_step;
654///     }
655/// }
656/// ```
657///
658/// **2. Safe Movement Distance:**
659/// ```ignore
660/// let desired_movement = Vector::new(5.0, 0.0, 0.0);
661/// if let Some((max_safe_dist, _, _, _)) = directional_distance(...) {
662///     let actual_movement = desired_movement.normalize() * max_safe_dist.min(5.0);
663/// }
664/// ```
665///
666/// # Notes
667///
668/// - All inputs and outputs are in the local-space of shape 1
669/// - If the shapes are already intersecting, the returned distance is 0.0 and witness
670///   points are undefined (set to origin)
671/// - The direction vector does not need to be normalized
672/// - This function internally uses GJK raycasting on the Minkowski difference
673pub fn directional_distance<G1, G2>(
674    pos12: &Pose,
675    g1: &G1,
676    g2: &G2,
677    dir: Vector,
678    simplex: &mut VoronoiSimplex,
679) -> Option<(Real, Vector, Vector, Vector)>
680where
681    G1: ?Sized + SupportMap,
682    G2: ?Sized + SupportMap,
683{
684    let ray = Ray::new(Vector::ZERO, dir);
685    minkowski_ray_cast(pos12, g1, g2, &ray, Real::max_value(), simplex).map(
686        |(time_of_impact, normal)| {
687            let witnesses = if !time_of_impact.is_zero() {
688                result(simplex, simplex.dimension() == DIM)
689            } else {
690                // If there is penetration, the witness points
691                // are undefined.
692                (Vector::ZERO, Vector::ZERO)
693            };
694
695            (time_of_impact, normal, witnesses.0, witnesses.1)
696        },
697    )
698}
699
700// Ray-cast on the Minkowski Difference `g1 - pos12 * g2`.
701fn minkowski_ray_cast<G1, G2>(
702    pos12: &Pose,
703    g1: &G1,
704    g2: &G2,
705    ray: &Ray,
706    max_time_of_impact: Real,
707    simplex: &mut VoronoiSimplex,
708) -> Option<(Real, Vector)>
709where
710    G1: ?Sized + SupportMap,
711    G2: ?Sized + SupportMap,
712{
713    let _eps = crate::math::DEFAULT_EPSILON;
714    let _eps_tol: Real = eps_tol();
715    let _eps_rel: Real = <Real as ComplexField>::sqrt(_eps_tol);
716
717    let ray_length = ray.dir.length();
718
719    if relative_eq!(ray_length, 0.0) {
720        return None;
721    }
722
723    let mut ltoi = 0.0;
724    let mut curr_ray = Ray::new(ray.origin, ray.dir / ray_length);
725    let dir = -curr_ray.dir;
726    let mut ldir = dir;
727
728    // Initialize the simplex.
729    let support_point = CsoPoint::from_shapes(pos12, g1, g2, dir);
730    simplex.reset(support_point.translate(-curr_ray.origin));
731
732    // TODO: reset the simplex if it is empty?
733    let mut proj = simplex.project_origin_and_reduce();
734    let mut max_bound = Real::max_value();
735    let mut dir;
736    let mut niter = 0;
737    let mut last_chance = false;
738
739    loop {
740        let old_max_bound = max_bound;
741
742        let neg_proj_coords = -proj;
743        let (normalized, dist) = neg_proj_coords.normalize_and_length();
744        if dist > _eps_tol {
745            dir = normalized;
746            max_bound = dist;
747        } else {
748            return Some((ltoi / ray_length, ldir));
749        }
750
751        let support_point = if max_bound >= old_max_bound {
752            // Upper bounds inconsistencies. Keep the projection as a valid support point
753            // for the last-chance path below.
754            last_chance = true;
755            CsoPoint::single_point(proj + curr_ray.origin)
756        } else {
757            CsoPoint::from_shapes(pos12, g1, g2, dir)
758        };
759
760        if last_chance && ltoi > 0.0 {
761            // The witnesses stay precise when large support coordinates stop the upper
762            // bound from decreasing, so refine the lower bound with their separation
763            // along the cast direction before accepting the impact.
764            let (witness1, witness2) = result(simplex, simplex.dimension() == DIM);
765            let witness_ltoi = (witness1 - witness2 - ray.origin).dot(curr_ray.dir);
766            if witness_ltoi.is_finite() && witness_ltoi > ltoi {
767                ltoi = witness_ltoi;
768
769                if ltoi / ray_length > max_time_of_impact {
770                    return None;
771                }
772            }
773
774            return Some((ltoi / ray_length, ldir));
775        }
776
777        // Clip the ray on the support halfspace (None <=> t < 0)
778        // The configurations are:
779        //   dir.dot(curr_ray.dir)  |   t   |               Action
780        // −−−−−−−−−−−−−−−−−−−−-----+−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
781        //          < 0             |  < 0  | Continue.
782        //          < 0             |  > 0  | New lower bound, move the origin.
783        //          > 0             |  < 0  | Miss. No intersection.
784        //          > 0             |  > 0  | New higher bound.
785        match query::details::ray_toi_with_halfspace(support_point.point, dir, &curr_ray) {
786            Some(t) => {
787                if dir.dot(curr_ray.dir) < 0.0 && t > 0.0 {
788                    // new lower bound
789                    ldir = dir;
790                    ltoi += t;
791
792                    // NOTE: we divide by ray_length instead of doing max_time_of_impact * ray_length
793                    // because the multiplication may cause an overflow if max_time_of_impact is set
794                    // to Real::max_value() by users that want to have an infinite ray.
795                    if ltoi / ray_length > max_time_of_impact {
796                        return None;
797                    }
798
799                    let shift = curr_ray.dir * t;
800                    curr_ray.origin += shift;
801                    max_bound = Real::max_value();
802                    simplex.modify_pnts(&|pt| pt.translate_mut(-shift));
803                    last_chance = false;
804                }
805            }
806            None => {
807                if dir.dot(curr_ray.dir) > _eps_tol {
808                    // miss
809                    return None;
810                }
811            }
812        }
813
814        if last_chance {
815            return None;
816        }
817
818        let min_bound = -dir.dot(support_point.point - curr_ray.origin);
819
820        assert!(min_bound.is_finite());
821
822        if max_bound - min_bound <= _eps_rel * max_bound {
823            // This is needed when using fixed-points to avoid missing
824            // some castes.
825            // TODO: I feel like we should always return `Some` in
826            // this case, even with floating-point numbers. Though it
827            // has not been sufficiently tested with floats yet to be sure.
828            if cfg!(feature = "improved_fixed_point_support") {
829                return Some((ltoi / ray_length, ldir));
830            } else {
831                return None;
832            }
833        }
834
835        let _ = simplex.add_point(support_point.translate(-curr_ray.origin));
836        proj = simplex.project_origin_and_reduce();
837
838        if simplex.dimension() == DIM {
839            // `min_bound` accumulates rounding errors proportional to the magnitude of the
840            // support coordinates, so we scale the tolerance accordingly.
841            let scale = support_point.point.length().max(curr_ray.origin.length());
842            if min_bound >= _eps_tol * scale.max(1.0) {
843                return None;
844            } else {
845                return Some((ltoi / ray_length, ldir)); // Vector inside of the cso.
846            }
847        }
848
849        niter += 1;
850        if niter == 100 {
851            return None;
852        }
853    }
854}
855
856// Deterministically perturbs the unit direction `dir` to escape degenerate support
857// directions that stall the GJK simplex without proving separation.
858// Successive seeds cycle through ±offsets along each coordinate axis.
859fn perturbed_dir(dir: Vector, seed: usize) -> Vector {
860    const OFFSET: Real = 1.0e-2;
861    let axis = (seed - 1) % DIM;
862    let sign = if ((seed - 1) / DIM).is_multiple_of(2) {
863        1.0
864    } else {
865        -1.0
866    };
867    let mut res = dir;
868    res[axis] += sign * OFFSET;
869    res.try_normalize().unwrap_or(dir)
870}
871
872fn result(simplex: &VoronoiSimplex, prev: bool) -> (Vector, Vector) {
873    let mut res = (Vector::ZERO, Vector::ZERO);
874    if prev {
875        for i in 0..simplex.prev_dimension() + 1 {
876            let coord = simplex.prev_proj_coord(i);
877            let point = simplex.prev_point(i);
878            res.0 += point.orig1 * coord;
879            res.1 += point.orig2 * coord;
880        }
881
882        res
883    } else {
884        for i in 0..simplex.dimension() + 1 {
885            let coord = simplex.proj_coord(i);
886            let point = simplex.point(i);
887            res.0 += point.orig1 * coord;
888            res.1 += point.orig2 * coord;
889        }
890
891        res
892    }
893}