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}