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