math_utils/geometry/primitive/
hull.rs

1//! Convex hulls
2
3use std;
4
5use crate::*;
6
7/// 2D convex hull
8#[derive(Clone, Debug, PartialEq)]
9pub struct Hull2 <S> {
10  /// Set of unique points sorted in counter-clockwise order for efficient
11  /// minimum bounding box algorithm
12  points : Vec <Point2 <S>>
13}
14
15/// 3D convex hull
16#[derive(Clone, Debug, PartialEq)]
17pub struct Hull3 <S> {
18  points : Vec <Point3 <S>>
19}
20
21impl <S> Hull2 <S> {
22  /// Create a new 2D convex hull from a bag of points.
23  ///
24  /// Uses Graham scan algorithm.
25  ///
26  /// Input must contain at least 1 point:
27  /// ```
28  /// # use math_utils::geometry::Hull2;
29  /// assert!(Hull2::<f32>::from_points (&[]).is_none());
30  /// ```
31  pub fn from_points (points : &[Point2 <S>]) -> Option <Self> where S : Real {
32    // code adapted from scirs2-spatial:
33    // <https://github.com/cool-japan/scirs/blob/a176b462aca55e73bd4b25eea83aad10a9f4a2b0/scirs2-spatial/src/convex_hull.rs>
34    use std::cmp::Ordering;
35    match points.len() {
36      0 => return None,
37      1 => return Some (Hull2 { points: points.to_vec() }),
38      2 => {
39        let mut points = points.to_vec();
40        points.dedup();
41        return Some (Hull2 { points})
42      }
43      _ => {} // continue
44    }
45    let mut indexed_points = points.iter().cloned().enumerate()
46      .collect::<Vec <(usize, Point2 <S>)>>();
47    // find bottom-most (lowest y-coordinate), then left-most x
48    let start_index = indexed_points.iter().min_by (|a, b|{
49      let cmp = a.1.0.y.partial_cmp (&b.1.0.y).unwrap();
50      if cmp == Ordering::Equal {
51        a.1.0.x.partial_cmp (&b.1.0.x).unwrap()
52      } else {
53        cmp
54      }
55    }).unwrap().0;
56    let start_point = indexed_points[start_index].1;
57    // sort points by polar angle with respect to start point
58    indexed_points.sort_by (|a, b| {
59      if a.0 == start_index {
60        return Ordering::Less
61      }
62      if b.0 == start_index {
63        return Ordering::Greater
64      }
65      let angle_a = (a.1.0.y - start_point.0.y)
66        .atan2 (a.1.0.x - start_point.0.x);
67      let angle_b = (b.1.0.y - start_point.0.y)
68        .atan2 (b.1.0.x - start_point.0.x);
69      let angle_cmp = angle_a.partial_cmp (&angle_b).unwrap();
70      if angle_cmp == Ordering::Equal {
71        // if angles are equal sort by distance
72        let dist_a = (a.1.0.x - start_point.0.x).powi (2) +
73          (a.1.0.y - start_point.0.y).powi (2);
74        let dist_b = (b.1.0.x - start_point.0.x).powi (2) +
75          (b.1.0.y - start_point.0.y).powi (2);
76        dist_a.partial_cmp (&dist_b).unwrap()
77      } else {
78        angle_cmp
79      }
80    });
81    // graham scan
82    let mut stack : Vec <usize> = Vec::new();
83    for (point_index, point) in indexed_points {
84      while stack.len() >= 2 {
85        let top = stack[stack.len() - 1];
86        let second = stack[stack.len() -2];
87        let p1 = points[second];
88        let p2 = points[top];
89        let p3 = point;
90        let det = Matrix2 {
91          cols: vector2 (p2 - p1, p3 - p1)
92        }.determinant();
93        if det <= S::zero() {
94          stack.pop();
95        } else {
96          break
97        }
98      }
99      stack.push (point_index)
100    }
101    let p = points;
102    let points = stack.iter().map (|i| p[*i]).collect();
103    Some (Hull2 { points })
104  }
105
106  /// Points sorted in counter-clockwise order
107  pub fn points (&self) -> &[Point2 <S>] {
108    &self.points
109  }
110}
111
112impl <S> Hull3 <S> {
113  pub fn from_points (_points : &[Point3 <S>]) -> Option <Self> where S : Real {
114    // code adapted from GeometricTools:
115    // <https://github.com/davideberly/GeometricTools/blob/f78dd0b65bc3a0a4723586de6991dd2c339b08ad/GTE/Mathematics/ConvexHull3.h>
116    unimplemented!("TODO")
117  }
118
119  pub fn points (&self) -> &[Point3 <S>] {
120    &self.points
121  }
122}
123
124#[cfg(test)]
125mod tests {
126  use super::*;
127  #[test]
128  fn hull2() {
129    use crate::*;
130    // dedup unique points
131    let points : Vec <Point2 <f32>> = [
132      [ 1.0,  1.0],
133      [ 1.0,  1.0]
134    ].map (Point2::from).into_iter().collect();
135    let hull = Hull2::from_points (points.as_slice()).unwrap();
136    assert_eq!(hull.points(), &[[1.0, 1.0].into()]);
137    // interior point at origin is excluded
138    let points : Vec <Point2 <f32>> = [
139      [ 0.0,  0.0],
140      [ 1.0,  3.0],
141      [ 2.0, -3.0],
142      [-3.0, -1.0]
143    ].map (Point2::from).into_iter().collect();
144    let hull = Hull2::from_points (points.as_slice()).unwrap();
145    // points are in counter-clockwise order
146    let points : Vec <Point2 <f32>> = [
147      [ 2.0, -3.0],
148      [ 1.0,  3.0],
149      [-3.0, -1.0]
150    ].map (Point2::from).into_iter().collect();
151    assert_eq!(hull.points(), points);
152    // colinear point on edge is excluded
153    let points : Vec <Point2 <f32>> = [
154      [ 0.0,  2.0],
155      [-2.0, -2.0],
156      [ 0.0, -2.0],
157      [ 2.0, -2.0]
158    ].map (Point2::from).into_iter().collect();
159    let hull = Hull2::from_points (points.as_slice()).unwrap();
160    // points are in counter-clockwise order
161    let points : Vec <Point2 <f32>> = [
162      [-2.0, -2.0],
163      [ 2.0, -2.0],
164      [ 0.0,  2.0]
165    ].map (Point2::from).into_iter().collect();
166    assert_eq!(hull.points(), points);
167    // multiple edge and interior points are excluded
168    let points : Vec <Point2 <f32>> = [
169      // perimeter points
170      [ 0.0,  6.0],
171      [ 1.0,  5.0],
172      [ 2.0,  4.0],
173      [ 3.0,  3.0],
174      [ 3.0,  1.0],
175      [ 3.0, -1.0],
176      [ 3.0, -3.0],
177      [ 1.0, -3.0],
178      [-1.0, -3.0],
179      [-3.0, -3.0],
180      [-3.0, -1.0],
181      [-3.0,  1.0],
182      [-3.0,  3.0],
183      [-2.0,  4.0],
184      [-1.0,  5.0],
185      // interior points
186      [-1.0,  2.0],
187      [ 2.0, -1.0],
188      [ 0.0,  3.0],
189      [-2.0, -2.0]
190    ].map (Point2::from).into_iter().collect();
191    let hull = Hull2::from_points (points.as_slice()).unwrap();
192    // points are in counter-clockwise order
193    let points : Vec <Point2 <f32>> = [
194      [-3.0, -3.0],
195      [ 3.0, -3.0],
196      [ 3.0,  3.0],
197      [ 0.0,  6.0],
198      [-3.0,  3.0]
199    ].map (Point2::from).into_iter().collect();
200    assert_eq!(hull.points(), points);
201  }
202}