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}