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