parry3d_f64/query/epa/epa3.rs
1//! Three-dimensional penetration depth queries using the Expanding Polytope Algorithm.
2//!
3//! This module provides the 3D-specific implementation of EPA, which works with
4//! polyhedra (triangular faces) rather than polygons (edges) as in the 2D version.
5
6use crate::math::{Pose, Real, Vector};
7use crate::query::gjk::{self, ConstantOrigin, CsoPoint, VoronoiSimplex};
8use crate::query::PointQueryWithLocation;
9use crate::shape::{SupportMap, Triangle, TrianglePointLocation};
10use crate::utils;
11use alloc::collections::BinaryHeap;
12use alloc::vec::Vec;
13use core::cmp::Ordering;
14use num::Bounded;
15
16#[derive(Copy, Clone, PartialEq)]
17struct FaceId {
18 id: usize,
19 neg_dist: Real,
20}
21
22impl FaceId {
23 fn new(id: usize, neg_dist: Real) -> Option<Self> {
24 if neg_dist > gjk::eps_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; 3],
57 adj: [usize; 3],
58 normal: Vector,
59 bcoords: [Real; 3],
60 deleted: bool,
61}
62
63impl Face {
64 pub fn new_with_proj(
65 vertices: &[CsoPoint],
66 bcoords: [Real; 3],
67 pts: [usize; 3],
68 adj: [usize; 3],
69 ) -> Self {
70 let normal;
71
72 if let Some(n) = utils::ccw_face_normal([
73 vertices[pts[0]].point,
74 vertices[pts[1]].point,
75 vertices[pts[2]].point,
76 ]) {
77 normal = n;
78 } else {
79 // This is a bit of a hack for degenerate faces.
80 // TODO: It will work OK with our current code, though
81 // we should do this in another way to avoid any risk
82 // of misusing the face normal in the future.
83 normal = Vector::ZERO;
84 }
85
86 Face {
87 pts,
88 bcoords,
89 adj,
90 normal,
91 deleted: false,
92 }
93 }
94
95 pub fn new(vertices: &[CsoPoint], pts: [usize; 3], adj: [usize; 3]) -> (Self, bool) {
96 let tri = Triangle::new(
97 vertices[pts[0]].point,
98 vertices[pts[1]].point,
99 vertices[pts[2]].point,
100 );
101 let (proj, loc) = tri.project_local_point_and_get_location(Vector::ZERO, true);
102
103 match loc {
104 TrianglePointLocation::OnVertex(_) | TrianglePointLocation::OnEdge(_, _) => {
105 let eps_tol = crate::math::DEFAULT_EPSILON * 100.0; // Same as in closest_points
106 (
107 // barycentric_coordinates is guaranteed to work in OnVertex and OnEdge locations
108 Self::new_with_proj(vertices, loc.barycentric_coordinates().unwrap(), pts, adj),
109 proj.is_inside_eps(Vector::ZERO, eps_tol),
110 )
111 }
112 TrianglePointLocation::OnFace(_, bcoords) => {
113 (Self::new_with_proj(vertices, bcoords, pts, adj), true)
114 }
115 _ => (Self::new_with_proj(vertices, [0.0; 3], pts, adj), false),
116 }
117 }
118
119 pub fn closest_points(&self, vertices: &[CsoPoint]) -> (Vector, Vector) {
120 (
121 vertices[self.pts[0]].orig1 * self.bcoords[0]
122 + vertices[self.pts[1]].orig1 * self.bcoords[1]
123 + vertices[self.pts[2]].orig1 * self.bcoords[2],
124 vertices[self.pts[0]].orig2 * self.bcoords[0]
125 + vertices[self.pts[1]].orig2 * self.bcoords[1]
126 + vertices[self.pts[2]].orig2 * self.bcoords[2],
127 )
128 }
129
130 pub fn contains_point(&self, id: usize) -> bool {
131 self.pts[0] == id || self.pts[1] == id || self.pts[2] == id
132 }
133
134 pub fn next_ccw_pt_id(&self, id: usize) -> usize {
135 if self.pts[0] == id {
136 1
137 } else if self.pts[1] == id {
138 2
139 } else {
140 if self.pts[2] != id {
141 log::debug!(
142 "Hit unexpected state in EPA: found index {}, expected: {}.",
143 self.pts[2],
144 id
145 );
146 }
147
148 0
149 }
150 }
151
152 pub fn can_be_seen_by(&self, vertices: &[CsoPoint], point: usize, opp_pt_id: usize) -> bool {
153 let p0 = &vertices[self.pts[opp_pt_id]].point;
154 let p1 = &vertices[self.pts[(opp_pt_id + 1) % 3]].point;
155 let p2 = &vertices[self.pts[(opp_pt_id + 2) % 3]].point;
156 let pt = &vertices[point].point;
157
158 // NOTE: it is important that we return true for the case where
159 // the dot product is zero. This is because degenerate faces will
160 // have a zero normal, causing the dot product to be zero.
161 // So return true for these case will let us skip the triangle
162 // during silhouette computation.
163 (*pt - *p0).dot(self.normal) >= -gjk::eps_tol()
164 || Triangle::new(*p1, *p2, *pt).is_affinely_dependent()
165 }
166}
167
168struct SilhouetteEdge {
169 face_id: usize,
170 opp_pt_id: usize,
171}
172
173impl SilhouetteEdge {
174 pub fn new(face_id: usize, opp_pt_id: usize) -> Self {
175 SilhouetteEdge { face_id, opp_pt_id }
176 }
177}
178
179/// The Expanding Polytope Algorithm in 3D.
180///
181/// This structure computes the penetration depth between two shapes when they are overlapping.
182/// It's used after GJK (Gilbert-Johnson-Keerthi) determines that two shapes are penetrating.
183///
184/// # What does EPA do?
185///
186/// EPA finds:
187/// - The **penetration depth**: How far the shapes are overlapping
188/// - The **contact normal**: The direction to separate the shapes
189/// - The **contact points**: Where the shapes are touching on each surface
190///
191/// # How it works in 3D
192///
193/// In 3D, EPA maintains a convex polyhedron (made of triangular faces) in the Minkowski
194/// difference space (CSO - Configuration Space Obstacle) that encloses the origin. It iteratively:
195///
196/// 1. Finds the triangular face closest to the origin
197/// 2. Expands the polyhedron by adding a new support point in the direction of that face's normal
198/// 3. Removes faces that can be "seen" from the new point (they're now inside)
199/// 4. Creates new faces connecting the boundary edges (silhouette) to the new point
200/// 5. Repeats until the polyhedron cannot expand further (convergence)
201///
202/// The final closest face provides the penetration depth (distance to origin) and contact normal.
203///
204/// # Example
205///
206/// ```
207/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
208/// use parry3d::query::epa::EPA;
209/// use parry3d::query::gjk::VoronoiSimplex;
210/// use parry3d::shape::Ball;
211/// use parry3d::math::Pose;
212///
213/// let ball1 = Ball::new(1.0);
214/// let ball2 = Ball::new(1.0);
215/// let pos12 = Pose::translation(1.5, 0.0, 0.0); // Overlapping spheres
216///
217/// // After GJK determines penetration and fills a simplex:
218/// let mut epa = EPA::new();
219/// let simplex = VoronoiSimplex::new(); // Would be filled by GJK
220///
221/// // EPA computes the contact details
222/// // if let Some((pt1, pt2, normal)) = epa.closest_points(&pos12, &ball1, &ball2, &simplex) {
223/// // println!("Penetration depth: {}", (pt2 - pt1).length());
224/// // println!("Contact normal: {}", normal);
225/// // }
226/// # }
227/// ```
228///
229/// # Reusability
230///
231/// The `EPA` structure can be reused across multiple queries to avoid allocations.
232/// Internal buffers are cleared and reused in each call to [`closest_points`](EPA::closest_points).
233///
234/// # Convergence and Failure Cases
235///
236/// EPA may return `None` in these situations:
237/// - The shapes are not actually penetrating (GJK should be used instead)
238/// - Degenerate or nearly-degenerate geometry causes numerical instability
239/// - The initial simplex from GJK is invalid
240/// - The algorithm fails to converge after 100 iterations
241/// - Silhouette extraction fails (topology issues)
242///
243/// When `None` is returned, the shapes may be touching at a single point, edge, or face,
244/// or there may be numerical precision issues with the input geometry.
245///
246/// # Complexity
247///
248/// The 3D EPA implementation is more complex than 2D because:
249/// - It maintains a 3D mesh topology with face adjacency information
250/// - It needs to compute silhouettes (visible edges from a point)
251/// - It handles more degenerate cases (coplanar faces, edge cases)
252#[derive(Default)]
253pub struct EPA {
254 vertices: Vec<CsoPoint>,
255 faces: Vec<Face>,
256 silhouette: Vec<SilhouetteEdge>,
257 heap: BinaryHeap<FaceId>,
258}
259
260impl EPA {
261 /// Creates a new instance of the 3D Expanding Polytope Algorithm.
262 ///
263 /// This allocates internal data structures (vertices, faces, silhouette buffer, and a priority heap).
264 /// The same `EPA` instance can be reused for multiple queries to avoid repeated allocations.
265 ///
266 /// # Example
267 ///
268 /// ```
269 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
270 /// use parry3d::query::epa::EPA;
271 ///
272 /// let mut epa = EPA::new();
273 /// // Use epa for multiple queries...
274 /// # }
275 /// ```
276 pub fn new() -> Self {
277 Self::default()
278 }
279
280 fn reset(&mut self) {
281 self.vertices.clear();
282 self.faces.clear();
283 self.heap.clear();
284 self.silhouette.clear();
285 }
286
287 /// Projects the origin onto the boundary of the given shape.
288 ///
289 /// This is a specialized version of [`closest_points`](EPA::closest_points) for projecting
290 /// a point (the origin) onto a single shape's surface.
291 ///
292 /// # Parameters
293 ///
294 /// - `m`: The position and orientation of the shape in world space
295 /// - `g`: The shape to project onto (must implement [`SupportMap`])
296 /// - `simplex`: A Voronoi simplex from GJK that encloses the origin (indicating the origin
297 /// is inside the shape)
298 ///
299 /// # Returns
300 ///
301 /// - `Some(point)`: The closest point on the shape's boundary to the origin, in the local
302 /// space of `g`
303 /// - `None`: If the origin is not inside the shape, or if EPA failed to converge
304 ///
305 /// # Prerequisites
306 ///
307 /// The origin **must be inside** the shape. If it's outside, use GJK instead.
308 /// Typically, you would:
309 /// 1. Run GJK to detect if a point is inside a shape
310 /// 2. If inside, use this method to find the closest boundary point
311 ///
312 /// # Example
313 ///
314 /// ```
315 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
316 /// use parry3d::query::epa::EPA;
317 /// use parry3d::query::gjk::VoronoiSimplex;
318 /// use parry3d::shape::Ball;
319 /// use parry3d::math::Pose;
320 ///
321 /// let ball = Ball::new(2.0);
322 /// let pos = Pose::identity();
323 ///
324 /// // Assume GJK determined the origin is inside and filled simplex
325 /// let simplex = VoronoiSimplex::new();
326 /// let mut epa = EPA::new();
327 ///
328 /// // Find the closest point on the ball's surface to the origin
329 /// // if let Some(surface_point) = epa.project_origin(&pos, &ball, &simplex) {
330 /// // println!("Closest surface point: {:?}", surface_point);
331 /// // }
332 /// # }
333 /// ```
334 pub fn project_origin<G: ?Sized + SupportMap>(
335 &mut self,
336 m: &Pose,
337 g: &G,
338 simplex: &VoronoiSimplex,
339 ) -> Option<Vector> {
340 self.closest_points(&m.inverse(), g, &ConstantOrigin, simplex)
341 .map(|(p, _, _)| p)
342 }
343
344 /// Computes the closest points between two penetrating shapes and their contact normal.
345 ///
346 /// This is the main EPA method that computes detailed contact information for overlapping shapes.
347 /// It should be called after GJK determines that two shapes are penetrating.
348 ///
349 /// # Parameters
350 ///
351 /// - `pos12`: The relative position/orientation from `g2`'s frame to `g1`'s frame
352 /// (typically computed as `pos1.inverse() * pos2`)
353 /// - `g1`: The first shape (must implement [`SupportMap`])
354 /// - `g2`: The second shape (must implement [`SupportMap`])
355 /// - `simplex`: A Voronoi simplex from GJK that encloses the origin, indicating penetration
356 ///
357 /// # Returns
358 ///
359 /// Returns `Some((point1, point2, normal))` where:
360 /// - `point1`: Contact point on shape `g1` in `g1`'s local frame
361 /// - `point2`: Contact point on shape `g2` in `g2`'s local frame
362 /// - `normal`: Contact normal pointing from `g2` toward `g1`, normalized
363 ///
364 /// The **penetration depth** can be computed as `(point1 - point2).length()` after transforming
365 /// both points to the same coordinate frame.
366 ///
367 /// Returns `None` if:
368 /// - The shapes are not actually penetrating
369 /// - EPA fails to converge (degenerate geometry, numerical issues)
370 /// - The simplex is invalid or empty
371 /// - The algorithm reaches the maximum iteration limit (100 iterations)
372 /// - Silhouette extraction fails (indicates topology corruption)
373 ///
374 /// # Prerequisites
375 ///
376 /// The shapes **must be penetrating**. The typical workflow is:
377 /// 1. Run GJK to check if shapes intersect
378 /// 2. If GJK detects penetration and returns a simplex enclosing the origin
379 /// 3. Use EPA with that simplex to compute detailed contact information
380 ///
381 /// # Example
382 ///
383 /// ```
384 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
385 /// use parry3d::query::epa::EPA;
386 /// use parry3d::query::gjk::{GJKResult, VoronoiSimplex};
387 /// use parry3d::shape::Ball;
388 /// use parry3d::math::Pose;
389 ///
390 /// let ball1 = Ball::new(1.0);
391 /// let ball2 = Ball::new(1.0);
392 ///
393 /// let pos1 = Pose::identity();
394 /// let pos2 = Pose::translation(1.5, 0.0, 0.0); // Overlapping
395 /// let pos12 = pos1.inverse() * pos2;
396 ///
397 /// // After GJK detects penetration:
398 /// let mut simplex = VoronoiSimplex::new();
399 /// // ... simplex would be filled by GJK ...
400 ///
401 /// let mut epa = EPA::new();
402 /// // if let Some((pt1, pt2, normal)) = epa.closest_points(&pos12, &ball1, &ball2, &simplex) {
403 /// // println!("Contact on shape 1: {:?}", pt1);
404 /// // println!("Contact on shape 2: {:?}", pt2);
405 /// // println!("Contact normal: {}", normal);
406 /// // println!("Penetration depth: ~0.5");
407 /// // }
408 /// # }
409 /// ```
410 ///
411 /// # Technical Details
412 ///
413 /// The algorithm works in the **Minkowski difference space** (also called the Configuration
414 /// Space Obstacle or CSO), where the difference between support points of the two shapes
415 /// forms a new shape. When shapes penetrate, this CSO contains the origin.
416 ///
417 /// EPA iteratively expands a convex polyhedron (in 3D) that surrounds the origin. At each
418 /// iteration:
419 /// 1. It finds the triangular face closest to the origin
420 /// 2. Computes a support point in that face's normal direction
421 /// 3. Determines which existing faces are visible from the new point (the **silhouette**)
422 /// 4. Removes visible faces and creates new ones connecting the silhouette boundary to the new point
423 ///
424 /// This process maintains a valid convex hull that progressively tightens around the
425 /// origin until convergence, at which point the closest face defines the penetration
426 /// depth and contact normal.
427 pub fn closest_points<G1, G2>(
428 &mut self,
429 pos12: &Pose,
430 g1: &G1,
431 g2: &G2,
432 simplex: &VoronoiSimplex,
433 ) -> Option<(Vector, Vector, Vector)>
434 where
435 G1: ?Sized + SupportMap,
436 G2: ?Sized + SupportMap,
437 {
438 let _eps = crate::math::DEFAULT_EPSILON;
439 let _eps_tol = _eps * 100.0;
440
441 self.reset();
442
443 /*
444 * Initialization.
445 */
446 for i in 0..simplex.dimension() + 1 {
447 self.vertices.push(*simplex.point(i));
448 }
449
450 if simplex.dimension() == 0 {
451 let mut n: Vector = Vector::ZERO;
452 n[1] = 1.0;
453 return Some((Vector::ZERO, Vector::ZERO, n));
454 } else if simplex.dimension() == 3 {
455 let dp1 = self.vertices[1] - self.vertices[0];
456 let dp2 = self.vertices[2] - self.vertices[0];
457 let dp3 = self.vertices[3] - self.vertices[0];
458
459 if dp1.cross(dp2).dot(dp3) > 0.0 {
460 self.vertices.swap(1, 2)
461 }
462
463 let pts1 = [0, 1, 2];
464 let pts2 = [1, 3, 2];
465 let pts3 = [0, 2, 3];
466 let pts4 = [0, 3, 1];
467
468 let adj1 = [3, 1, 2];
469 let adj2 = [3, 2, 0];
470 let adj3 = [0, 1, 3];
471 let adj4 = [2, 1, 0];
472
473 let (face1, proj_inside1) = Face::new(&self.vertices, pts1, adj1);
474 let (face2, proj_inside2) = Face::new(&self.vertices, pts2, adj2);
475 let (face3, proj_inside3) = Face::new(&self.vertices, pts3, adj3);
476 let (face4, proj_inside4) = Face::new(&self.vertices, pts4, adj4);
477
478 self.faces.push(face1);
479 self.faces.push(face2);
480 self.faces.push(face3);
481 self.faces.push(face4);
482
483 if proj_inside1 {
484 let dist1 = self.faces[0].normal.dot(self.vertices[0].point);
485 self.heap.push(FaceId::new(0, -dist1)?);
486 }
487
488 if proj_inside2 {
489 let dist2 = self.faces[1].normal.dot(self.vertices[1].point);
490 self.heap.push(FaceId::new(1, -dist2)?);
491 }
492
493 if proj_inside3 {
494 let dist3 = self.faces[2].normal.dot(self.vertices[2].point);
495 self.heap.push(FaceId::new(2, -dist3)?);
496 }
497
498 if proj_inside4 {
499 let dist4 = self.faces[3].normal.dot(self.vertices[3].point);
500 self.heap.push(FaceId::new(3, -dist4)?);
501 }
502
503 if !(proj_inside1 || proj_inside2 || proj_inside3 || proj_inside4) {
504 // Related issues:
505 // https://github.com/dimforge/parry/issues/253
506 // https://github.com/dimforge/parry/issues/246
507 log::debug!("Hit unexpected state in EPA: failed to project the origin on the initial simplex.");
508 return None;
509 }
510 } else {
511 if simplex.dimension() == 1 {
512 let dpt = self.vertices[1] - self.vertices[0];
513
514 crate::math::orthonormal_subspace_basis(&[dpt], |dir| {
515 // dir is already normalized by orthonormal_subspace_basis
516 self.vertices
517 .push(CsoPoint::from_shapes(pos12, g1, g2, dir));
518 false
519 });
520 }
521
522 let pts1 = [0, 1, 2];
523 let pts2 = [0, 2, 1];
524
525 let adj1 = [1, 1, 1];
526 let adj2 = [0, 0, 0];
527
528 let (face1, _) = Face::new(&self.vertices, pts1, adj1);
529 let (face2, _) = Face::new(&self.vertices, pts2, adj2);
530 self.faces.push(face1);
531 self.faces.push(face2);
532
533 self.heap.push(FaceId::new(0, 0.0)?);
534 self.heap.push(FaceId::new(1, 0.0)?);
535 }
536
537 let mut niter = 0;
538 let mut max_dist = Real::max_value();
539 let mut best_face_id = *self.heap.peek()?;
540 let mut old_dist = 0.0;
541
542 /*
543 * Run the expansion.
544 */
545 while let Some(face_id) = self.heap.pop() {
546 // Create new faces.
547 let face = self.faces[face_id.id].clone();
548
549 if face.deleted {
550 continue;
551 }
552
553 let cso_point = CsoPoint::from_shapes(pos12, g1, g2, face.normal);
554 let support_point_id = self.vertices.len();
555 self.vertices.push(cso_point);
556
557 let candidate_max_dist = cso_point.point.dot(face.normal);
558
559 if candidate_max_dist < max_dist {
560 best_face_id = face_id;
561 max_dist = candidate_max_dist;
562 }
563
564 let curr_dist = -face_id.neg_dist;
565
566 if max_dist - curr_dist < _eps_tol ||
567 // Accept the intersection as the algorithm is stuck and no new points will be found
568 // This happens because of numerical stability issue
569 ((curr_dist - old_dist).abs() < _eps && candidate_max_dist < max_dist)
570 {
571 let best_face = &self.faces[best_face_id.id];
572 let points = best_face.closest_points(&self.vertices);
573 return Some((points.0, points.1, best_face.normal));
574 }
575
576 old_dist = curr_dist;
577
578 self.faces[face_id.id].deleted = true;
579
580 let adj_opp_pt_id1 = self.faces[face.adj[0]].next_ccw_pt_id(face.pts[0]);
581 let adj_opp_pt_id2 = self.faces[face.adj[1]].next_ccw_pt_id(face.pts[1]);
582 let adj_opp_pt_id3 = self.faces[face.adj[2]].next_ccw_pt_id(face.pts[2]);
583
584 self.compute_silhouette(support_point_id, face.adj[0], adj_opp_pt_id1);
585 self.compute_silhouette(support_point_id, face.adj[1], adj_opp_pt_id2);
586 self.compute_silhouette(support_point_id, face.adj[2], adj_opp_pt_id3);
587
588 let first_new_face_id = self.faces.len();
589
590 if self.silhouette.is_empty() {
591 // TODO: Something went very wrong because we failed to extract a silhouette…
592 return None;
593 }
594
595 for edge in &self.silhouette {
596 if !self.faces[edge.face_id].deleted {
597 let new_face_id = self.faces.len();
598
599 let face_adj = &mut self.faces[edge.face_id];
600 let pt_id1 = face_adj.pts[(edge.opp_pt_id + 2) % 3];
601 let pt_id2 = face_adj.pts[(edge.opp_pt_id + 1) % 3];
602
603 let pts = [pt_id1, pt_id2, support_point_id];
604 let adj = [edge.face_id, new_face_id + 1, new_face_id - 1];
605 let new_face = Face::new(&self.vertices, pts, adj);
606
607 face_adj.adj[(edge.opp_pt_id + 1) % 3] = new_face_id;
608
609 self.faces.push(new_face.0);
610
611 if new_face.1 {
612 let pt = self.vertices[self.faces[new_face_id].pts[0]].point;
613 let dist = self.faces[new_face_id].normal.dot(pt);
614 if dist < curr_dist {
615 // TODO: if we reach this point, there were issues due to
616 // numerical errors.
617 let points = face.closest_points(&self.vertices);
618 return Some((points.0, points.1, face.normal));
619 }
620
621 self.heap.push(FaceId::new(new_face_id, -dist)?);
622 }
623 }
624 }
625
626 if first_new_face_id == self.faces.len() {
627 // Something went very wrong because all the edges
628 // from the silhouette belonged to deleted faces.
629 return None;
630 }
631
632 self.faces[first_new_face_id].adj[2] = self.faces.len() - 1;
633 self.faces.last_mut().unwrap().adj[1] = first_new_face_id;
634
635 self.silhouette.clear();
636 // self.check_topology(); // NOTE: for debugging only.
637
638 niter += 1;
639 if niter > 100 {
640 // if we reached this point, our algorithm didn't converge to what precision we wanted.
641 // still return an intersection point, as it's probably close enough.
642 break;
643 }
644 }
645
646 let best_face = &self.faces[best_face_id.id];
647 let points = best_face.closest_points(&self.vertices);
648 Some((points.0, points.1, best_face.normal))
649 }
650
651 fn compute_silhouette(&mut self, point: usize, id: usize, opp_pt_id: usize) {
652 if !self.faces[id].deleted {
653 if !self.faces[id].can_be_seen_by(&self.vertices, point, opp_pt_id) {
654 self.silhouette.push(SilhouetteEdge::new(id, opp_pt_id));
655 } else {
656 self.faces[id].deleted = true;
657
658 let adj_pt_id1 = (opp_pt_id + 2) % 3;
659 let adj_pt_id2 = opp_pt_id;
660
661 let adj1 = self.faces[id].adj[adj_pt_id1];
662 let adj2 = self.faces[id].adj[adj_pt_id2];
663
664 let adj_opp_pt_id1 =
665 self.faces[adj1].next_ccw_pt_id(self.faces[id].pts[adj_pt_id1]);
666 let adj_opp_pt_id2 =
667 self.faces[adj2].next_ccw_pt_id(self.faces[id].pts[adj_pt_id2]);
668
669 self.compute_silhouette(point, adj1, adj_opp_pt_id1);
670 self.compute_silhouette(point, adj2, adj_opp_pt_id2);
671 }
672 }
673 }
674
675 #[allow(dead_code)]
676 #[cfg(feature = "std")]
677 fn print_silhouette(&self) {
678 use std::{print, println};
679
680 print!("Silhouette points: ");
681 for i in 0..self.silhouette.len() {
682 let edge = &self.silhouette[i];
683 let face = &self.faces[edge.face_id];
684
685 if !face.deleted {
686 print!(
687 "({}, {}) ",
688 face.pts[(edge.opp_pt_id + 2) % 3],
689 face.pts[(edge.opp_pt_id + 1) % 3]
690 );
691 }
692 }
693 println!();
694 }
695
696 #[allow(dead_code)]
697 fn check_topology(&self) {
698 for i in 0..self.faces.len() {
699 let face = &self.faces[i];
700 if face.deleted {
701 continue;
702 }
703
704 // println!("checking {}-th face.", i);
705 let adj1 = &self.faces[face.adj[0]];
706 let adj2 = &self.faces[face.adj[1]];
707 let adj3 = &self.faces[face.adj[2]];
708
709 assert!(!adj1.deleted);
710 assert!(!adj2.deleted);
711 assert!(!adj3.deleted);
712
713 assert!(face.pts[0] != face.pts[1]);
714 assert!(face.pts[0] != face.pts[2]);
715 assert!(face.pts[1] != face.pts[2]);
716
717 assert!(adj1.contains_point(face.pts[0]));
718 assert!(adj1.contains_point(face.pts[1]));
719
720 assert!(adj2.contains_point(face.pts[1]));
721 assert!(adj2.contains_point(face.pts[2]));
722
723 assert!(adj3.contains_point(face.pts[2]));
724 assert!(adj3.contains_point(face.pts[0]));
725
726 let opp_pt_id1 = adj1.next_ccw_pt_id(face.pts[0]);
727 let opp_pt_id2 = adj2.next_ccw_pt_id(face.pts[1]);
728 let opp_pt_id3 = adj3.next_ccw_pt_id(face.pts[2]);
729
730 assert!(!face.contains_point(adj1.pts[opp_pt_id1]));
731 assert!(!face.contains_point(adj2.pts[opp_pt_id2]));
732 assert!(!face.contains_point(adj3.pts[opp_pt_id3]));
733
734 assert!(adj1.adj[(opp_pt_id1 + 1) % 3] == i);
735 assert!(adj2.adj[(opp_pt_id2 + 1) % 3] == i);
736 assert!(adj3.adj[(opp_pt_id3 + 1) % 3] == i);
737 }
738 }
739}