Skip to main content

oxiphysics_collision/narrowphase/epa/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::super::gjk::{Simplex, SupportPoint};
6use crate::types::Contact;
7use oxiphysics_core::Transform;
8use oxiphysics_core::math::{Real, Vec3};
9use oxiphysics_geometry::Shape;
10
11use super::functions::{
12    MAX_ITERATIONS, TOLERANCE, add_edge, build_contact, build_initial_faces, compute_face_normal,
13    epa_add_edge, epa_dot3, epa_face_normal, epa_negate3, epa_scale3, epa_sub3, support_minkowski,
14};
15
16/// A face of the EPA polytope (raw array version).
17#[derive(Debug, Clone)]
18pub struct EpaFaceRaw {
19    /// Indices of the three vertices forming this face.
20    pub vertices: [usize; 3],
21    /// Outward-pointing normal (unit vector).
22    pub normal: [f64; 3],
23    /// Distance from the origin to this face along the normal.
24    pub distance: f64,
25}
26/// Stateful EPA solver providing minimum-penetration-axis and polytope-refinement
27/// operations as explicit, named entry points.
28///
29/// The underlying algorithm is the same expanding-polytope approach used by
30/// `epa_penetration`, but wrapped in a struct to allow step-by-step refinement
31/// and integration testing.
32pub struct EpaSolver {
33    /// Current polytope being refined.
34    pub(super) polytope: Option<EpaPolytope>,
35    /// Number of iterations performed so far.
36    pub(super) iterations: usize,
37    /// Last convergence tolerance used.
38    pub(super) tolerance: f64,
39}
40impl EpaSolver {
41    /// Create a new solver seeded from a GJK tetrahedron simplex.
42    pub fn from_simplex(simplex: &[[f64; 3]; 4]) -> Self {
43        Self {
44            polytope: Some(EpaPolytope::from_gjk_simplex(simplex)),
45            iterations: 0,
46            tolerance: 1e-6,
47        }
48    }
49    /// Override the convergence tolerance (default 1e-6).
50    pub fn with_tolerance(mut self, tol: f64) -> Self {
51        self.tolerance = tol;
52        self
53    }
54    /// Compute the *minimum penetration axis* – the contact normal and depth
55    /// for the current polytope state without expanding further.
56    ///
57    /// Returns `(normal, depth)` where `normal` is a unit vector pointing from
58    /// shape B to shape A and `depth` is the penetration depth.
59    ///
60    /// If the polytope is empty or unset, returns `([0,1,0], 0.0)`.
61    pub fn compute_penetration_axis(&self) -> ([f64; 3], f64) {
62        let poly = match &self.polytope {
63            Some(p) => p,
64            None => return ([0.0, 1.0, 0.0], 0.0),
65        };
66        if poly.faces.is_empty() {
67            return ([0.0, 1.0, 0.0], 0.0);
68        }
69        let (_idx, dist, normal) = poly.closest_face();
70        (normal, dist)
71    }
72    /// Perform a single polytope refinement step.
73    ///
74    /// `support_fn(dir)` should return the support point of the Minkowski
75    /// difference in direction `dir`.
76    ///
77    /// Returns `true` if the polytope was successfully expanded (i.e. the
78    /// algorithm has not yet converged), or `false` if it converged or failed.
79    pub fn refine_polytope(&mut self, support_fn: &mut dyn FnMut([f64; 3]) -> [f64; 3]) -> bool {
80        let poly = match self.polytope.as_mut() {
81            Some(p) => p,
82            None => return false,
83        };
84        if poly.faces.is_empty() {
85            return false;
86        }
87        self.iterations += 1;
88        let closest_idx = poly.find_closest_face();
89        let closest = poly.faces[closest_idx].clone();
90        let new_point = support_fn(closest.normal);
91        let new_dist = epa_dot3(new_point, closest.normal);
92        if (new_dist - closest.distance).abs() < self.tolerance {
93            return false;
94        }
95        poly.expand(new_point)
96    }
97    /// Run the solver to completion, performing at most `max_iter` refinement
98    /// steps and returning the final penetration result.
99    ///
100    /// Equivalent to calling `refine_polytope` in a loop and then
101    /// `compute_penetration_axis`.
102    pub fn solve(
103        &mut self,
104        support_fn: &mut dyn FnMut([f64; 3]) -> [f64; 3],
105        max_iter: usize,
106    ) -> Option<EpaPenetration> {
107        for _ in 0..max_iter {
108            if !self.refine_polytope(support_fn) {
109                break;
110            }
111        }
112        let (normal, depth) = self.compute_penetration_axis();
113        if depth <= 0.0 && normal == [0.0, 1.0, 0.0] {
114            return None;
115        }
116        let poly = self.polytope.as_ref()?;
117        if poly.faces.is_empty() {
118            return None;
119        }
120        let closest_idx = poly.find_closest_face();
121        let closest = &poly.faces[closest_idx];
122        let witness_a = poly.vertices[closest.vertices[0]];
123        let witness_b = epa_sub3(witness_a, epa_scale3(normal, depth));
124        Some(EpaPenetration {
125            normal,
126            depth,
127            witness_a,
128            witness_b,
129        })
130    }
131    /// Number of refinement steps performed so far.
132    pub fn iterations(&self) -> usize {
133        self.iterations
134    }
135    /// Current number of faces in the polytope.
136    pub fn face_count(&self) -> usize {
137        self.polytope.as_ref().map(|p| p.faces.len()).unwrap_or(0)
138    }
139    /// Current number of vertices in the polytope.
140    pub fn vertex_count(&self) -> usize {
141        self.polytope
142            .as_ref()
143            .map(|p| p.vertices.len())
144            .unwrap_or(0)
145    }
146}
147/// EPA polytope storing vertices and faces.
148#[derive(Debug, Clone)]
149pub struct EpaPolytope {
150    /// Vertices of the polytope (Minkowski-difference points).
151    pub vertices: Vec<[f64; 3]>,
152    /// Faces of the polytope.
153    pub faces: Vec<EpaFaceRaw>,
154}
155impl EpaPolytope {
156    /// Initialize the polytope from a GJK tetrahedron (4 vertices).
157    pub fn from_gjk_simplex(simplex: &[[f64; 3]; 4]) -> Self {
158        let vertices = simplex.to_vec();
159        let face_indices: [[usize; 3]; 4] = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
160        let mut faces = Vec::new();
161        for idx in &face_indices {
162            let a = vertices[idx[0]];
163            let b = vertices[idx[1]];
164            let c = vertices[idx[2]];
165            let normal = epa_face_normal(a, b, c);
166            let mut distance = epa_dot3(normal, a);
167            let mut n = normal;
168            if distance < 0.0 {
169                n = epa_negate3(n);
170                distance = -distance;
171            }
172            faces.push(EpaFaceRaw {
173                vertices: *idx,
174                normal: n,
175                distance,
176            });
177        }
178        Self { vertices, faces }
179    }
180    /// Find the index of the closest face to the origin.
181    pub fn find_closest_face(&self) -> usize {
182        self.faces
183            .iter()
184            .enumerate()
185            .min_by(|(_, a), (_, b)| {
186                a.distance
187                    .partial_cmp(&b.distance)
188                    .unwrap_or(std::cmp::Ordering::Equal)
189            })
190            .map(|(i, _)| i)
191            .unwrap_or(0)
192    }
193    /// Return `(face_index, distance, normal)` for the face closest to the origin.
194    pub fn closest_face(&self) -> (usize, f64, [f64; 3]) {
195        let idx = self.find_closest_face();
196        if self.faces.is_empty() {
197            return (0, 0.0, [0.0, 1.0, 0.0]);
198        }
199        let f = &self.faces[idx];
200        (idx, f.distance, f.normal)
201    }
202    /// Expand the polytope toward a new support point.
203    ///
204    /// Returns `true` if the polytope was successfully expanded, `false` if the
205    /// support point did not make progress.
206    pub fn expand(&mut self, support_point: [f64; 3]) -> bool {
207        let new_idx = self.vertices.len();
208        let mut edges: Vec<(usize, usize)> = Vec::new();
209        let mut i = 0;
210        let mut any_removed = false;
211        while i < self.faces.len() {
212            let face = &self.faces[i];
213            let v = self.vertices[face.vertices[0]];
214            let to_point = epa_sub3(support_point, v);
215            if epa_dot3(face.normal, to_point) > 1e-10 {
216                let [a, b, c] = face.vertices;
217                epa_add_edge(&mut edges, a, b);
218                epa_add_edge(&mut edges, b, c);
219                epa_add_edge(&mut edges, c, a);
220                self.faces.swap_remove(i);
221                any_removed = true;
222            } else {
223                i += 1;
224            }
225        }
226        if !any_removed || edges.is_empty() {
227            return false;
228        }
229        self.vertices.push(support_point);
230        for &(ea, eb) in &edges {
231            let a = self.vertices[ea];
232            let b = self.vertices[eb];
233            let c = support_point;
234            let normal = epa_face_normal(a, b, c);
235            let mut distance = epa_dot3(normal, a);
236            let mut n = normal;
237            if distance < 0.0 {
238                n = epa_negate3(n);
239                distance = -distance;
240            }
241            self.faces.push(EpaFaceRaw {
242                vertices: [ea, eb, new_idx],
243                normal: n,
244                distance,
245            });
246        }
247        true
248    }
249}
250/// Configuration for EPA termination.
251#[derive(Debug, Clone)]
252pub struct EpaConfig {
253    /// Maximum number of expansion iterations.
254    pub max_iter: usize,
255    /// Convergence tolerance (distance improvement threshold).
256    pub tolerance: f64,
257    /// Maximum number of faces allowed in the polytope.
258    pub max_faces: usize,
259}
260impl Default for EpaConfig {
261    /// Default EPA configuration.
262    fn default() -> Self {
263        Self {
264            max_iter: 64,
265            tolerance: 1e-6,
266            max_faces: 256,
267        }
268    }
269}
270impl EpaConfig {
271    /// Create a high-precision configuration.
272    pub fn high_precision() -> Self {
273        Self {
274            max_iter: 128,
275            tolerance: 1e-9,
276            max_faces: 512,
277        }
278    }
279}
280/// Summary statistics about an EPA polytope's geometry.
281#[derive(Debug, Clone, Default)]
282pub struct EpaPolytopeStats {
283    /// Number of faces.
284    pub face_count: usize,
285    /// Number of vertices.
286    pub vertex_count: usize,
287    /// Minimum face distance from origin.
288    pub min_distance: f64,
289    /// Maximum face distance from origin.
290    pub max_distance: f64,
291    /// Average face distance from origin.
292    pub avg_distance: f64,
293    /// Minimum face area.
294    pub min_area: f64,
295    /// Maximum face area.
296    pub max_area: f64,
297}
298/// Witness point data: contact positions on each shape.
299#[derive(Debug, Clone)]
300pub struct EpaWitness {
301    /// Contact point on shape A.
302    pub point_a: [f64; 3],
303    /// Contact point on shape B.
304    pub point_b: [f64; 3],
305    /// Contact normal (unit vector, from B to A).
306    pub normal: [f64; 3],
307    /// Penetration depth.
308    pub depth: f64,
309}
310/// EPA algorithm for computing penetration depth and contact normal.
311pub struct Epa;
312impl Epa {
313    /// Compute penetration depth and contact information from a GJK simplex
314    /// that has been determined to contain the origin.
315    pub fn penetration_depth(
316        shape_a: &dyn Shape,
317        transform_a: &Transform,
318        shape_b: &dyn Shape,
319        transform_b: &Transform,
320        simplex: &Simplex,
321    ) -> Option<Contact> {
322        if simplex.len() < 4 {
323            return Self::fallback_contact(simplex);
324        }
325        let mut vertices: Vec<SupportPoint> = simplex.points.clone();
326        let mut faces = build_initial_faces(&vertices);
327        for _ in 0..MAX_ITERATIONS {
328            let closest_idx = match faces.iter().enumerate().min_by(|(_, a), (_, b)| {
329                a.distance
330                    .partial_cmp(&b.distance)
331                    .unwrap_or(std::cmp::Ordering::Equal)
332            }) {
333                Some((idx, _)) => idx,
334                None => return None,
335            };
336            let closest = faces[closest_idx].clone();
337            let search_dir = closest.normal;
338            let new_point =
339                support_minkowski(shape_a, transform_a, shape_b, transform_b, &search_dir);
340            let new_dist = new_point.point.dot(&search_dir);
341            if (new_dist - closest.distance).abs() < TOLERANCE {
342                return Some(build_contact(&closest, &vertices));
343            }
344            let new_idx = vertices.len();
345            vertices.push(new_point);
346            let mut edges: Vec<(usize, usize)> = Vec::new();
347            let mut i = 0;
348            while i < faces.len() {
349                let face = &faces[i];
350                let v = vertices[face.indices[0]].point;
351                if face.normal.dot(&(new_point.point - v)) > 0.0 {
352                    let [a, b, c] = face.indices;
353                    add_edge(&mut edges, a, b);
354                    add_edge(&mut edges, b, c);
355                    add_edge(&mut edges, c, a);
356                    faces.swap_remove(i);
357                } else {
358                    i += 1;
359                }
360            }
361            for &(ea, eb) in &edges {
362                let normal = compute_face_normal(
363                    &vertices[ea].point,
364                    &vertices[eb].point,
365                    &vertices[new_idx].point,
366                );
367                let distance = normal.dot(&vertices[ea].point);
368                let (normal, distance) = if distance < 0.0 {
369                    (-normal, -distance)
370                } else {
371                    (normal, distance)
372                };
373                faces.push(Face {
374                    indices: [ea, eb, new_idx],
375                    normal,
376                    distance,
377                });
378            }
379        }
380        faces
381            .iter()
382            .min_by(|a, b| {
383                a.distance
384                    .partial_cmp(&b.distance)
385                    .unwrap_or(std::cmp::Ordering::Equal)
386            })
387            .map(|face| build_contact(face, &vertices))
388    }
389    fn fallback_contact(simplex: &Simplex) -> Option<Contact> {
390        if simplex.is_empty() {
391            return None;
392        }
393        let (closest, _) = simplex.closest_point_to_origin();
394        let depth = closest.norm();
395        let normal = if depth > 1e-10 {
396            -closest.normalize()
397        } else {
398            Vec3::new(0.0, 1.0, 0.0)
399        };
400        let point_a: Vec3 =
401            simplex.points.iter().map(|p| p.support_a).sum::<Vec3>() / simplex.len() as Real;
402        let point_b: Vec3 =
403            simplex.points.iter().map(|p| p.support_b).sum::<Vec3>() / simplex.len() as Real;
404        Some(Contact::new(point_a, point_b, normal, depth))
405    }
406}
407/// Statistics collected during an EPA run.
408#[derive(Debug, Clone, Default)]
409pub struct EpaStats {
410    /// Number of expansion iterations performed.
411    pub iterations: usize,
412    /// Final number of faces in the polytope.
413    pub face_count: usize,
414    /// Final penetration depth.
415    pub final_depth: f64,
416    /// Whether the algorithm converged within the tolerance.
417    pub converged: bool,
418}
419/// A triangle face of the EPA polytope.
420#[derive(Debug, Clone)]
421pub struct Face {
422    /// Indices of the three vertices forming this triangle face.
423    pub indices: [usize; 3],
424    /// Outward-pointing unit normal of the face.
425    pub normal: Vec3,
426    /// Signed distance from the origin to the face plane along the normal.
427    pub distance: Real,
428}
429/// A priority-queue-like structure that tracks the EPA face with minimum distance.
430///
431/// In practice, EPA typically scans all faces each iteration (O(n)), but this
432/// wrapper exposes a clean interface to find the best face.
433pub struct EpaFaceQueue<'a> {
434    pub(super) polytope: &'a EpaPolytope,
435}
436impl<'a> EpaFaceQueue<'a> {
437    /// Create a new face queue backed by the given polytope.
438    pub fn new(polytope: &'a EpaPolytope) -> Self {
439        Self { polytope }
440    }
441    /// Return the index of the face with minimum distance from origin.
442    pub fn pop_min(&self) -> Option<usize> {
443        if self.polytope.faces.is_empty() {
444            return None;
445        }
446        Some(self.polytope.find_closest_face())
447    }
448    /// Return `(index, distance, normal)` for the closest face.
449    pub fn peek_min(&self) -> Option<(usize, f64, [f64; 3])> {
450        let idx = self.pop_min()?;
451        let f = &self.polytope.faces[idx];
452        Some((idx, f.distance, f.normal))
453    }
454    /// Number of faces in the backing polytope.
455    pub fn len(&self) -> usize {
456        self.polytope.faces.len()
457    }
458    /// Whether the backing polytope has no faces.
459    pub fn is_empty(&self) -> bool {
460        self.polytope.faces.is_empty()
461    }
462}
463/// Penetration result from the EPA algorithm.
464#[derive(Debug, Clone)]
465pub struct EpaPenetration {
466    /// Penetration normal (unit vector, points from B to A).
467    pub normal: [f64; 3],
468    /// Penetration depth.
469    pub depth: f64,
470    /// Witness point on shape A.
471    pub witness_a: [f64; 3],
472    /// Witness point on shape B.
473    pub witness_b: [f64; 3],
474}