parry2d/query/epa/
epa2.rs

1//! Two-dimensional penetration depth queries using the Expanding Polytope Algorithm.
2//!
3//! This module provides the 2D-specific implementation of EPA, which works with
4//! polygons (edges) rather than polyhedra (faces) as in the 3D version.
5
6use alloc::{collections::BinaryHeap, vec::Vec};
7use core::cmp::Ordering;
8use num::Bounded;
9
10use crate::math::{Pose, Real, Vector};
11use crate::query::gjk::{self, ConstantOrigin, CsoPoint, VoronoiSimplex};
12use crate::shape::SupportMap;
13use crate::utils;
14
15#[derive(Copy, Clone, PartialEq)]
16struct FaceId {
17    id: usize,
18    neg_dist: Real,
19}
20
21impl FaceId {
22    fn new(id: usize, neg_dist: Real) -> Option<Self> {
23        if neg_dist > gjk::eps_tol() {
24            None
25        } else {
26            Some(FaceId { id, neg_dist })
27        }
28    }
29}
30
31impl Eq for FaceId {}
32
33impl PartialOrd for FaceId {
34    #[inline]
35    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
36        Some(self.cmp(other))
37    }
38}
39
40impl Ord for FaceId {
41    #[inline]
42    fn cmp(&self, other: &Self) -> Ordering {
43        if self.neg_dist < other.neg_dist {
44            Ordering::Less
45        } else if self.neg_dist > other.neg_dist {
46            Ordering::Greater
47        } else {
48            Ordering::Equal
49        }
50    }
51}
52
53#[derive(Clone, Debug)]
54struct Face {
55    pts: [usize; 2],
56    normal: Vector,
57    proj: Vector,
58    bcoords: [Real; 2],
59    deleted: bool,
60}
61
62impl Face {
63    pub fn new(vertices: &[CsoPoint], pts: [usize; 2]) -> (Self, bool) {
64        if let Some((proj, bcoords)) =
65            project_origin(vertices[pts[0]].point, vertices[pts[1]].point)
66        {
67            (Self::new_with_proj(vertices, proj, bcoords, pts), true)
68        } else {
69            (
70                Self::new_with_proj(vertices, Vector::ZERO, [0.0; 2], pts),
71                false,
72            )
73        }
74    }
75
76    pub fn new_with_proj(
77        vertices: &[CsoPoint],
78        proj: Vector,
79        bcoords: [Real; 2],
80        pts: [usize; 2],
81    ) -> Self {
82        let normal;
83        let deleted;
84
85        if let Some(n) = utils::ccw_face_normal([vertices[pts[0]].point, vertices[pts[1]].point]) {
86            normal = n;
87            deleted = false;
88        } else {
89            normal = Vector::ZERO;
90            deleted = true;
91        }
92
93        Face {
94            pts,
95            normal,
96            proj,
97            bcoords,
98            deleted,
99        }
100    }
101
102    pub fn closest_points(&self, vertices: &[CsoPoint]) -> (Vector, Vector) {
103        (
104            vertices[self.pts[0]].orig1 * self.bcoords[0]
105                + vertices[self.pts[1]].orig1 * self.bcoords[1],
106            vertices[self.pts[0]].orig2 * self.bcoords[0]
107                + vertices[self.pts[1]].orig2 * self.bcoords[1],
108        )
109    }
110}
111
112/// The Expanding Polytope Algorithm in 2D.
113///
114/// This structure computes the penetration depth between two shapes when they are overlapping.
115/// It's used after GJK (Gilbert-Johnson-Keerthi) determines that two shapes are penetrating.
116///
117/// # What does EPA do?
118///
119/// EPA finds:
120/// - The **penetration depth**: How far the shapes are overlapping
121/// - The **contact normal**: The direction to separate the shapes
122/// - The **contact points**: Where the shapes are touching on each surface
123///
124/// # How it works in 2D
125///
126/// In 2D, EPA maintains a polygon in the Minkowski difference space (CSO - Configuration Space
127/// Obstacle) that encloses the origin. It iteratively:
128///
129/// 1. Finds the edge closest to the origin
130/// 2. Expands the polygon by adding a new support point in the direction of that edge's normal
131/// 3. Updates the polygon structure with the new point
132/// 4. Repeats until the polygon cannot expand further (convergence)
133///
134/// The final closest edge provides the penetration depth (distance to origin) and contact normal.
135///
136/// # Example
137///
138/// ```
139/// # #[cfg(all(feature = "dim2", feature = "f32"))] {
140/// use parry2d::query::epa::EPA;
141/// use parry2d::query::gjk::VoronoiSimplex;
142/// use parry2d::shape::Ball;
143/// use parry2d::math::Pose;
144///
145/// let ball1 = Ball::new(1.0);
146/// let ball2 = Ball::new(1.0);
147/// let pos12 = Pose::translation(1.5, 0.0); // Overlapping circles
148///
149/// // After GJK determines penetration and fills a simplex:
150/// let mut epa = EPA::new();
151/// let simplex = VoronoiSimplex::new(); // Would be filled by GJK
152///
153/// // EPA computes the contact details
154/// // if let Some((pt1, pt2, normal)) = epa.closest_points(&pos12, &ball1, &ball2, &simplex) {
155/// //     println!("Penetration depth: {}", (pt2 - pt1).length());
156/// //     println!("Contact normal: {}", normal);
157/// // }
158/// # }
159/// ```
160///
161/// # Reusability
162///
163/// The `EPA` structure can be reused across multiple queries to avoid allocations.
164/// Internal buffers are cleared and reused in each call to [`closest_points`](EPA::closest_points).
165///
166/// # Convergence and Failure Cases
167///
168/// EPA may return `None` in these situations:
169/// - The shapes are not actually penetrating (GJK should be used instead)
170/// - Degenerate or nearly-degenerate geometry causes numerical instability
171/// - The initial simplex from GJK is invalid
172/// - The algorithm fails to converge after 100 iterations
173///
174/// When `None` is returned, the shapes may be touching at a single point or edge, or there
175/// may be numerical precision issues with the input geometry.
176#[derive(Default)]
177pub struct EPA {
178    vertices: Vec<CsoPoint>,
179    faces: Vec<Face>,
180    heap: BinaryHeap<FaceId>,
181}
182
183impl EPA {
184    /// Creates a new instance of the 2D Expanding Polytope Algorithm.
185    ///
186    /// This allocates internal data structures (vertices, faces, and a priority heap).
187    /// The same `EPA` instance can be reused for multiple queries to avoid repeated allocations.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
193    /// use parry2d::query::epa::EPA;
194    ///
195    /// let mut epa = EPA::new();
196    /// // Use epa for multiple queries...
197    /// # }
198    /// ```
199    pub fn new() -> Self {
200        EPA::default()
201    }
202
203    fn reset(&mut self) {
204        self.vertices.clear();
205        self.faces.clear();
206        self.heap.clear();
207    }
208
209    /// Projects the origin onto the boundary of the given shape.
210    ///
211    /// This is a specialized version of [`closest_points`](EPA::closest_points) for projecting
212    /// a point (the origin) onto a single shape's surface.
213    ///
214    /// # Parameters
215    ///
216    /// - `m`: The position and orientation of the shape in world space
217    /// - `g`: The shape to project onto (must implement [`SupportMap`])
218    /// - `simplex`: A Voronoi simplex from GJK that encloses the origin (indicating the origin
219    ///   is inside the shape)
220    ///
221    /// # Returns
222    ///
223    /// - `Some(point)`: The closest point on the shape's boundary to the origin, in the local
224    ///   space of `g`
225    /// - `None`: If the origin is not inside the shape, or if EPA failed to converge
226    ///
227    /// # Prerequisites
228    ///
229    /// The origin **must be inside** the shape. If it's outside, use GJK instead.
230    /// Typically, you would:
231    /// 1. Run GJK to detect if a point is inside a shape
232    /// 2. If inside, use this method to find the closest boundary point
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
238    /// use parry2d::query::epa::EPA;
239    /// use parry2d::query::gjk::VoronoiSimplex;
240    /// use parry2d::shape::Ball;
241    /// use parry2d::math::Pose;
242    ///
243    /// let ball = Ball::new(2.0);
244    /// let pos = Pose::identity();
245    ///
246    /// // Assume GJK determined the origin is inside and filled simplex
247    /// let simplex = VoronoiSimplex::new();
248    /// let mut epa = EPA::new();
249    ///
250    /// // Find the closest point on the ball's surface to the origin
251    /// // if let Some(surface_point) = epa.project_origin(&pos, &ball, &simplex) {
252    /// //     println!("Closest surface point: {:?}", surface_point);
253    /// // }
254    /// # }
255    /// ```
256    pub fn project_origin<G: ?Sized + SupportMap>(
257        &mut self,
258        m: &Pose,
259        g: &G,
260        simplex: &VoronoiSimplex,
261    ) -> Option<Vector> {
262        self.closest_points(&m.inverse(), g, &ConstantOrigin, simplex)
263            .map(|(p, _, _)| p)
264    }
265
266    /// Computes the closest points between two penetrating shapes and their contact normal.
267    ///
268    /// This is the main EPA method that computes detailed contact information for overlapping shapes.
269    /// It should be called after GJK determines that two shapes are penetrating.
270    ///
271    /// # Parameters
272    ///
273    /// - `pos12`: The relative position/orientation from `g2`'s frame to `g1`'s frame
274    ///   (typically computed as `pos1.inverse() * pos2`)
275    /// - `g1`: The first shape (must implement [`SupportMap`])
276    /// - `g2`: The second shape (must implement [`SupportMap`])
277    /// - `simplex`: A Voronoi simplex from GJK that encloses the origin, indicating penetration
278    ///
279    /// # Returns
280    ///
281    /// Returns `Some((point1, point2, normal))` where:
282    /// - `point1`: Contact point on shape `g1` in `g1`'s local frame
283    /// - `point2`: Contact point on shape `g2` in `g2`'s local frame
284    /// - `normal`: Contact normal pointing from `g2` toward `g1`, normalized
285    ///
286    /// The **penetration depth** can be computed as `(point1 - point2).length()` after transforming
287    /// both points to the same coordinate frame.
288    ///
289    /// Returns `None` if:
290    /// - The shapes are not actually penetrating
291    /// - EPA fails to converge (degenerate geometry, numerical issues)
292    /// - The simplex is invalid or empty
293    /// - The algorithm reaches the maximum iteration limit (100 iterations)
294    ///
295    /// # Prerequisites
296    ///
297    /// The shapes **must be penetrating**. The typical workflow is:
298    /// 1. Run GJK to check if shapes intersect
299    /// 2. If GJK detects penetration and returns a simplex enclosing the origin
300    /// 3. Use EPA with that simplex to compute detailed contact information
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
306    /// use parry2d::query::epa::EPA;
307    /// use parry2d::query::gjk::{GJKResult, VoronoiSimplex};
308    /// use parry2d::shape::Ball;
309    /// use parry2d::math::Pose;
310    ///
311    /// let ball1 = Ball::new(1.0);
312    /// let ball2 = Ball::new(1.0);
313    ///
314    /// let pos1 = Pose::identity();
315    /// let pos2 = Pose::translation(1.5, 0.0); // Overlapping
316    /// let pos12 = pos1.inverse() * pos2;
317    ///
318    /// // After GJK detects penetration:
319    /// let mut simplex = VoronoiSimplex::new();
320    /// // ... simplex would be filled by GJK ...
321    ///
322    /// let mut epa = EPA::new();
323    /// // if let Some((pt1, pt2, normal)) = epa.closest_points(&pos12, &ball1, &ball2, &simplex) {
324    /// //     println!("Contact on shape 1: {:?}", pt1);
325    /// //     println!("Contact on shape 2: {:?}", pt2);
326    /// //     println!("Contact normal: {}", normal);
327    /// //     println!("Penetration depth: ~0.5");
328    /// // }
329    /// # }
330    /// ```
331    ///
332    /// # Technical Details
333    ///
334    /// The algorithm works in the **Minkowski difference space** (also called the Configuration
335    /// Space Obstacle or CSO), where the difference between support points of the two shapes
336    /// forms a new shape. When shapes penetrate, this CSO contains the origin.
337    ///
338    /// EPA iteratively expands a polygon (in 2D) that surrounds the origin, finding the edge
339    /// closest to the origin at each iteration. This closest edge defines the penetration
340    /// depth and contact normal.
341    pub fn closest_points<G1, G2>(
342        &mut self,
343        pos12: &Pose,
344        g1: &G1,
345        g2: &G2,
346        simplex: &VoronoiSimplex,
347    ) -> Option<(Vector, Vector, Vector)>
348    where
349        G1: ?Sized + SupportMap,
350        G2: ?Sized + SupportMap,
351    {
352        let _eps: Real = crate::math::DEFAULT_EPSILON;
353        let _eps_tol = _eps * 100.0;
354
355        self.reset();
356
357        /*
358         * Initialization.
359         */
360        for i in 0..simplex.dimension() + 1 {
361            self.vertices.push(*simplex.point(i));
362        }
363
364        if simplex.dimension() == 0 {
365            const MAX_ITERS: usize = 100; // If there is no convergence, just use whatever direction was extracted so fare
366
367            // The contact is vertex-vertex.
368            // We need to determine a valid normal that lies
369            // on both vertices' normal cone.
370            let mut n = Vector::Y;
371
372            // First, find a vector on the first vertex tangent cone.
373            let orig1 = self.vertices[0].orig1;
374            for _ in 0..MAX_ITERS {
375                let supp1 = g1.local_support_point(n);
376                if let Some(tangent) = (supp1 - orig1).try_normalize() {
377                    if n.dot(tangent) < _eps_tol {
378                        break;
379                    }
380
381                    n = Vector::new(-tangent.y, tangent.x);
382                } else {
383                    break;
384                }
385            }
386
387            // Second, ensure the direction lies on the second vertex's tangent cone.
388            let orig2 = self.vertices[0].orig2;
389            for _ in 0..MAX_ITERS {
390                let supp2 = g2.support_point(pos12, -n);
391                if let Some(tangent) = (supp2 - orig2).try_normalize() {
392                    if (-n).dot(tangent) < _eps_tol {
393                        break;
394                    }
395
396                    n = Vector::new(-tangent.y, tangent.x);
397                } else {
398                    break;
399                }
400            }
401
402            return Some((Vector::ZERO, Vector::ZERO, n));
403        } else if simplex.dimension() == 2 {
404            let dp1 = self.vertices[1] - self.vertices[0];
405            let dp2 = self.vertices[2] - self.vertices[0];
406
407            if dp1.perp_dot(dp2) < 0.0 {
408                self.vertices.swap(1, 2)
409            }
410
411            let pts1 = [0, 1];
412            let pts2 = [1, 2];
413            let pts3 = [2, 0];
414
415            let (face1, proj_inside1) = Face::new(&self.vertices, pts1);
416            let (face2, proj_inside2) = Face::new(&self.vertices, pts2);
417            let (face3, proj_inside3) = Face::new(&self.vertices, pts3);
418
419            self.faces.push(face1);
420            self.faces.push(face2);
421            self.faces.push(face3);
422
423            if proj_inside1 {
424                let dist1 = self.faces[0].normal.dot(self.vertices[0].point);
425                self.heap.push(FaceId::new(0, -dist1)?);
426            }
427
428            if proj_inside2 {
429                let dist2 = self.faces[1].normal.dot(self.vertices[1].point);
430                self.heap.push(FaceId::new(1, -dist2)?);
431            }
432
433            if proj_inside3 {
434                let dist3 = self.faces[2].normal.dot(self.vertices[2].point);
435                self.heap.push(FaceId::new(2, -dist3)?);
436            }
437
438            if !(proj_inside1 || proj_inside2 || proj_inside3) {
439                // Related issues:
440                // https://github.com/dimforge/parry/issues/253
441                // https://github.com/dimforge/parry/issues/246
442                log::debug!("Hit unexpected state in EPA: failed to project the origin on the initial simplex.");
443                return None;
444            }
445        } else {
446            let pts1 = [0, 1];
447            let pts2 = [1, 0];
448
449            self.faces.push(Face::new_with_proj(
450                &self.vertices,
451                Vector::ZERO,
452                [1.0, 0.0],
453                pts1,
454            ));
455            self.faces.push(Face::new_with_proj(
456                &self.vertices,
457                Vector::ZERO,
458                [1.0, 0.0],
459                pts2,
460            ));
461
462            let dist1 = self.faces[0].normal.dot(self.vertices[0].point);
463            let dist2 = self.faces[1].normal.dot(self.vertices[1].point);
464
465            self.heap.push(FaceId::new(0, dist1)?);
466            self.heap.push(FaceId::new(1, dist2)?);
467        }
468
469        let mut niter = 0;
470        let mut max_dist = Real::max_value();
471        let mut best_face_id = *self.heap.peek().unwrap();
472        let mut old_dist = 0.0;
473
474        /*
475         * Run the expansion.
476         */
477        while let Some(face_id) = self.heap.pop() {
478            // Create new faces.
479            let face = self.faces[face_id.id].clone();
480
481            if face.deleted {
482                continue;
483            }
484
485            let cso_point = CsoPoint::from_shapes(pos12, g1, g2, face.normal);
486            let support_point_id = self.vertices.len();
487            self.vertices.push(cso_point);
488
489            let candidate_max_dist = cso_point.point.dot(face.normal);
490
491            if candidate_max_dist < max_dist {
492                best_face_id = face_id;
493                max_dist = candidate_max_dist;
494            }
495
496            let curr_dist = -face_id.neg_dist;
497
498            if max_dist - curr_dist < _eps_tol ||
499                // Accept the intersection as the algorithm is stuck and no new points will be found
500                // This happens because of numerical stability issue
501                ((curr_dist - old_dist).abs() < _eps && candidate_max_dist < max_dist)
502            {
503                let best_face = &self.faces[best_face_id.id];
504                let cpts = best_face.closest_points(&self.vertices);
505                return Some((cpts.0, cpts.1, best_face.normal));
506            }
507
508            old_dist = curr_dist;
509
510            let pts1 = [face.pts[0], support_point_id];
511            let pts2 = [support_point_id, face.pts[1]];
512
513            let new_faces = [
514                Face::new(&self.vertices, pts1),
515                Face::new(&self.vertices, pts2),
516            ];
517
518            for f in new_faces.iter() {
519                if f.1 {
520                    let dist = f.0.normal.dot(f.0.proj);
521                    if dist < curr_dist {
522                        // TODO: if we reach this point, there were issues due to
523                        // numerical errors.
524                        let cpts = f.0.closest_points(&self.vertices);
525                        return Some((cpts.0, cpts.1, f.0.normal));
526                    }
527
528                    if !f.0.deleted {
529                        self.heap.push(FaceId::new(self.faces.len(), -dist)?);
530                    }
531                }
532
533                self.faces.push(f.0.clone());
534            }
535
536            niter += 1;
537            if niter > 100 {
538                // if we reached this point, our algorithm didn't converge to what precision we wanted.
539                // still return an intersection point, as it's probably close enough.
540                break;
541            }
542        }
543
544        let best_face = &self.faces[best_face_id.id];
545        let cpts = best_face.closest_points(&self.vertices);
546        Some((cpts.0, cpts.1, best_face.normal))
547    }
548}
549
550fn project_origin(a: Vector, b: Vector) -> Option<(Vector, [Real; 2])> {
551    let ab = b - a;
552    let ap = -a;
553    let ab_ap = ab.dot(ap);
554    let sqnab = ab.length_squared();
555
556    if sqnab == 0.0 {
557        return None;
558    }
559
560    let position_on_segment;
561
562    let _eps: Real = gjk::eps_tol();
563
564    if ab_ap < -_eps || ab_ap > sqnab + _eps {
565        // Voronoï region of vertex 'a' or 'b'.
566        None
567    } else {
568        // Voronoï region of the segment interior.
569        position_on_segment = ab_ap / sqnab;
570
571        let res = a + ab * position_on_segment;
572
573        Some((res, [1.0 - position_on_segment, position_on_segment]))
574    }
575}