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::{self, 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    Self::from_points_with_mesh (points).map (|x| x.0)
133  }
134
135  pub fn from_points_with_mesh (points : &[Point3 <S>])
136    -> Option <(Self, VertexEdgeTriangleMesh)>
137  where S : Real + approx::RelativeEq {
138    // code adapted from GeometricTools:
139    // <https://github.com/davideberly/GeometricTools/blob/f78dd0b65bc3a0a4723586de6991dd2c339b08ad/GTE/Mathematics/ConvexHull3.h>
140    // TODO: multi-threaded
141    let point_hull = |points : Vec <Point3 <S>>| {
142      debug_assert_eq!(points.len(), 1);
143      ( Hull3 { points },
144        VertexEdgeTriangleMesh::with_vertex (edge_triangle::Vertex::new (0))
145      )
146    };
147    let line_hull = |points : Vec <Point3 <S>>| {
148      debug_assert_eq!(points.len(), 2);
149      ( Hull3 { points },
150        VertexEdgeTriangleMesh::with_edge (edge_triangle::Edge::new (0, 1))
151      )
152    };
153    match points.len() {
154      0 => return None,
155      n if n < 3 => {
156        let mut points = points.to_vec();
157        points.dedup();
158        match points.len() {
159          1 => return Some (point_hull (points)),
160          2 => return Some (line_hull (points)),
161          _ => unreachable!()
162        }
163      }
164      _ => {}
165    }
166    let get_point = |i : u32| points[i as usize];
167    let collect_points = |indices : &[u32]|
168      indices.iter().copied().map (get_point).collect();
169    let sorted = {
170      #[expect(clippy::cast_possible_truncation)]
171      let mut sorted = (0..points.len() as u32).collect::<Vec<_>>();
172      sorted.sort_by (|a, b|
173        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
174      sorted.dedup_by_key (|i| get_point(*i));
175      sorted
176    };
177    let mut hull = Vec::with_capacity (sorted.len());
178    hull.push (sorted[0]);  // hull[0]
179    let mut current = 1;
180    let mut dimension = 0;
181    // point hull
182    for i in &sorted[current..] {
183      if get_point (hull[0]) != get_point (*i) {
184        dimension = 1;
185        break
186      }
187      current += 1;
188    }
189    debug_assert_eq!(hull.len(), 1);
190    if dimension == 0 {
191      let points = collect_points (hull.as_slice());
192      return Some (point_hull (points))
193    }
194    // linear hull
195    hull.push (sorted[current]);  // hull[1]
196    current += 1;
197    for i in &sorted[current..] {
198      if !colinear_3d (&get_point (hull[0]), &get_point (hull[1]), &get_point (*i)) {
199        dimension = 2;
200        break
201      }
202      hull.push (sorted[current]);
203      current += 1;
204    }
205    if hull.len() > 2 {
206      // keep endpoints
207      hull.sort_by (|a, b|
208        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
209      hull.drain (1..hull.len()-1);
210    }
211    debug_assert_eq!(hull.len(), 2);
212    if dimension == 1 {
213      let points = collect_points (hull.as_slice());
214      return Some (line_hull (points))
215    }
216    // planar hull
217    hull.push (sorted[current]);  // hull[2]
218    current += 1;
219    while current < sorted.len() {
220      if !coplanar_3d (
221        &get_point (hull[0]), &get_point (hull[1]), &get_point (hull[2]),
222        &get_point (sorted[current])
223      ) {
224        dimension = 3;
225        break
226      }
227      hull.push (sorted[current]);
228      current += 1;
229    }
230    if hull.len() > 3 {
231      // compute planar convex hull
232      let v0     = get_point (hull[0]);
233      let v1     = get_point (hull[1]);
234      let v2     = get_point (hull[2]);
235      let diff1  = v1 - v0;
236      let diff2  = v2 - v0;
237      let mut normal = diff1.cross (diff2);
238      let signs  = normal.sigvec();
239      let c;
240      debug_assert_eq!(signs.len(), 3);
241      normal = normal.map (|s| s.abs());
242      #[expect(clippy::collapsible_else_if)]
243      if normal.x > normal.y {
244        if normal.x > normal.z {
245          if signs[0] > S::zero() {
246            c = [1, 2];
247          } else {
248            c = [2, 1];
249          }
250        } else {
251          if signs[2] > S::zero() {
252            c = [0, 1];
253          } else {
254            c = [1, 0];
255          }
256        }
257      } else {
258        if normal.y > normal.z {
259          if signs[1] > S::zero() {
260            c = [2, 0];
261          } else {
262            c = [0, 2];
263          }
264        } else {
265          if signs[2] > S::zero() {
266            c = [0, 1];
267          } else {
268            c = [1, 0];
269          }
270        }
271      }
272      let projections = hull.iter().copied()
273        .map (|i| point2 (get_point (i).0[c[0]], get_point (i).0[c[1]]))
274        .collect::<Vec <_>>();
275      let hull2_indices = Hull2::from_points_indices (projections.as_slice()).unwrap();
276      hull = hull2_indices.iter().map (|i| hull[*i as usize]).collect::<Vec <_>>();
277    }
278    if dimension == 2 {
279      let points = collect_points (hull.as_slice());
280      // TODO: the following mesh creation relies on the fact that the points in a 2d
281      // hull are sorted in counter-clockwise order, but it is not necessarily a good
282      // triangulation
283      debug_assert!(points.len() >= 3);
284      let mut mesh = VertexEdgeTriangleMesh::default();
285      #[expect(clippy::cast_possible_truncation)]
286      for i in 1u32..points.len() as u32 - 1 {
287        mesh.insert (0, i, i+1);
288      }
289      return Some ((Hull3 { points }, mesh))
290    }
291    // 3-dimensional hull
292    let plane_side = |a, b, c, d| {
293      let [s0, s1, s2, s3] = [a, b, c, d].map (get_point);
294      let diff1 = s1 - s0;
295      let diff2 = s2 - s0;
296      let diff3 = s3 - s0;
297      diff1.dot (diff2.cross (diff3)).signum_or_zero()
298    };
299    let sign = plane_side (hull[0], hull[1], hull[2], sorted[current]);
300    let mut mesh = VertexEdgeTriangleMesh::default();
301    let mut h0 = hull[0];
302    let mut h1;
303    if sign > S::zero() {
304      for i in 1..hull.len()-1 {
305        h1 = hull[i];
306        let h2 = hull[i+1];
307        let inserted = mesh.insert (h0, h2, h1);
308        debug_assert!(inserted.is_some());
309      }
310      h0 = sorted[current];
311      let mut i1 = hull.len() - 1;
312      let mut i2 = 0;
313      while i2 < hull.len() {
314        h1 = hull[i1];
315        let h2 = hull[i2];
316        let inserted = mesh.insert (h0, h1, h2);
317        debug_assert!(inserted.is_some());
318        // iter
319        i1 = i2;
320        i2 += 1;
321      }
322    } else {
323      for i in 1..hull.len()-1 {
324        h1 = hull[i];
325        let h2 = hull[i+1];
326        let inserted = mesh.insert (h0, h1, h2);
327        debug_assert!(inserted.is_some());
328      }
329      h0 = sorted[current];
330      let mut i1 = hull.len() - 1;
331      let mut i2 = 0;
332      while i2 < hull.len() {
333        h1 = hull[i1];
334        let h2 = hull[i2];
335        let inserted = mesh.insert (h0, h2, h1);
336        debug_assert!(inserted.is_some());
337        // iter
338        i1 = i2;
339        i2 += 1;
340      }
341    }
342    let mut terminator : Vec <[u32; 2]> = Vec::new();
343    current += 1;
344    while current < sorted.len() {
345      let vertex = mesh.get_vertex (h0).unwrap();
346      h1 = sorted[current];
347      let mut visible = VecDeque::<TriangleKey>::new();
348      let mut visited = BTreeSet::<TriangleKey>::new();
349      for triangle_key in vertex.adjacent_triangles().iter() {
350        let triangle = mesh.get_triangle (triangle_key).unwrap();
351        let sign = plane_side (
352          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2], h1);
353        if sign > S::zero() {
354          visible.push_back (*triangle_key);
355          visited.insert (*triangle_key);
356          break
357        }
358      }
359      debug_assert!(visible.len() > 0);
360      debug_assert!(terminator.is_empty());
361      while visible.len() > 0 {
362        let triangle_key = visible.pop_front().unwrap();
363        let triangle = mesh.get_triangle (&triangle_key).copied().unwrap();
364        for (i, adjacent_key) in triangle.adjacent_triangles().iter().enumerate() {
365          if let Some (key) = adjacent_key {
366            let adjacent = mesh.get_triangle (key).unwrap();
367            if plane_side (
368              adjacent.vertices()[0], adjacent.vertices()[1], adjacent.vertices()[2], h1
369            ) <= S::zero() {
370              terminator.push (
371                [triangle.vertices()[i], triangle.vertices()[(i + 1) % 3]]);
372            } else if visited.insert (*key) {
373              visible.push_back (*key);
374            }
375          }
376        }
377        visited.remove (&triangle_key);
378        let removed = mesh.remove (
379          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2]);
380        debug_assert!(removed);
381      }
382      // TODO: remove expect if clippy issue #15119 is resolved
383      #[expect(clippy::iter_with_drain)]
384      for edge in terminator.drain(..) {
385        let inserted = mesh.insert (edge[0], edge[1], h1);
386        debug_assert!(inserted.is_some());
387      }
388      h0 = h1;
389      current += 1;
390    }
391    let points = mesh.vertices().keys().copied().map (get_point).collect();
392    Some ((Hull3 { points }, mesh))
393  }
394
395  pub fn points (&self) -> &[Point3 <S>] {
396    &self.points
397  }
398}
399
400#[cfg(test)]
401mod tests {
402  use super::*;
403
404  #[test]
405  fn hull2() {
406    use crate::*;
407    // dedup unique points
408    let points : Vec <Point2 <f32>> = [
409      [ 1.0,  1.0],
410      [ 1.0,  1.0]
411    ].map (Point2::from).into_iter().collect();
412    let hull = Hull2::from_points (points.as_slice()).unwrap();
413    assert_eq!(hull.points(), &[[1.0, 1.0].into()]);
414    // interior point at origin is excluded
415    let points : Vec <Point2 <f32>> = [
416      [ 0.0,  0.0],
417      [ 1.0,  3.0],
418      [ 2.0, -3.0],
419      [-3.0, -1.0]
420    ].map (Point2::from).into_iter().collect();
421    let hull = Hull2::from_points (points.as_slice()).unwrap();
422    // points are in counter-clockwise order
423    let points : Vec <Point2 <f32>> = [
424      [ 2.0, -3.0],
425      [ 1.0,  3.0],
426      [-3.0, -1.0]
427    ].map (Point2::from).into_iter().collect();
428    assert_eq!(hull.points(), points);
429    // colinear point on edge is excluded
430    let points : Vec <Point2 <f32>> = [
431      [ 0.0,  2.0],
432      [-2.0, -2.0],
433      [ 0.0, -2.0],
434      [ 2.0, -2.0]
435    ].map (Point2::from).into_iter().collect();
436    let hull = Hull2::from_points (points.as_slice()).unwrap();
437    // points are in counter-clockwise order
438    let points : Vec <Point2 <f32>> = [
439      [-2.0, -2.0],
440      [ 2.0, -2.0],
441      [ 0.0,  2.0]
442    ].map (Point2::from).into_iter().collect();
443    assert_eq!(hull.points(), points);
444    // multiple edge and interior points are excluded
445    let points : Vec <Point2 <f32>> = [
446      // perimeter points
447      [ 0.0,  6.0],
448      [ 1.0,  5.0],
449      [ 2.0,  4.0],
450      [ 3.0,  3.0],
451      [ 3.0,  1.0],
452      [ 3.0, -1.0],
453      [ 3.0, -3.0],
454      [ 1.0, -3.0],
455      [-1.0, -3.0],
456      [-3.0, -3.0],
457      [-3.0, -1.0],
458      [-3.0,  1.0],
459      [-3.0,  3.0],
460      [-2.0,  4.0],
461      [-1.0,  5.0],
462      // interior points
463      [-1.0,  2.0],
464      [ 2.0, -1.0],
465      [ 0.0,  3.0],
466      [-2.0, -2.0]
467    ].map (Point2::from).into_iter().collect();
468    let hull = Hull2::from_points (points.as_slice()).unwrap();
469    // points are in counter-clockwise order
470    let points : Vec <Point2 <f32>> = [
471      [-3.0, -3.0],
472      [ 3.0, -3.0],
473      [ 3.0,  3.0],
474      [ 0.0,  6.0],
475      [-3.0,  3.0]
476    ].map (Point2::from).into_iter().collect();
477    assert_eq!(hull.points(), points);
478  }
479
480  #[test]
481  fn hull3() {
482    // dedup 0 dimensional
483    let points = [
484      [-1.0, -4.0, -1.0],
485      [-1.0, -4.0, -1.0]
486    ].map (Point3::<f32>::from);
487    let hull = Hull3::from_points (points.as_slice()).unwrap();
488    assert_eq!(hull.points(), &[
489      [-1.0, -4.0, -1.0]
490    ].map (Into::into));
491    // 1 dimensional endpoints
492    let points = [
493      [-1.0, -4.0, -1.0],
494      [ 0.0,  0.0,  0.0],
495      [ 1.0,  4.0,  1.0]
496    ].map (Point3::<f32>::from);
497    let hull = Hull3::from_points (points.as_slice()).unwrap();
498    assert_eq!(hull.points(), &[
499      [-1.0, -4.0, -1.0],
500      [ 1.0,  4.0,  1.0]
501    ].map (Into::into));
502    // 2 dimensional
503    let points = [
504      [-1.0, -4.0, 0.0],
505      [ 2.0,  2.0, 0.0],
506      [-1.0, -1.0, 0.0],
507      [-4.0, -1.0, 0.0],
508      [ 0.0,  2.0, 0.0]
509    ].map (Point3::<f32>::from);
510    let hull = Hull3::from_points (points.as_slice()).unwrap();
511    assert_eq!(hull.points(), &[
512      [-1.0, -4.0, 0.0],
513      [ 2.0,  2.0, 0.0],
514      [ 0.0,  2.0, 0.0],
515      [-4.0, -1.0, 0.0]
516    ].map (Into::into));
517    // already a hull
518    let points = [
519      [-1.0, -4.0, -1.0],
520      [ 2.0,  2.0,  2.0],
521      [-4.0, -1.0,  2.0],
522      [ 0.0,  2.0, -3.0]
523    ].map (Point3::<f32>::from);
524    let hull = Hull3::from_points (points.as_slice()).unwrap();
525    assert_eq!(hull.points(), &[
526      [-1.0, -4.0, -1.0],
527      [ 2.0,  2.0,  2.0],
528      [-4.0, -1.0,  2.0],
529      [ 0.0,  2.0, -3.0]
530    ].map (Into::into));
531    let points = [
532      [ 1.0, -1.0, -1.0],
533      [ 1.0, -1.0,  1.0],
534      [ 1.0,  1.0, -1.0],
535      [ 1.0,  1.0,  1.0],
536      [-1.0, -1.0, -1.0],
537      [-1.0, -1.0,  1.0],
538      [-1.0,  1.0, -1.0],
539      [-1.0,  1.0,  1.0]
540    ].map (Point3::<f32>::from);
541    let hull = Hull3::from_points (points.as_slice()).unwrap();
542    assert_eq!(hull.points(), points.as_slice());
543    // removes interior points
544    let points = [
545      [-1.0, -4.0, -1.0],
546      [ 2.0,  2.0,  2.0],
547      [-4.0, -1.0,  2.0],
548      [ 0.0,  2.0, -3.0],
549      [ 0.1,  0.2,  0.3],
550      [-0.1, -0.2, -0.3],
551      [-0.1,  0.2,  0.3]
552    ].map (Point3::<f32>::from);
553    let hull = Hull3::from_points (points.as_slice()).unwrap();
554    assert_eq!(hull.points(), &[
555      [-1.0, -4.0, -1.0],
556      [ 2.0,  2.0,  2.0],
557      [-4.0, -1.0,  2.0],
558      [ 0.0,  2.0, -3.0]
559    ].map (Into::into));
560  }
561}