Skip to main content

proof_engine/geometry/
hull.rs

1//! Convex hull computation in 2D and 3D.
2
3use glam::{Vec2, Vec3};
4
5/// 2D convex hull result.
6#[derive(Debug, Clone)]
7pub struct ConvexHull2D {
8    pub points: Vec<Vec2>,
9    pub hull_indices: Vec<usize>,
10}
11
12/// 3D convex hull result.
13#[derive(Debug, Clone)]
14pub struct ConvexHull {
15    pub points: Vec<Vec3>,
16    pub hull_faces: Vec<[usize; 3]>,
17    pub hull_vertices: Vec<usize>,
18}
19
20impl ConvexHull2D {
21    /// Compute 2D convex hull using Andrew's monotone chain algorithm.
22    pub fn compute(points: &[Vec2]) -> Self {
23        let n = points.len();
24        if n < 3 {
25            return Self { points: points.to_vec(), hull_indices: (0..n).collect() };
26        }
27
28        let mut sorted: Vec<(usize, Vec2)> = points.iter().copied().enumerate().collect();
29        sorted.sort_by(|a, b| a.1.x.partial_cmp(&b.1.x).unwrap().then(a.1.y.partial_cmp(&b.1.y).unwrap()));
30
31        let mut hull = Vec::with_capacity(2 * n);
32
33        // Lower hull
34        for &(i, p) in &sorted {
35            while hull.len() >= 2 {
36                let a = points[hull[hull.len() - 2]];
37                let b = points[hull[hull.len() - 1]];
38                if cross2d(a, b, p) <= 0.0 { hull.pop(); } else { break; }
39            }
40            hull.push(i);
41        }
42
43        // Upper hull
44        let lower_len = hull.len() + 1;
45        for &(i, p) in sorted.iter().rev() {
46            while hull.len() >= lower_len {
47                let a = points[hull[hull.len() - 2]];
48                let b = points[hull[hull.len() - 1]];
49                if cross2d(a, b, p) <= 0.0 { hull.pop(); } else { break; }
50            }
51            hull.push(i);
52        }
53        hull.pop(); // remove duplicate of first point
54
55        Self { points: points.to_vec(), hull_indices: hull }
56    }
57
58    pub fn hull_points(&self) -> Vec<Vec2> {
59        self.hull_indices.iter().map(|&i| self.points[i]).collect()
60    }
61
62    pub fn perimeter(&self) -> f32 {
63        let pts = self.hull_points();
64        let n = pts.len();
65        (0..n).map(|i| (pts[(i + 1) % n] - pts[i]).length()).sum()
66    }
67
68    pub fn area(&self) -> f32 {
69        let pts = self.hull_points();
70        let n = pts.len();
71        if n < 3 { return 0.0; }
72        let mut area = 0.0;
73        for i in 0..n {
74            let j = (i + 1) % n;
75            area += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
76        }
77        area.abs() * 0.5
78    }
79}
80
81impl ConvexHull {
82    /// Compute 3D convex hull using incremental algorithm.
83    pub fn compute(points: &[Vec3]) -> Self {
84        let n = points.len();
85        if n < 4 {
86            return Self {
87                points: points.to_vec(),
88                hull_faces: Vec::new(),
89                hull_vertices: (0..n).collect(),
90            };
91        }
92
93        // Start with a tetrahedron from first 4 non-coplanar points
94        let mut faces: Vec<[usize; 3]> = vec![
95            [0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2],
96        ];
97
98        // Ensure outward-facing normals
99        let center = (points[0] + points[1] + points[2] + points[3]) * 0.25;
100        for face in &mut faces {
101            let a = points[face[0]];
102            let b = points[face[1]];
103            let c = points[face[2]];
104            let n = (b - a).cross(c - a);
105            let mid = (a + b + c) / 3.0;
106            if n.dot(mid - center) < 0.0 {
107                face.swap(1, 2);
108            }
109        }
110
111        // Add remaining points
112        for pi in 4..n {
113            let p = points[pi];
114            let mut visible = Vec::new();
115
116            for (fi, face) in faces.iter().enumerate() {
117                let a = points[face[0]];
118                let b = points[face[1]];
119                let c = points[face[2]];
120                let normal = (b - a).cross(c - a);
121                if normal.dot(p - a) > 1e-8 {
122                    visible.push(fi);
123                }
124            }
125
126            if visible.is_empty() { continue; }
127
128            // Find horizon edges
129            let mut horizon: Vec<(usize, usize)> = Vec::new();
130            for &fi in &visible {
131                let face = faces[fi];
132                let edges = [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])];
133                for &(a, b) in &edges {
134                    let is_shared = visible.iter().any(|&ofi| {
135                        if ofi == fi { return false; }
136                        let of = faces[ofi];
137                        let oedges = [(of[0], of[1]), (of[1], of[2]), (of[2], of[0])];
138                        oedges.iter().any(|&(oa, ob)| (oa == b && ob == a))
139                    });
140                    if !is_shared {
141                        horizon.push((a, b));
142                    }
143                }
144            }
145
146            // Remove visible faces
147            visible.sort_unstable_by(|a, b| b.cmp(a));
148            for fi in visible {
149                faces.swap_remove(fi);
150            }
151
152            // Add new faces from horizon to new point
153            for (a, b) in horizon {
154                faces.push([a, b, pi]);
155            }
156        }
157
158        let mut hull_verts: Vec<usize> = faces.iter()
159            .flat_map(|f| f.iter().copied())
160            .collect();
161        hull_verts.sort_unstable();
162        hull_verts.dedup();
163
164        Self {
165            points: points.to_vec(),
166            hull_faces: faces,
167            hull_vertices: hull_verts,
168        }
169    }
170
171    pub fn face_count(&self) -> usize { self.hull_faces.len() }
172    pub fn vertex_count(&self) -> usize { self.hull_vertices.len() }
173}
174
175fn cross2d(o: Vec2, a: Vec2, b: Vec2) -> f32 {
176    (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn hull_2d_square() {
185        let points = vec![
186            Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0),
187            Vec2::new(1.0, 1.0), Vec2::new(0.0, 1.0),
188            Vec2::new(0.5, 0.5), // interior point
189        ];
190        let hull = ConvexHull2D::compute(&points);
191        assert_eq!(hull.hull_indices.len(), 4); // only corners
192    }
193
194    #[test]
195    fn hull_2d_area() {
196        let points = vec![
197            Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0),
198            Vec2::new(2.0, 2.0), Vec2::new(0.0, 2.0),
199        ];
200        let hull = ConvexHull2D::compute(&points);
201        assert!((hull.area() - 4.0).abs() < 0.01);
202    }
203
204    #[test]
205    fn hull_3d_tetrahedron() {
206        let points = vec![
207            Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0),
208            Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0),
209        ];
210        let hull = ConvexHull::compute(&points);
211        assert_eq!(hull.face_count(), 4);
212        assert_eq!(hull.vertex_count(), 4);
213    }
214
215    #[test]
216    fn hull_3d_with_interior() {
217        let mut points = vec![
218            Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, -1.0, -1.0),
219            Vec3::new(-1.0, 1.0, -1.0), Vec3::new(1.0, 1.0, -1.0),
220            Vec3::new(-1.0, -1.0, 1.0), Vec3::new(1.0, -1.0, 1.0),
221            Vec3::new(-1.0, 1.0, 1.0), Vec3::new(1.0, 1.0, 1.0),
222            Vec3::ZERO, // interior
223        ];
224        let hull = ConvexHull::compute(&points);
225        assert_eq!(hull.vertex_count(), 8); // interior excluded
226    }
227}