parry3d/transformation/convex_hull3/
convex_hull.rs

1use super::InitialMesh;
2use super::{ConvexHullError, TriangleFacet};
3use crate::math::Real;
4use crate::transformation::convex_hull_utils::indexed_support_point_nth;
5use crate::transformation::convex_hull_utils::{indexed_support_point_id, normalize};
6use crate::utils;
7use alloc::{vec, vec::Vec};
8use na::{self, Point3};
9
10/// Computes the convex hull of a set of 3D points.
11///
12/// The convex hull is the smallest convex shape that contains all input points.
13/// Imagine wrapping a rubber band (2D) or shrink-wrap (3D) tightly around the points.
14///
15/// # Algorithm
16///
17/// Uses the **quickhull algorithm**, which is efficient for most inputs:
18/// - Average case: O(n log n)
19/// - Worst case: O(n²) for specially constructed inputs
20///
21/// # Returns
22///
23/// A tuple `(vertices, indices)`:
24/// - **vertices**: The hull vertices (subset of input points, duplicates removed)
25/// - **indices**: Triangle faces as triplets of vertex indices (CCW winding)
26///
27/// # Panics
28///
29/// Panics if the input is degenerate or the algorithm fails. For error handling,
30/// use [`try_convex_hull`] instead.
31///
32/// # Example
33///
34/// ```rust
35/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
36/// use parry3d::transformation::convex_hull;
37/// use nalgebra::Point3;
38///
39/// // Points forming a tetrahedron
40/// let points = vec![
41///     Point3::origin(),
42///     Point3::new(1.0, 0.0, 0.0),
43///     Point3::new(0.0, 1.0, 0.0),
44///     Point3::new(0.0, 0.0, 1.0),
45/// ];
46///
47/// let (vertices, indices) = convex_hull(&points);
48///
49/// // Hull has 4 vertices (all input points are on the hull)
50/// assert_eq!(vertices.len(), 4);
51///
52/// // Hull has 4 triangular faces
53/// assert_eq!(indices.len(), 4);
54/// # }
55/// ```
56///
57/// # See Also
58///
59/// - [`try_convex_hull`] - Returns `Result` instead of panicking
60pub fn convex_hull(points: &[Point3<Real>]) -> (Vec<Point3<Real>>, Vec<[u32; 3]>) {
61    try_convex_hull(points).unwrap()
62}
63
64/// Computes the convex hull of a set of 3D points, with error handling.
65///
66/// This is the safe version of [`convex_hull`] that returns a `Result` instead
67/// of panicking on degenerate inputs.
68///
69/// # Arguments
70///
71/// * `points` - The input points (must have at least 3 points)
72///
73/// # Returns
74///
75/// * `Ok((vertices, indices))` - Successfully computed convex hull
76/// * `Err(ConvexHullError::IncompleteInput)` - Less than 3 input points
77/// * `Err(ConvexHullError::...)` - Other errors (degenerate geometry, numerical issues)
78///
79/// # Example
80///
81/// ```rust
82/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
83/// use parry3d::transformation::try_convex_hull;
84/// use nalgebra::Point3;
85///
86/// // Valid input
87/// let points = vec![
88///     Point3::origin(),
89///     Point3::new(1.0, 0.0, 0.0),
90///     Point3::new(0.0, 1.0, 0.0),
91///     Point3::new(0.0, 0.0, 1.0),
92/// ];
93///
94/// match try_convex_hull(&points) {
95///     Ok((vertices, indices)) => {
96///         println!("Hull: {} vertices, {} faces", vertices.len(), indices.len());
97///     }
98///     Err(e) => {
99///         println!("Failed: {:?}", e);
100///     }
101/// }
102///
103/// // Degenerate input (too few points)
104/// let bad_points = vec![Point3::origin(), Point3::new(1.0, 0.0, 0.0)];
105/// assert!(try_convex_hull(&bad_points).is_err());
106/// # }
107/// ```
108pub fn try_convex_hull(
109    points: &[Point3<Real>],
110) -> Result<(Vec<Point3<Real>>, Vec<[u32; 3]>), ConvexHullError> {
111    if points.len() < 3 {
112        return Err(ConvexHullError::IncompleteInput);
113    }
114
115    // print_buildable_vec("input", points);
116
117    let mut normalized_points = points.to_vec();
118    let _ = normalize(&mut normalized_points[..]);
119
120    let mut undecidable_points = Vec::new();
121    let mut silhouette_loop_facets_and_idx = Vec::new();
122    let mut removed_facets = Vec::new();
123
124    let mut triangles;
125
126    match super::try_get_initial_mesh(points, &mut normalized_points[..], &mut undecidable_points)?
127    {
128        InitialMesh::Facets(facets) => {
129            triangles = facets;
130        }
131        InitialMesh::ResultMesh(vertices, indices) => {
132            return Ok((vertices, indices));
133        }
134    }
135
136    let mut i = 0;
137    while i != triangles.len() {
138        silhouette_loop_facets_and_idx.clear();
139
140        if !triangles[i].valid || triangles[i].affinely_dependent {
141            i += 1;
142            continue;
143        }
144
145        // TODO: use triangles[i].furthest_point instead.
146        let pt_id = indexed_support_point_id(
147            &triangles[i].normal,
148            &normalized_points[..],
149            triangles[i].visible_points[..].iter().copied(),
150        );
151
152        if let Some(point) = pt_id {
153            triangles[i].valid = false;
154
155            removed_facets.clear();
156            removed_facets.push(i);
157
158            for j in 0usize..3 {
159                // println!(">> loop;");
160                compute_silhouette(
161                    triangles[i].adj[j],
162                    triangles[i].indirect_adj_id[j],
163                    point,
164                    &mut silhouette_loop_facets_and_idx,
165                    &normalized_points[..],
166                    &mut removed_facets,
167                    &mut triangles[..],
168                );
169            }
170
171            // In some degenerate cases (because of float rounding problems), the silhouette may:
172            // 1. Contain self-intersections (i.e. a single vertex is used by more than two edges).
173            // 2. Contain multiple disjoint (but nested) loops.
174            fix_silhouette_topology(
175                &normalized_points,
176                &mut silhouette_loop_facets_and_idx,
177                &mut removed_facets,
178                &mut triangles[..],
179            )?;
180
181            // Check that the silhouette is valid.
182            // TODO: remove this debug code.
183            // {
184            //     for (facet, id) in &silhouette_loop_facets_and_idx {
185            //         assert!(triangles[*facet].valid);
186            //         assert!(!triangles[triangles[*facet].adj[*id]].valid);
187            //     }
188            // }
189
190            if silhouette_loop_facets_and_idx.is_empty() {
191                // Due to inaccuracies, the silhouette could not be computed
192                // (the point seems to be visible from… every triangle).
193                let mut any_valid = false;
194                for triangle in &triangles[i + 1..] {
195                    if triangle.valid && !triangle.affinely_dependent {
196                        any_valid = true;
197                    }
198                }
199
200                if any_valid {
201                    return Err(ConvexHullError::InternalError(
202                        "Internal error: exiting an unfinished work.",
203                    ));
204                }
205
206                // TODO: this is very harsh.
207                triangles[i].valid = true;
208                break;
209            }
210
211            attach_and_push_facets(
212                &silhouette_loop_facets_and_idx[..],
213                point,
214                &normalized_points[..],
215                &mut triangles,
216                &removed_facets[..],
217                &mut undecidable_points,
218            );
219
220            // println!("Verifying facets at iteration: {}, k: {}", i, k);
221            // for i in 0..triangles.len() {
222            //     if triangles[i].valid {
223            //         super::check_facet_links(i, &triangles[..]);
224            //     }
225            // }
226        }
227
228        i += 1;
229    }
230
231    let mut idx = Vec::new();
232
233    for facet in triangles.iter() {
234        if facet.valid {
235            idx.push([
236                facet.pts[0] as u32,
237                facet.pts[1] as u32,
238                facet.pts[2] as u32,
239            ]);
240        }
241    }
242
243    let mut points = points.to_vec();
244    utils::remove_unused_points(&mut points, &mut idx[..]);
245    // super::check_convex_hull(&points, &idx);
246
247    Ok((points, idx))
248}
249
250fn compute_silhouette(
251    facet: usize,
252    indirect_id: usize,
253    point: usize,
254    out_facets_and_idx: &mut Vec<(usize, usize)>,
255    points: &[Point3<Real>],
256    removed_facets: &mut Vec<usize>,
257    triangles: &mut [TriangleFacet],
258) {
259    if triangles[facet].valid {
260        if !triangles[facet].order_independent_can_be_seen_by_point(point, points) {
261            // println!("triangles: {}, valid: true, keep: true", facet);
262            // println!(
263            //     "Taking edge: [{}, {}]",
264            //     triangles[facet].second_point_from_edge(indirect_id),
265            //     triangles[facet].first_point_from_edge(indirect_id)
266            // );
267            out_facets_and_idx.push((facet, indirect_id));
268        } else {
269            triangles[facet].valid = false; // The facet must be removed from the convex hull.
270            removed_facets.push(facet);
271            // println!("triangles: {}, valid: true, keep: false", facet);
272
273            compute_silhouette(
274                triangles[facet].adj[(indirect_id + 1) % 3],
275                triangles[facet].indirect_adj_id[(indirect_id + 1) % 3],
276                point,
277                out_facets_and_idx,
278                points,
279                removed_facets,
280                triangles,
281            );
282
283            compute_silhouette(
284                triangles[facet].adj[(indirect_id + 2) % 3],
285                triangles[facet].indirect_adj_id[(indirect_id + 2) % 3],
286                point,
287                out_facets_and_idx,
288                points,
289                removed_facets,
290                triangles,
291            );
292        }
293    } else {
294        // println!("triangles: {}, valid: false, keep: false", facet);
295    }
296}
297
298fn fix_silhouette_topology(
299    points: &[Point3<Real>],
300    out_facets_and_idx: &mut Vec<(usize, usize)>,
301    removed_facets: &mut Vec<usize>,
302    triangles: &mut [TriangleFacet],
303) -> Result<(), ConvexHullError> {
304    // TODO: don't allocate this everytime.
305    let mut workspace = vec![0; points.len()];
306    let mut needs_fixing = false;
307
308    // NOTE: we wore with the second_point_from_edge instead
309    // of the first one, because when we traverse the silhouette
310    // we see the second edge point before the first.
311    for (facet, adj_id) in &*out_facets_and_idx {
312        let p = triangles[*facet].second_point_from_edge(*adj_id);
313        workspace[p] += 1;
314
315        if workspace[p] > 1 {
316            needs_fixing = true;
317        }
318    }
319
320    // We detected a topological problem, i.e., we have
321    // multiple loops.
322    if needs_fixing {
323        // First, we need to know which loop is the one we
324        // need to keep.
325        let mut loop_start = 0;
326        for (facet, adj_id) in &*out_facets_and_idx {
327            let p1 = points[triangles[*facet].second_point_from_edge(*adj_id)];
328            let p2 = points[triangles[*facet].first_point_from_edge(*adj_id)];
329            let supp = indexed_support_point_nth(
330                &(p2 - p1),
331                points,
332                out_facets_and_idx
333                    .iter()
334                    .map(|(f, ai)| triangles[*f].second_point_from_edge(*ai)),
335            )
336            .ok_or(ConvexHullError::MissingSupportPoint)?;
337            let selected = &out_facets_and_idx[supp];
338            if workspace[triangles[selected.0].second_point_from_edge(selected.1)] == 1 {
339                // This is a valid point to start with.
340                loop_start = supp;
341                break;
342            }
343        }
344
345        let mut removing = None;
346        let old_facets_and_idx = core::mem::take(out_facets_and_idx);
347
348        for i in 0..old_facets_and_idx.len() {
349            let facet_id = (loop_start + i) % old_facets_and_idx.len();
350            let (facet, adj_id) = old_facets_and_idx[facet_id];
351
352            match removing {
353                Some(p) => {
354                    let p1 = triangles[facet].second_point_from_edge(adj_id);
355                    if p == p1 {
356                        removing = None;
357                    }
358                }
359                _ => {
360                    let p1 = triangles[facet].second_point_from_edge(adj_id);
361                    if workspace[p1] > 1 {
362                        removing = Some(p1);
363                    }
364                }
365            }
366
367            if removing.is_some() {
368                if triangles[facet].valid {
369                    triangles[facet].valid = false;
370                    removed_facets.push(facet);
371                }
372            } else {
373                out_facets_and_idx.push((facet, adj_id));
374            }
375
376            // // Debug
377            // {
378            //     let p1 = triangles[facet].second_point_from_edge(adj_id);
379            //     let p2 = triangles[facet].first_point_from_edge(adj_id);
380            //     if removing.is_some() {
381            //         print!("/{}, {}\\ ", p1, p2);
382            //     } else {
383            //         print!("[{}, {}] ", p1, p2);
384            //     }
385            // }
386        }
387
388        // println!();
389    }
390
391    Ok(())
392}
393
394fn attach_and_push_facets(
395    silhouette_loop_facets_and_idx: &[(usize, usize)],
396    point: usize,
397    points: &[Point3<Real>],
398    triangles: &mut Vec<TriangleFacet>,
399    removed_facets: &[usize],
400    undecidable: &mut Vec<usize>,
401) {
402    // The silhouette is built to be in CCW order.
403    let mut new_facets = Vec::with_capacity(silhouette_loop_facets_and_idx.len());
404
405    // Create new facets.
406    let mut adj_facet: usize;
407    let mut indirect_id: usize;
408
409    for silhouette_loop_facets_and_id in silhouette_loop_facets_and_idx {
410        adj_facet = silhouette_loop_facets_and_id.0;
411        indirect_id = silhouette_loop_facets_and_id.1;
412
413        // print!(
414        //     "[{}, {}] ",
415        //     triangles[adj_facet].second_point_from_edge(indirect_id),
416        //     triangles[adj_facet].first_point_from_edge(indirect_id)
417        // );
418
419        let facet = TriangleFacet::new(
420            point,
421            triangles[adj_facet].second_point_from_edge(indirect_id),
422            triangles[adj_facet].first_point_from_edge(indirect_id),
423            points,
424        );
425        new_facets.push(facet);
426    }
427    // println!();
428
429    // Link the facets together.
430    for i in 0..silhouette_loop_facets_and_idx.len() {
431        let prev_facet = if i == 0 {
432            triangles.len() + silhouette_loop_facets_and_idx.len() - 1
433        } else {
434            triangles.len() + i - 1
435        };
436
437        let (middle_facet, middle_id) = silhouette_loop_facets_and_idx[i];
438        let next_facet = triangles.len() + (i + 1) % silhouette_loop_facets_and_idx.len();
439
440        new_facets[i].set_facets_adjascency(prev_facet, middle_facet, next_facet, 2, middle_id, 0);
441        assert!(!triangles[triangles[middle_facet].adj[middle_id]].valid); // Check that we are not overwriting a valid link.
442        triangles[middle_facet].adj[middle_id] = triangles.len() + i; // The future id of curr_facet.
443        triangles[middle_facet].indirect_adj_id[middle_id] = 1;
444    }
445
446    // Assign to each facets some of the points which can see it.
447    // TODO: refactor this with the others.
448    for curr_facet in removed_facets.iter() {
449        for visible_point in triangles[*curr_facet].visible_points.iter() {
450            if points[*visible_point] == points[point] {
451                continue;
452            }
453
454            let mut furthest = usize::MAX;
455            let mut furthest_dist = 0.0;
456
457            for (i, curr_facet) in new_facets.iter_mut().enumerate() {
458                if !curr_facet.affinely_dependent {
459                    let distance = curr_facet.distance_to_point(*visible_point, points);
460
461                    if distance > furthest_dist {
462                        furthest = i;
463                        furthest_dist = distance;
464                    }
465                }
466            }
467
468            if furthest != usize::MAX && new_facets[furthest].can_see_point(*visible_point, points)
469            {
470                new_facets[furthest].add_visible_point(*visible_point, points);
471            }
472
473            // If none of the facet can be seen from the point, it is implicitly
474            // deleted because it won't be referenced by any facet.
475        }
476    }
477
478    // Try to assign collinear points to one of the new facets.
479    let mut i = 0;
480
481    while i != undecidable.len() {
482        let mut furthest = usize::MAX;
483        let mut furthest_dist = 0.0;
484        let undecidable_point = undecidable[i];
485
486        for (j, curr_facet) in new_facets.iter_mut().enumerate() {
487            if curr_facet.can_see_point(undecidable_point, points) {
488                let distance = curr_facet.distance_to_point(undecidable_point, points);
489
490                if distance > furthest_dist {
491                    furthest = j;
492                    furthest_dist = distance;
493                }
494            }
495        }
496
497        if furthest != usize::MAX {
498            new_facets[furthest].add_visible_point(undecidable_point, points);
499            let _ = undecidable.swap_remove(i);
500        } else {
501            i += 1;
502        }
503    }
504
505    // Push facets.
506    // TODO: can we avoid the tmp vector `new_facets` ?
507    triangles.append(&mut new_facets);
508}
509
510#[cfg(test)]
511mod test {
512    use crate::transformation;
513    #[cfg(feature = "dim2")]
514    use na::Point2;
515
516    #[cfg(feature = "dim2")]
517    #[test]
518    fn test_simple_convex_hull() {
519        let points = [
520            Point2::new(4.723881f32, 3.597233),
521            Point2::new(3.333363, 3.429991),
522            Point2::new(3.137215, 2.812263),
523        ];
524
525        let chull = transformation::convex_hull(points.as_slice());
526
527        assert!(chull.coords.len() == 3);
528    }
529
530    #[cfg(feature = "dim3")]
531    #[test]
532    fn test_ball_convex_hull() {
533        use crate::shape::Ball;
534
535        // This triggered a failure to an affinely dependent facet.
536        let (points, _) = Ball::new(0.4).to_trimesh(20, 20);
537        let (vertices, _) = transformation::convex_hull(points.as_slice());
538
539        // dummy test, we are just checking that the construction did not fail.
540        assert!(vertices.len() == vertices.len());
541    }
542}