math_utils/geometry/primitive/
hull.rs

1//! Convex hulls
2
3use std;
4use std::collections::{BTreeSet, VecDeque};
5use approx;
6
7use crate::*;
8use super::*;
9
10use geometry::mesh::VertexEdgeTriangleMesh;
11use geometry::mesh::edge_triangle::TriangleKey;
12
13/// 2D convex hull
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Hull2 <S> {
16  /// Set of unique points sorted in counter-clockwise order for efficient
17  /// minimum bounding box algorithm
18  points : Vec <Point2 <S>>
19}
20
21/// 3D convex hull
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct Hull3 <S> {
24  points : Vec <Point3 <S>>
25}
26
27impl <S> Hull2 <S> {
28  /// Create a new 2D convex hull from a bag of points.
29  ///
30  /// Uses Graham scan algorithm.
31  ///
32  /// Input must contain at least 1 point:
33  /// ```
34  /// # use math_utils::geometry::Hull2;
35  /// assert!(Hull2::<f32>::from_points (&[]).is_none());
36  /// ```
37  pub fn from_points (points : &[Point2 <S>]) -> Option <Self> where S : Real {
38    Self::from_points_indices (points).map (|indices|{
39      let points = indices.iter().map (|i| points[*i as usize]).collect();
40      Hull2 { points }
41    })
42  }
43
44  /// Points sorted in counter-clockwise order
45  pub fn points (&self) -> &[Point2 <S>] {
46    &self.points
47  }
48
49  #[expect(clippy::missing_asserts_for_indexing)]
50  fn from_points_indices (points : &[Point2 <S>]) -> Option <Vec <u32>> where S : Real {
51    // code adapted from scirs2-spatial:
52    // <https://github.com/cool-japan/scirs/blob/a176b462aca55e73bd4b25eea83aad10a9f4a2b0/scirs2-spatial/src/convex_hull.rs>
53    use std::cmp::Ordering;
54    match points.len() {
55      0 => return None,
56      1 => return Some (vec![0]),
57      2 => {
58        let v = if points[0] == points[1] {
59          vec![0]
60        } else {
61          vec![0, 1]
62        };
63        return Some (v)
64      }
65      _ => {} // continue
66    }
67    let mut indexed_points = points.iter().copied().enumerate()
68      .collect::<Vec <(usize, Point2 <S>)>>();
69    // find bottom-most (lowest y-coordinate), then left-most x
70    let start_index = indexed_points.iter().min_by (|a, b|{
71      let cmp = a.1.0.y.partial_cmp (&b.1.0.y).unwrap();
72      if cmp == Ordering::Equal {
73        a.1.0.x.partial_cmp (&b.1.0.x).unwrap()
74      } else {
75        cmp
76      }
77    }).unwrap().0;
78    let start_point = indexed_points[start_index].1;
79    // sort points by polar angle with respect to start point
80    indexed_points.sort_by (|a, b| {
81      if a.0 == start_index {
82        return Ordering::Less
83      }
84      if b.0 == start_index {
85        return Ordering::Greater
86      }
87      let angle_a = (a.1.0.y - start_point.0.y)
88        .atan2 (a.1.0.x - start_point.0.x);
89      let angle_b = (b.1.0.y - start_point.0.y)
90        .atan2 (b.1.0.x - start_point.0.x);
91      let angle_cmp = angle_a.partial_cmp (&angle_b).unwrap();
92      if angle_cmp == Ordering::Equal {
93        // if angles are equal sort by distance
94        let dist_a = (a.1.0.x - start_point.0.x).powi (2) +
95          (a.1.0.y - start_point.0.y).powi (2);
96        let dist_b = (b.1.0.x - start_point.0.x).powi (2) +
97          (b.1.0.y - start_point.0.y).powi (2);
98        dist_a.partial_cmp (&dist_b).unwrap()
99      } else {
100        angle_cmp
101      }
102    });
103    // graham scan
104    let mut stack : Vec <u32> = Vec::new();
105    for (point_index, point) in indexed_points {
106      while stack.len() >= 2 {
107        let top = stack[stack.len() - 1];
108        let second = stack[stack.len() -2];
109        let p1 = points[second as usize];
110        let p2 = points[top as usize];
111        let p3 = point;
112        let det = Matrix2 {
113          cols: vector2 (p2 - p1, p3 - p1)
114        }.determinant();
115        if det <= S::zero() {
116          stack.pop();
117        } else {
118          break
119        }
120      }
121      #[expect(clippy::cast_possible_truncation)]
122      stack.push (point_index as u32)
123    }
124    Some (stack)
125  }
126}
127
128impl <S> Hull3 <S> {
129  pub fn from_points (points : &[Point3 <S>]) -> Option <Self> where
130    S : Real + approx::RelativeEq
131  {
132    // code adapted from GeometricTools:
133    // <https://github.com/davideberly/GeometricTools/blob/f78dd0b65bc3a0a4723586de6991dd2c339b08ad/GTE/Mathematics/ConvexHull3.h>
134    // TODO: multi-threaded
135    match points.len() {
136      0 => return None,
137      1 => return Some (Hull3 { points: points.to_vec() }),
138      2 => {
139        let mut points = points.to_vec();
140        points.dedup();
141        return Some (Hull3 { points })
142      }
143      _ => {}
144    }
145    let get_point = |i : u32| points[i as usize];
146    let collect_points = |indices : &[u32]|
147      indices.iter().copied().map (get_point).collect();
148    let sorted = {
149      #[expect(clippy::cast_possible_truncation)]
150      let mut sorted = (0..points.len() as u32).collect::<Vec<_>>();
151      sorted.sort_by (|a, b|
152        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
153      sorted.dedup_by_key (|i| get_point(*i));
154      sorted
155    };
156    let mut hull = Vec::with_capacity (sorted.len());
157    hull.push (sorted[0]);  // hull[0]
158    let mut current = 1;
159    let mut dimension = 0;
160    // point hull
161    for i in &sorted[current..] {
162      if get_point (hull[0]) != get_point (*i) {
163        dimension = 1;
164        break
165      }
166      current += 1;
167    }
168    debug_assert_eq!(hull.len(), 1);
169    if dimension == 0 {
170      let points = collect_points (hull.as_slice());
171      return Some (Hull3 { points })
172    }
173    // linear hull
174    hull.push (sorted[current]);  // hull[1]
175    current += 1;
176    for i in &sorted[current..] {
177      if !colinear_3d (&get_point (hull[0]), &get_point (hull[1]), &get_point (*i)) {
178        dimension = 2;
179        break
180      }
181      hull.push (sorted[current]);
182      current += 1;
183    }
184    if hull.len() > 2 {
185      // keep endpoints
186      hull.sort_by (|a, b|
187        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
188      hull.drain (1..hull.len()-1);
189    }
190    debug_assert_eq!(hull.len(), 2);
191    if dimension == 1 {
192      let points = collect_points (hull.as_slice());
193      return Some (Hull3 { points })
194    }
195    // planar hull
196    hull.push (sorted[current]);  // hull[2]
197    current += 1;
198    while current < sorted.len() {
199      if !coplanar_3d (
200        &get_point (hull[0]), &get_point (hull[1]), &get_point (hull[2]),
201        &get_point (sorted[current])
202      ) {
203        dimension = 3;
204        break
205      }
206      hull.push (sorted[current]);
207      current += 1;
208    }
209    if hull.len() > 3 {
210      // compute planar convex hull
211      let v0     = get_point (hull[0]);
212      let v1     = get_point (hull[1]);
213      let v2     = get_point (hull[2]);
214      let diff1  = v1 - v0;
215      let diff2  = v2 - v0;
216      let normal = diff1.cross (diff2);
217      let signs  = normal.sigvec();
218      let c;
219      debug_assert_eq!(signs.len(), 3);
220      #[expect(clippy::collapsible_else_if)]
221      if normal.x > normal.y {
222        if normal.x > normal.z {
223          if signs[0] > S::zero() {
224            c = [1, 2];
225          } else {
226            c = [2, 1];
227          }
228        } else {
229          if signs[2] > S::zero() {
230            c = [0, 1];
231          } else {
232            c = [1, 0];
233          }
234        }
235      } else {
236        if normal.y > normal.z {
237          if signs[1] > S::zero() {
238            c = [2, 0];
239          } else {
240            c = [0, 2];
241          }
242        } else {
243          if signs[2] > S::zero() {
244            c = [0, 1];
245          } else {
246            c = [1, 0];
247          }
248        }
249      }
250      let projections = hull.iter().copied()
251        .map (|i| point2 (get_point (i).0[c[0]], get_point (i).0[c[1]]))
252        .collect::<Vec <_>>();
253      let hull2_indices = Hull2::from_points_indices (projections.as_slice()).unwrap();
254      hull = hull2_indices.iter().map (|i| hull[*i as usize]).collect::<Vec <_>>();
255    }
256    if dimension == 2 {
257      let points = collect_points (hull.as_slice());
258      return Some (Hull3 { points })
259    }
260    // 3-dimensional hull
261    let plane_side = |a, b, c, d| {
262      let [s0, s1, s2, s3] = [a, b, c, d].map (get_point);
263      let diff1 = s1 - s0;
264      let diff2 = s2 - s0;
265      let diff3 = s3 - s0;
266      diff1.dot (diff2.cross (diff3)).signum_or_zero()
267    };
268    let sign = plane_side (hull[0], hull[1], hull[2], sorted[current]);
269    let mut mesh = VertexEdgeTriangleMesh::default();
270    let mut h0 = hull[0];
271    let mut h1;
272    if sign > S::zero() {
273      for i in 1..hull.len()-1 {
274        h1 = hull[i];
275        let h2 = hull[i+1];
276        let inserted = mesh.insert (h0, h2, h1);
277        debug_assert!(inserted.is_some());
278      }
279      h0 = sorted[current];
280      let mut i1 = hull.len() - 1;
281      let mut i2 = 0;
282      while i2 < hull.len() {
283        h1 = hull[i1];
284        let h2 = hull[i2];
285        let inserted = mesh.insert (h0, h1, h2);
286        debug_assert!(inserted.is_some());
287        // iter
288        i1 = i2;
289        i2 += 1;
290      }
291    } else {
292      for i in 1..hull.len()-1 {
293        h1 = hull[i];
294        let h2 = hull[i+1];
295        let inserted = mesh.insert (h0, h1, h2);
296        debug_assert!(inserted.is_some());
297      }
298      h0 = sorted[current];
299      let mut i1 = hull.len() - 1;
300      let mut i2 = 0;
301      while i2 < hull.len() {
302        h1 = hull[i1];
303        let h2 = hull[i2];
304        let inserted = mesh.insert (h0, h2, h1);
305        debug_assert!(inserted.is_some());
306        // iter
307        i1 = i2;
308        i2 += 1;
309      }
310    }
311    let mut terminator : Vec <[u32; 2]> = Vec::new();
312    current += 1;
313    while current < sorted.len() {
314      let vertex = mesh.get_vertex (h0).unwrap();
315      h1 = sorted[current];
316      let mut visible = VecDeque::<TriangleKey>::new();
317      let mut visited = BTreeSet::<TriangleKey>::new();
318      for triangle_key in vertex.adjacent_triangles().iter() {
319        let triangle = mesh.get_triangle (triangle_key).unwrap();
320        let sign = plane_side (
321          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2], h1);
322        if sign > S::zero() {
323          visible.push_back (*triangle_key);
324          visited.insert (*triangle_key);
325          break
326        }
327      }
328      debug_assert!(visible.len() > 0);
329      debug_assert!(terminator.is_empty());
330      while visible.len() > 0 {
331        let triangle_key = visible.pop_front().unwrap();
332        let triangle = mesh.get_triangle (&triangle_key).copied().unwrap();
333        for (i, adjacent_key) in triangle.adjacent_triangles().iter().enumerate() {
334          if let Some (key) = adjacent_key {
335            let adjacent = mesh.get_triangle (key).unwrap();
336            if plane_side (
337              adjacent.vertices()[0], adjacent.vertices()[1], adjacent.vertices()[2], h1
338            ) <= S::zero() {
339              terminator.push (
340                [triangle.vertices()[i], triangle.vertices()[(i + 1) % 3]]);
341            } else if visited.insert (*key) {
342              visible.push_back (*key);
343            }
344          }
345        }
346        visited.remove (&triangle_key);
347        let removed = mesh.remove (
348          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2]);
349        debug_assert!(removed);
350      }
351      // TODO: remove expect if clippy issue #15119 is resolved
352      #[expect(clippy::iter_with_drain)]
353      for edge in terminator.drain(..) {
354        let inserted = mesh.insert (edge[0], edge[1], h1);
355        debug_assert!(inserted.is_some());
356      }
357      h0 = h1;
358      current += 1;
359    }
360    let points = mesh.vertices().keys().copied().map (get_point).collect();
361    Some (Hull3 { points })
362  }
363
364  pub fn points (&self) -> &[Point3 <S>] {
365    &self.points
366  }
367}
368
369#[cfg(test)]
370mod tests {
371  use super::*;
372
373  #[test]
374  fn hull2() {
375    use crate::*;
376    // dedup unique points
377    let points : Vec <Point2 <f32>> = [
378      [ 1.0,  1.0],
379      [ 1.0,  1.0]
380    ].map (Point2::from).into_iter().collect();
381    let hull = Hull2::from_points (points.as_slice()).unwrap();
382    assert_eq!(hull.points(), &[[1.0, 1.0].into()]);
383    // interior point at origin is excluded
384    let points : Vec <Point2 <f32>> = [
385      [ 0.0,  0.0],
386      [ 1.0,  3.0],
387      [ 2.0, -3.0],
388      [-3.0, -1.0]
389    ].map (Point2::from).into_iter().collect();
390    let hull = Hull2::from_points (points.as_slice()).unwrap();
391    // points are in counter-clockwise order
392    let points : Vec <Point2 <f32>> = [
393      [ 2.0, -3.0],
394      [ 1.0,  3.0],
395      [-3.0, -1.0]
396    ].map (Point2::from).into_iter().collect();
397    assert_eq!(hull.points(), points);
398    // colinear point on edge is excluded
399    let points : Vec <Point2 <f32>> = [
400      [ 0.0,  2.0],
401      [-2.0, -2.0],
402      [ 0.0, -2.0],
403      [ 2.0, -2.0]
404    ].map (Point2::from).into_iter().collect();
405    let hull = Hull2::from_points (points.as_slice()).unwrap();
406    // points are in counter-clockwise order
407    let points : Vec <Point2 <f32>> = [
408      [-2.0, -2.0],
409      [ 2.0, -2.0],
410      [ 0.0,  2.0]
411    ].map (Point2::from).into_iter().collect();
412    assert_eq!(hull.points(), points);
413    // multiple edge and interior points are excluded
414    let points : Vec <Point2 <f32>> = [
415      // perimeter points
416      [ 0.0,  6.0],
417      [ 1.0,  5.0],
418      [ 2.0,  4.0],
419      [ 3.0,  3.0],
420      [ 3.0,  1.0],
421      [ 3.0, -1.0],
422      [ 3.0, -3.0],
423      [ 1.0, -3.0],
424      [-1.0, -3.0],
425      [-3.0, -3.0],
426      [-3.0, -1.0],
427      [-3.0,  1.0],
428      [-3.0,  3.0],
429      [-2.0,  4.0],
430      [-1.0,  5.0],
431      // interior points
432      [-1.0,  2.0],
433      [ 2.0, -1.0],
434      [ 0.0,  3.0],
435      [-2.0, -2.0]
436    ].map (Point2::from).into_iter().collect();
437    let hull = Hull2::from_points (points.as_slice()).unwrap();
438    // points are in counter-clockwise order
439    let points : Vec <Point2 <f32>> = [
440      [-3.0, -3.0],
441      [ 3.0, -3.0],
442      [ 3.0,  3.0],
443      [ 0.0,  6.0],
444      [-3.0,  3.0]
445    ].map (Point2::from).into_iter().collect();
446    assert_eq!(hull.points(), points);
447  }
448
449  #[test]
450  fn hull3() {
451    // dedup 0 dimensional
452    let points : Vec <Point3 <f32>> = [
453      [-1.0, -4.0, -1.0],
454      [-1.0, -4.0, -1.0]
455    ].map (Point3::from).into_iter().collect();
456    let hull = Hull3::from_points (points.as_slice()).unwrap();
457    assert_eq!(hull.points(), &[
458      [-1.0, -4.0, -1.0]
459    ].map (Into::into));
460    // 1 dimensional endpoints
461    let points : Vec <Point3 <f32>> = [
462      [-1.0, -4.0, -1.0],
463      [ 0.0,  0.0,  0.0],
464      [ 1.0,  4.0,  1.0]
465    ].map (Point3::from).into_iter().collect();
466    let hull = Hull3::from_points (points.as_slice()).unwrap();
467    assert_eq!(hull.points(), &[
468      [-1.0, -4.0, -1.0],
469      [ 1.0,  4.0,  1.0]
470    ].map (Into::into));
471    // 2 dimensional
472    let points : Vec <Point3 <f32>> = [
473      [-1.0, -4.0, 0.0],
474      [ 2.0,  2.0, 0.0],
475      [-1.0, -1.0, 0.0],
476      [-4.0, -1.0, 0.0],
477      [ 0.0,  2.0, 0.0]
478    ].map (Point3::from).into_iter().collect();
479    let hull = Hull3::from_points (points.as_slice()).unwrap();
480    assert_eq!(hull.points(), &[
481      [-1.0, -4.0, 0.0],
482      [ 2.0,  2.0, 0.0],
483      [ 0.0,  2.0, 0.0],
484      [-4.0, -1.0, 0.0]
485    ].map (Into::into));
486    // already a hull
487    let points : Vec <Point3 <f32>> = [
488      [-1.0, -4.0, -1.0],
489      [ 2.0,  2.0,  2.0],
490      [-4.0, -1.0,  2.0],
491      [ 0.0,  2.0, -3.0]
492    ].map (Point3::from).into_iter().collect();
493    let hull = Hull3::from_points (points.as_slice()).unwrap();
494    assert_eq!(hull.points(), &[
495      [-1.0, -4.0, -1.0],
496      [ 2.0,  2.0,  2.0],
497      [-4.0, -1.0,  2.0],
498      [ 0.0,  2.0, -3.0]
499    ].map (Into::into));
500    // removes interior points
501    let points : Vec <Point3 <f32>> = [
502      [-1.0, -4.0, -1.0],
503      [ 2.0,  2.0,  2.0],
504      [-4.0, -1.0,  2.0],
505      [ 0.0,  2.0, -3.0],
506      [ 0.1,  0.2,  0.3],
507      [-0.1, -0.2, -0.3],
508      [-0.1,  0.2,  0.3]
509    ].map (Point3::from).into_iter().collect();
510    let hull = Hull3::from_points (points.as_slice()).unwrap();
511    assert_eq!(hull.points(), &[
512      [-1.0, -4.0, -1.0],
513      [ 2.0,  2.0,  2.0],
514      [-4.0, -1.0,  2.0],
515      [ 0.0,  2.0, -3.0]
516    ].map (Into::into));
517  }
518}