Skip to main content

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, Edge, Triangle, TriangleKey};
12
13/// 2D convex hull
14#[derive(Clone, Debug, Eq, PartialEq)]
15#[cfg_attr(feature = "derive_serdes", derive(Serialize, Deserialize))]
16pub struct Hull2 <S> {
17  /// Set of unique points sorted in counter-clockwise order for efficient
18  /// minimum bounding box algorithm
19  points : Vec <Point2 <S>>
20}
21
22/// 3D convex hull
23#[derive(Clone, Debug, Eq, PartialEq)]
24#[cfg_attr(feature = "derive_serdes", derive(Serialize, Deserialize))]
25pub struct Hull3 <S> {
26  points : Vec <Point3 <S>>
27}
28
29impl <S> Hull2 <S> {
30  /// Create a new 2D convex hull from a bag of points.
31  ///
32  /// Uses Graham scan algorithm.
33  ///
34  /// Input must contain at least 1 point:
35  /// ```
36  /// # use math_utils::geometry::Hull2;
37  /// assert!(Hull2::<f32>::from_points (&[]).is_none());
38  /// ```
39  pub fn from_points (points : &[Point2 <S>]) -> Option <Self> where S : Real {
40    Self::from_points_indices (points).map (|indices|{
41      let points = indices.iter().map (|i| points[*i as usize]).collect();
42      Hull2 { points }
43    })
44  }
45
46  /// Points sorted in counter-clockwise order
47  #[inline]
48  pub fn points (&self) -> &[Point2 <S>] {
49    &self.points
50  }
51
52  #[inline]
53  pub fn numcast <T> (&self) -> Option <Hull2 <T>> where
54    S : num::NumCast + num::ToPrimitive + Copy,
55    T : num::NumCast
56  {
57    let points = self.points.iter().map (|p| p.numcast())
58      .collect::<Option <Vec <Point2 <T>>>>()?;
59    Some (Hull2 { points })
60  }
61
62  #[expect(clippy::missing_asserts_for_indexing)]
63  fn from_points_indices (points : &[Point2 <S>]) -> Option <Vec <u32>> where S : Real {
64    // code adapted from scirs2-spatial:
65    // <https://github.com/cool-japan/scirs/blob/a176b462aca55e73bd4b25eea83aad10a9f4a2b0/scirs2-spatial/src/convex_hull.rs>
66    use std::cmp::Ordering;
67    match points.len() {
68      0 => return None,
69      1 => return Some (vec![0]),
70      2 => {
71        let v = if points[0] == points[1] {
72          vec![0]
73        } else {
74          vec![0, 1]
75        };
76        return Some (v)
77      }
78      _ => {} // continue
79    }
80    let mut indexed_points = points.iter().copied().enumerate()
81      .collect::<Vec <(usize, Point2 <S>)>>();
82    // find bottom-most (lowest y-coordinate), then left-most x
83    let start_index = indexed_points.iter().min_by (|a, b|{
84      let cmp = a.1.0.y.partial_cmp (&b.1.0.y).unwrap();
85      if cmp == Ordering::Equal {
86        a.1.0.x.partial_cmp (&b.1.0.x).unwrap()
87      } else {
88        cmp
89      }
90    }).unwrap().0;
91    let start_point = indexed_points[start_index].1;
92    // sort points by polar angle with respect to start point
93    indexed_points.sort_by (|a, b| {
94      if a.0 == start_index {
95        return Ordering::Less
96      }
97      if b.0 == start_index {
98        return Ordering::Greater
99      }
100      let angle_a = (a.1.0.y - start_point.0.y)
101        .atan2 (a.1.0.x - start_point.0.x);
102      let angle_b = (b.1.0.y - start_point.0.y)
103        .atan2 (b.1.0.x - start_point.0.x);
104      let angle_cmp = angle_a.partial_cmp (&angle_b).unwrap();
105      if angle_cmp == Ordering::Equal {
106        // if angles are equal sort by distance
107        let dist_a = (a.1.0.x - start_point.0.x).powi (2) +
108          (a.1.0.y - start_point.0.y).powi (2);
109        let dist_b = (b.1.0.x - start_point.0.x).powi (2) +
110          (b.1.0.y - start_point.0.y).powi (2);
111        dist_a.partial_cmp (&dist_b).unwrap()
112      } else {
113        angle_cmp
114      }
115    });
116    // graham scan
117    let mut stack : Vec <u32> = Vec::new();
118    for (point_index, point) in indexed_points {
119      while stack.len() >= 2 {
120        let top = stack[stack.len() - 1];
121        let second = stack[stack.len() -2];
122        let p1 = points[second as usize];
123        let p2 = points[top as usize];
124        let p3 = point;
125        let det = Matrix2 {
126          cols: vector2 (p2 - p1, p3 - p1)
127        }.determinant();
128        if det <= S::zero() {
129          stack.pop();
130        } else {
131          break
132        }
133      }
134      #[expect(clippy::cast_possible_truncation)]
135      stack.push (point_index as u32)
136    }
137    Some (stack)
138  }
139}
140impl <S> Primitive <S, Point2 <S>> for Hull2 <S> where
141  S : Real + num::real::Real + std::fmt::Debug + MaybeSerDes
142{
143  fn translate (&mut self, displacement : Vector2 <S>) {
144    self.points.iter_mut().for_each (|p| *p += displacement);
145  }
146  fn scale (&mut self, scale : Positive <S>) {
147    self.points.iter_mut().for_each (|p| p.0 *= *scale);
148  }
149  fn support (&self, direction : NonZero2 <S>) -> (Point2 <S>, S) {
150    support2 (self.points(), direction)
151  }
152}
153impl <S : Real> From <simplex2::Segment <S>> for Hull2 <S> {
154  fn from (segment : simplex2::Segment <S>) -> Self {
155    Hull2::from_points (segment.points().as_slice()).unwrap()
156  }
157}
158impl <S : Real> From <simplex2::Triangle <S>> for Hull2 <S> {
159  fn from (triangle : simplex2::Triangle <S>) -> Self {
160    Hull2::from_points (triangle.points().as_slice()).unwrap()
161  }
162}
163impl <S : Real> From <Simplex2 <S>> for Hull2 <S> {
164  fn from (simplex : Simplex2 <S>) -> Self {
165    match simplex {
166      Simplex2::Segment (segment)   => Hull2::from_points (segment.points().as_slice()),
167      Simplex2::Triangle (triangle) => Hull2::from_points (triangle.points().as_slice())
168    }.unwrap()
169  }
170}
171
172impl <S> Hull3 <S> {
173  pub fn from_points (points : &[Point3 <S>]) -> Option <Self> where
174    S : Real + approx::RelativeEq <Epsilon = S>
175  {
176    Self::from_points_with_mesh (points).map (|x| x.0)
177  }
178
179  pub fn from_points_with_mesh (points : &[Point3 <S>])
180    -> Option <(Self, VertexEdgeTriangleMesh)>
181  where S : Real + approx::RelativeEq <Epsilon = S> {
182    // code adapted from GeometricTools:
183    // <https://github.com/davideberly/GeometricTools/blob/f78dd0b65bc3a0a4723586de6991dd2c339b08ad/GTE/Mathematics/ConvexHull3.h>
184    // with robustness improvements added by Opus 4.7 (sections marked by /****/)
185    // TODO: multi-threaded
186    let point_hull = |points : Vec <Point3 <S>>| {
187      debug_assert_eq!(points.len(), 1);
188      ( Hull3 { points },
189        VertexEdgeTriangleMesh::with_vertex (edge_triangle::Vertex::new (0))
190      )
191    };
192    let line_hull = |points : Vec <Point3 <S>>| {
193      debug_assert_eq!(points.len(), 2);
194      ( Hull3 { points },
195        VertexEdgeTriangleMesh::with_edge (Edge::new (0, 1))
196      )
197    };
198    match points.len() {
199      0 => return None,
200      n if n < 3 => {
201        let mut points = points.to_vec();
202        points.dedup();
203        match points.len() {
204          1 => return Some (point_hull (points)),
205          2 => return Some (line_hull (points)),
206          _ => unreachable!()
207        }
208      }
209      _ => {}
210    }
211    let get_point = |i : u32| points[i as usize];
212    let collect_points = |indices : &[u32]|
213      indices.iter().copied().map (get_point).collect();
214    let sorted = {
215      #[expect(clippy::cast_possible_truncation)]
216      let mut sorted = (0..points.len() as u32).collect::<Vec<_>>();
217      sorted.sort_by (|a, b|
218        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
219      sorted.dedup_by_key (|i| get_point(*i));
220      sorted
221    };
222    let mut hull = Vec::with_capacity (sorted.len());
223    hull.push (sorted[0]);  // hull[0]
224    let mut current = 1;
225    let mut dimension = 0;
226    // point hull
227    for i in &sorted[current..] {
228      if get_point (hull[0]) != get_point (*i) {
229        dimension = 1;
230        break
231      }
232      current += 1;
233    }
234    debug_assert_eq!(hull.len(), 1);
235    if dimension == 0 {
236      let points = collect_points (hull.as_slice());
237      return Some (point_hull (points))
238    }
239    // linear hull
240    hull.push (sorted[current]);  // hull[1]
241    current += 1;
242    for i in &sorted[current..] {
243      if !colinear_3d (get_point (hull[0]), get_point (hull[1]), get_point (*i)) {
244        dimension = 2;
245        break
246      }
247      hull.push (sorted[current]);
248      current += 1;
249    }
250    if hull.len() > 2 {
251      // keep endpoints
252      hull.sort_by (|a, b|
253        Point3::partial_cmp (&get_point (*a), &get_point (*b)).unwrap());
254      hull.drain (1..hull.len()-1);
255    }
256    debug_assert_eq!(hull.len(), 2);
257    if dimension == 1 {
258      let points = collect_points (hull.as_slice());
259      return Some (line_hull (points))
260    }
261    // planar hull
262    hull.push (sorted[current]);  // hull[2]
263    current += 1;
264    while current < sorted.len() {
265      if !coplanar_3d (
266        get_point (hull[0]), get_point (hull[1]), get_point (hull[2]),
267        get_point (sorted[current])
268      ) {
269        //dimension = 3;
270        //break
271        /******************************************************************************/
272        // Robustness: the planar reference (hull[0..=2]) can be poorly conditioned
273        // (near-colinear), causing `coplanar_3d` to give a false negative for
274        // points that actually lie on the plane. Re-test using the
275        // best-conditioned triple from the current planar hull, plus a check for
276        // colinearity with any pair of existing hull points (which guarantees
277        // coplanarity regardless of the plane's representation).
278        let p = get_point (sorted[current]);
279        let mut actually_coplanar = false;
280        // colinear with any pair => on the plane
281        'outer: for i in 0..hull.len() {
282          for j in i+1..hull.len() {
283            if colinear_3d (get_point (hull[i]), get_point (hull[j]), p) {
284              actually_coplanar = true;
285              break 'outer;
286            }
287          }
288        }
289        if !actually_coplanar {
290          // Find the triple (i, j, k) in `hull` that maximizes the normal
291          // magnitude (best-conditioned plane), and re-test coplanarity.
292          let mut best_norm_sq = S::zero();
293          let mut best : Option <(usize, usize, usize)> = None;
294          for i in 0..hull.len() {
295            for j in i+1..hull.len() {
296              for k in j+1..hull.len() {
297                let a = get_point (hull[i]);
298                let b = get_point (hull[j]);
299                let c = get_point (hull[k]);
300                let n = (b - a).cross (c - a);
301                let n_sq = n.magnitude_squared();
302                if n_sq > best_norm_sq {
303                  best_norm_sq = n_sq;
304                  best = Some ((i, j, k));
305                }
306              }
307            }
308          }
309          if let Some ((i, j, k)) = best
310            && coplanar_3d (
311              get_point (hull[i]), get_point (hull[j]), get_point (hull[k]), p
312            )
313          {
314            actually_coplanar = true;
315          }
316        }
317        if !actually_coplanar {
318          dimension = 3;
319          break
320        }
321        /******************************************************************************/
322      }
323      hull.push (sorted[current]);
324      current += 1;
325    }
326    if hull.len() > 3 {
327      /********************************************************************************/
328      // Pick a well-conditioned triple to define the projection plane. Using
329      // hull[0..=2] as a reference is fragile when those points are nearly
330      // colinear, which can produce a near-zero plane normal and ill-defined
331      // projection axes.
332      let (i_ref, j_ref, k_ref) = {
333        let mut best_norm_sq = S::zero();
334        let mut best = (0, 1, 2);
335        for i in 0..hull.len() {
336          for j in i+1..hull.len() {
337            for k in j+1..hull.len() {
338              let a = get_point (hull[i]);
339              let b = get_point (hull[j]);
340              let c = get_point (hull[k]);
341              let n = (b - a).cross (c - a);
342              let n_sq = n.magnitude_squared();
343              if n_sq > best_norm_sq {
344                best_norm_sq = n_sq;
345                best = (i, j, k);
346              }
347            }
348          }
349        }
350        best
351      };
352      /********************************************************************************/
353      // compute planar convex hull
354      let v0     = get_point (hull[i_ref]);
355      let v1     = get_point (hull[j_ref]);
356      let v2     = get_point (hull[k_ref]);
357      let diff1  = v1 - v0;
358      let diff2  = v2 - v0;
359      let mut normal = diff1.cross (diff2);
360      let signs  = normal.sigvec();
361      let c;
362      debug_assert_eq!(signs.len(), 3);
363      normal = normal.map (|s| s.abs());
364      if normal.x > normal.y {
365        if normal.x > normal.z {
366          if signs[0] > S::zero() {
367            c = [1, 2];
368          } else {
369            c = [2, 1];
370          }
371        } else {
372          if signs[2] > S::zero() {
373            c = [0, 1];
374          } else {
375            c = [1, 0];
376          }
377        }
378      } else {
379        if normal.y > normal.z {
380          if signs[1] > S::zero() {
381            c = [2, 0];
382          } else {
383            c = [0, 2];
384          }
385        } else {
386          if signs[2] > S::zero() {
387            c = [0, 1];
388          } else {
389            c = [1, 0];
390          }
391        }
392      }
393      let projections = hull.iter().copied()
394        .map (|i| point2 (get_point (i).0[c[0]], get_point (i).0[c[1]]))
395        .collect::<Vec <_>>();
396      let hull2_indices = Hull2::from_points_indices (projections.as_slice()).unwrap();
397      hull = hull2_indices.iter().map (|i| hull[*i as usize]).collect::<Vec <_>>();
398    }
399    if dimension == 2 {
400      let points = collect_points (hull.as_slice());
401      // TODO: the following mesh creation relies on the fact that the points in a 2d
402      // hull are sorted in counter-clockwise order, but it is not necessarily a good
403      // triangulation
404      debug_assert!(points.len() >= 3);
405      let mut mesh = VertexEdgeTriangleMesh::default();
406      #[expect(clippy::cast_possible_truncation)]
407      for i in 1u32..points.len() as u32 - 1 {
408        mesh.insert (0, i, i+1);
409      }
410      return Some ((Hull3 { points }, mesh))
411    }
412    // 3-dimensional hull
413    let plane_side = |a, b, c, d| {
414      let [s0, s1, s2, s3] = [a, b, c, d].map (get_point);
415      plane_side_3d (s0, s1, s2, s3)
416    };
417    let sign = plane_side (hull[0], hull[1], hull[2], sorted[current]);
418    let mut mesh = VertexEdgeTriangleMesh::default();
419    let mut h0 = hull[0];
420    let mut h1;
421    if sign > S::zero() {
422      for i in 1..hull.len()-1 {
423        h1 = hull[i];
424        let h2 = hull[i+1];
425        let inserted = mesh.insert (h0, h2, h1);
426        debug_assert!(inserted.is_some());
427      }
428      h0 = sorted[current];
429      let mut i1 = hull.len() - 1;
430      let mut i2 = 0;
431      while i2 < hull.len() {
432        h1 = hull[i1];
433        let h2 = hull[i2];
434        let inserted = mesh.insert (h0, h1, h2);
435        debug_assert!(inserted.is_some());
436        // iter
437        i1 = i2;
438        i2 += 1;
439      }
440    } else {
441      for i in 1..hull.len()-1 {
442        h1 = hull[i];
443        let h2 = hull[i+1];
444        let inserted = mesh.insert (h0, h1, h2);
445        debug_assert!(inserted.is_some());
446      }
447      h0 = sorted[current];
448      let mut i1 = hull.len() - 1;
449      let mut i2 = 0;
450      while i2 < hull.len() {
451        h1 = hull[i1];
452        let h2 = hull[i2];
453        let inserted = mesh.insert (h0, h2, h1);
454        debug_assert!(inserted.is_some());
455        // iter
456        i1 = i2;
457        i2 += 1;
458      }
459    }
460    let mut terminator : Vec <[u32; 2]>    = Vec::new();
461    let mut to_remove  : Vec <TriangleKey> = Vec::new();
462    current += 1;
463    while current < sorted.len() {
464      let vertex = mesh.get_vertex (h0).unwrap();
465      h1 = sorted[current];
466      let mut visible = VecDeque::<TriangleKey>::new();
467      let mut visited = BTreeSet::<TriangleKey>::new();
468      for triangle_key in vertex.adjacent_triangles().iter() {
469        let triangle = mesh.get_triangle (triangle_key).unwrap();
470        let sign = plane_side (
471          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2], h1);
472        if sign > S::zero() {
473          visible.push_back (*triangle_key);
474          visited.insert (*triangle_key);
475          break
476        }
477      }
478      debug_assert!(visible.len() > 0);
479      debug_assert!(terminator.is_empty());
480      /********************************************************************************/
481      debug_assert!(to_remove.is_empty());
482      // First pass: gather the visible region and the terminator silhouette
483      // *without* mutating the mesh. This allows us to detect degenerate
484      // configurations (e.g., a terminator edge colinear with h1, which would
485      // produce a degenerate new triangle) and bail out cleanly.
486      /********************************************************************************/
487      while visible.len() > 0 {
488        let triangle_key = visible.pop_front().unwrap();
489        let triangle = mesh.get_triangle (&triangle_key).copied().unwrap();
490        to_remove.push (triangle_key);
491        for (i, adjacent_key) in triangle.adjacent_triangles().iter().enumerate() {
492          if let Some (key) = adjacent_key {
493            let adjacent = mesh.get_triangle (key).unwrap();
494            if plane_side (
495              adjacent.vertices()[0], adjacent.vertices()[1], adjacent.vertices()[2], h1
496            ) <= S::zero() {
497              terminator.push (
498                [triangle.vertices()[i], triangle.vertices()[(i + 1) % 3]]);
499            } else if visited.insert (*key) {
500              visible.push_back (*key);
501            }
502          }
503        }
504        //visited.remove (&triangle_key);
505      /********************************************************************************/
506        // Note: triangle remains in `visited` to prevent it from being
507        // re-enqueued via cycles, since the mesh isn't being mutated here.
508      }
509      // Robustness: floating-point misclassifications in `plane_side` can
510      // produce a silhouette edge that is colinear with h1. Inserting the
511      // corresponding new triangle (edge_a, edge_b, h1) would yield a
512      // degenerate (colinear) triangle. Properly absorbing the offending
513      // adjacent triangles into the visible region can break the silhouette's
514      // simple-cycle topology, so as a safe fallback we skip h1 entirely
515      // (leave the mesh unchanged and proceed). h1 is then not added as a
516      // hull vertex; this is a small loss of fidelity in pathological
517      // configurations but preserves a valid manifold mesh with no
518      // degenerate triangles.
519      let any_degenerate = terminator.iter().any (|edge|
520        colinear_3d (get_point (edge[0]), get_point (edge[1]), get_point (h1)));
521      if any_degenerate {
522        terminator.clear();
523        to_remove.clear();
524        current += 1;
525        continue
526      }
527      // Commit the visible region's removal and insert the new fan.
528      // TODO: remove expect if clippy issue #15119 is resolved
529      #[expect(clippy::iter_with_drain)]
530      for triangle_key in to_remove.drain(..) {
531        let triangle = *mesh.get_triangle (&triangle_key).unwrap();
532      /********************************************************************************/
533        let removed = mesh.remove (
534          triangle.vertices()[0], triangle.vertices()[1], triangle.vertices()[2]);
535        debug_assert!(removed);
536      }
537      // TODO: remove expect if clippy issue #15119 is resolved
538      #[expect(clippy::iter_with_drain)]
539      for edge in terminator.drain(..) {
540        let inserted = mesh.insert (edge[0], edge[1], h1);
541        debug_assert!(inserted.is_some());
542      }
543      h0 = h1;
544      current += 1;
545    }
546    let points = mesh.vertices().keys().copied().map (get_point).collect();
547    mesh.reindex();
548    Some ((Hull3 { points }, mesh))
549  }
550  #[inline]
551  pub fn points (&self) -> &[Point3 <S>] {
552    &self.points
553  }
554  #[inline]
555  pub fn edge (&self, edge : &Edge) -> Segment3 <S> where S : OrderedRing {
556    Segment3::from_array (edge.vertices().map(|i| self.points[i as usize])).unwrap()
557  }
558  #[inline]
559  pub fn triangle (&self, triangle : &Triangle) -> Triangle3 <S> where
560    S : OrderedField + Sqrt + approx::RelativeEq <Epsilon = S>
561  {
562    Triangle3::from_array (triangle.vertices().map(|i| self.points[i as usize])).unwrap()
563  }
564  #[inline]
565  pub fn numcast <T> (&self) -> Option <Hull3 <T>> where
566    S : num::NumCast + num::ToPrimitive + Copy,
567    T : num::NumCast
568  {
569    let points = self.points.iter().map (|p| p.numcast())
570      .collect::<Option <Vec <Point3 <T>>>>()?;
571    Some (Hull3 { points })
572  }
573
574}
575impl <S> Primitive <S, Point3 <S>> for Hull3 <S> where
576  S : Real + num::real::Real + MaybeSerDes
577{
578  fn translate (&mut self, displacement : Vector3 <S>) {
579    self.points.iter_mut().for_each (|p| *p += displacement);
580  }
581  fn scale (&mut self, scale : Positive <S>) {
582    self.points.iter_mut().for_each (|p| p.0 *= *scale);
583  }
584  fn support (&self, direction : NonZero3 <S>) -> (Point3 <S>, S) {
585    support3 (self.points(), direction)
586  }
587}
588impl <S> From <simplex3::Segment <S>> for Hull3 <S> where
589  S : Real + approx::RelativeEq <Epsilon = S>
590{
591  fn from (segment : simplex3::Segment <S>) -> Self {
592    Hull3::from_points (segment.points().as_slice()).unwrap()
593  }
594}
595impl <S> From <simplex3::Triangle <S>> for Hull3 <S> where
596  S : Real + approx::RelativeEq <Epsilon = S>
597{
598  fn from (triangle : simplex3::Triangle <S>) -> Self {
599    Hull3::from_points (triangle.points().as_slice()).unwrap()
600  }
601}
602impl <S> From <simplex3::Tetrahedron <S>> for Hull3 <S> where
603  S : Real + approx::RelativeEq <Epsilon = S>
604{
605  fn from (tetrahedron : simplex3::Tetrahedron <S>) -> Self {
606    Hull3::from_points (tetrahedron.points().as_slice()).unwrap()
607  }
608}
609impl <S> From <Simplex3 <S>> for Hull3 <S> where
610  S : Real + approx::RelativeEq <Epsilon = S>
611{
612  fn from (simplex : Simplex3 <S>) -> Self {
613    match simplex {
614      Simplex3::Segment (segment) =>
615        Hull3::from_points (segment.points().as_slice()),
616      Simplex3::Triangle (triangle) =>
617        Hull3::from_points (triangle.points().as_slice()),
618      Simplex3::Tetrahedron (tetrahedron) =>
619        Hull3::from_points (tetrahedron.points().as_slice())
620    }.unwrap()
621  }
622}
623
624#[cfg(test)]
625mod tests {
626  use super::*;
627
628  #[test]
629  fn hull2() {
630    use crate::*;
631    // dedup unique points
632    let points : Vec <Point2 <f32>> = [
633      [ 1.0,  1.0],
634      [ 1.0,  1.0]
635    ].map (Point2::from).into_iter().collect();
636    let hull = Hull2::from_points (points.as_slice()).unwrap();
637    assert_eq!(hull.points(), &[[1.0, 1.0].into()]);
638    // interior point at origin is excluded
639    let points : Vec <Point2 <f32>> = [
640      [ 0.0,  0.0],
641      [ 1.0,  3.0],
642      [ 2.0, -3.0],
643      [-3.0, -1.0]
644    ].map (Point2::from).into_iter().collect();
645    let hull = Hull2::from_points (points.as_slice()).unwrap();
646    // points are in counter-clockwise order
647    let points : Vec <Point2 <f32>> = [
648      [ 2.0, -3.0],
649      [ 1.0,  3.0],
650      [-3.0, -1.0]
651    ].map (Point2::from).into_iter().collect();
652    assert_eq!(hull.points(), points);
653    // colinear point on edge is excluded
654    let points : Vec <Point2 <f32>> = [
655      [ 0.0,  2.0],
656      [-2.0, -2.0],
657      [ 0.0, -2.0],
658      [ 2.0, -2.0]
659    ].map (Point2::from).into_iter().collect();
660    let hull = Hull2::from_points (points.as_slice()).unwrap();
661    // points are in counter-clockwise order
662    let points : Vec <Point2 <f32>> = [
663      [-2.0, -2.0],
664      [ 2.0, -2.0],
665      [ 0.0,  2.0]
666    ].map (Point2::from).into_iter().collect();
667    assert_eq!(hull.points(), points);
668    // multiple edge and interior points are excluded
669    let points : Vec <Point2 <f32>> = [
670      // perimeter points
671      [ 0.0,  6.0],
672      [ 1.0,  5.0],
673      [ 2.0,  4.0],
674      [ 3.0,  3.0],
675      [ 3.0,  1.0],
676      [ 3.0, -1.0],
677      [ 3.0, -3.0],
678      [ 1.0, -3.0],
679      [-1.0, -3.0],
680      [-3.0, -3.0],
681      [-3.0, -1.0],
682      [-3.0,  1.0],
683      [-3.0,  3.0],
684      [-2.0,  4.0],
685      [-1.0,  5.0],
686      // interior points
687      [-1.0,  2.0],
688      [ 2.0, -1.0],
689      [ 0.0,  3.0],
690      [-2.0, -2.0]
691    ].map (Point2::from).into_iter().collect();
692    let hull = Hull2::from_points (points.as_slice()).unwrap();
693    // points are in counter-clockwise order
694    let points : Vec <Point2 <f32>> = [
695      [-3.0, -3.0],
696      [ 3.0, -3.0],
697      [ 3.0,  3.0],
698      [ 0.0,  6.0],
699      [-3.0,  3.0]
700    ].map (Point2::from).into_iter().collect();
701    assert_eq!(hull.points(), points);
702  }
703
704  #[test]
705  fn hull3() {
706    use rand::{RngExt, SeedableRng};
707    use rand_xorshift::XorShiftRng;
708    use rand_distr::Distribution;
709    // dedup 0 dimensional
710    let points = [
711      [-1.0, -4.0, -1.0],
712      [-1.0, -4.0, -1.0]
713    ].map (Point3::<f32>::from);
714    let hull = Hull3::from_points (points.as_slice()).unwrap();
715    assert_eq!(hull.points(), &[
716      [-1.0, -4.0, -1.0]
717    ].map (Into::into));
718    // 1 dimensional endpoints
719    let points = [
720      [-1.0, -4.0, -1.0],
721      [ 0.0,  0.0,  0.0],
722      [ 1.0,  4.0,  1.0]
723    ].map (Point3::<f32>::from);
724    let hull = Hull3::from_points (points.as_slice()).unwrap();
725    assert_eq!(hull.points(), &[
726      [-1.0, -4.0, -1.0],
727      [ 1.0,  4.0,  1.0]
728    ].map (Into::into));
729    // 2 dimensional
730    let points = [
731      [-1.0, -4.0, 0.0],
732      [ 2.0,  2.0, 0.0],
733      [-1.0, -1.0, 0.0],
734      [-4.0, -1.0, 0.0],
735      [ 0.0,  2.0, 0.0]
736    ].map (Point3::<f32>::from);
737    let hull = Hull3::from_points (points.as_slice()).unwrap();
738    assert_eq!(hull.points(), &[
739      [-1.0, -4.0, 0.0],
740      [ 2.0,  2.0, 0.0],
741      [ 0.0,  2.0, 0.0],
742      [-4.0, -1.0, 0.0]
743    ].map (Into::into));
744    // already a hull
745    let points = [
746      [-1.0, -4.0, -1.0],
747      [ 2.0,  2.0,  2.0],
748      [-4.0, -1.0,  2.0],
749      [ 0.0,  2.0, -3.0]
750    ].map (Point3::<f32>::from);
751    let hull = Hull3::from_points (points.as_slice()).unwrap();
752    assert_eq!(hull.points(), &[
753      [-1.0, -4.0, -1.0],
754      [ 2.0,  2.0,  2.0],
755      [-4.0, -1.0,  2.0],
756      [ 0.0,  2.0, -3.0]
757    ].map (Into::into));
758    let points = [
759      [ 1.0, -1.0, -1.0],
760      [ 1.0, -1.0,  1.0],
761      [ 1.0,  1.0, -1.0],
762      [ 1.0,  1.0,  1.0],
763      [-1.0, -1.0, -1.0],
764      [-1.0, -1.0,  1.0],
765      [-1.0,  1.0, -1.0],
766      [-1.0,  1.0,  1.0]
767    ].map (Point3::<f32>::from);
768    let hull = Hull3::from_points (points.as_slice()).unwrap();
769    assert_eq!(hull.points(), points.as_slice());
770    // removes interior points
771    let points = [
772      [-1.0, -4.0, -1.0],
773      [ 2.0,  2.0,  2.0],
774      [-4.0, -1.0,  2.0],
775      [ 0.0,  2.0, -3.0],
776      [ 0.1,  0.2,  0.3],
777      [-0.1, -0.2, -0.3],
778      [-0.1,  0.2,  0.3]
779    ].map (Point3::<f32>::from);
780    let hull = Hull3::from_points (points.as_slice()).unwrap();
781    assert_eq!(hull.points(), &[
782      [-1.0, -4.0, -1.0],
783      [ 2.0,  2.0,  2.0],
784      [-4.0, -1.0,  2.0],
785      [ 0.0,  2.0, -3.0]
786    ].map (Into::into));
787    // check that all triangles are valid
788    // the following generated hulls are not best-case (there may be clusters of points
789    // in planes), but we want to test that the hull algorithm is robust
790    let mut rng = XorShiftRng::seed_from_u64 (0);
791    let cauchy  = rand_distr::Cauchy::new (0.0, 1.0).unwrap();
792    let randf   = |rng : &mut XorShiftRng| cauchy.sample (rng);
793    for _ in 0..500 {
794      let _aabb = Aabb3::with_minmax (
795        [-5.0, -5.0, -5.0].into(),
796        [ 5.0,  5.0,  5.0].into()).unwrap();
797      let mut points = std::iter::repeat_with (
798        || point3 (randf (&mut rng), randf (&mut rng), randf (&mut rng))
799        //|| aabb.clamp (&point3 (randf (&mut rng), randf (&mut rng), randf (&mut rng)))
800      ).take (3).collect::<Vec<_>>();
801      let npoints = rng.random_range (4..50);
802      let mut rand_vec =
803        || vector3 (randf (&mut rng), randf (&mut rng), randf (&mut rng));
804      for _ in 0..npoints {
805        points.push (points.last().unwrap() + rand_vec());
806        //points.push (aabb.clamp (&(points.last().unwrap() + rand_vec())));
807      }
808      let (hull, mesh) = Hull3::from_points_with_mesh (&points).unwrap();
809      for triangle in mesh.triangles().values() {
810        let _triangle = hull.triangle (triangle);
811      }
812    }
813  }
814}