Skip to main content

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