Skip to main content

math_utils/geometry/primitive/
hull.rs

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