Skip to main content

proof_engine/geometry/
voronoi.rs

1//! Voronoi diagrams and Delaunay triangulation in 2D and 3D.
2
3use glam::{Vec2, Vec3};
4use std::collections::HashMap;
5
6/// A cell in a Voronoi diagram.
7#[derive(Debug, Clone)]
8pub struct VoronoiCell {
9    pub site: Vec2,
10    pub vertices: Vec<Vec2>,
11    pub neighbor_indices: Vec<usize>,
12}
13
14/// 2D Voronoi diagram computed from a set of sites.
15#[derive(Debug, Clone)]
16pub struct VoronoiDiagram {
17    pub sites: Vec<Vec2>,
18    pub cells: Vec<VoronoiCell>,
19    pub bounds: (Vec2, Vec2),
20}
21
22impl VoronoiDiagram {
23    /// Compute Voronoi diagram using Fortune's sweep line (simplified brute-force for now).
24    pub fn compute(sites: &[Vec2], bounds_min: Vec2, bounds_max: Vec2) -> Self {
25        let mut cells = Vec::with_capacity(sites.len());
26
27        // For each site, find its Voronoi cell by clipping the half-planes
28        for (i, &site) in sites.iter().enumerate() {
29            let mut polygon = vec![
30                bounds_min,
31                Vec2::new(bounds_max.x, bounds_min.y),
32                bounds_max,
33                Vec2::new(bounds_min.x, bounds_max.y),
34            ];
35
36            let mut neighbors = Vec::new();
37            for (j, &other) in sites.iter().enumerate() {
38                if i == j { continue; }
39                let clipped = clip_polygon_by_bisector(&polygon, site, other);
40                if clipped.len() != polygon.len() || clipped.iter().zip(polygon.iter()).any(|(a, b)| (*a - *b).length() > 1e-6) {
41                    neighbors.push(j);
42                }
43                polygon = clipped;
44                if polygon.is_empty() { break; }
45            }
46
47            cells.push(VoronoiCell {
48                site,
49                vertices: polygon,
50                neighbor_indices: neighbors,
51            });
52        }
53
54        Self {
55            sites: sites.to_vec(),
56            cells,
57            bounds: (bounds_min, bounds_max),
58        }
59    }
60
61    /// Find the nearest site to a point.
62    pub fn nearest_site(&self, point: Vec2) -> usize {
63        self.sites.iter().enumerate()
64            .min_by(|(_, a), (_, b)| {
65                let da = (**a - point).length_squared();
66                let db = (**b - point).length_squared();
67                da.partial_cmp(&db).unwrap()
68            })
69            .map(|(i, _)| i)
70            .unwrap_or(0)
71    }
72
73    /// Cell area for a given site.
74    pub fn cell_area(&self, index: usize) -> f32 {
75        polygon_area(&self.cells[index].vertices)
76    }
77}
78
79/// Clip a convex polygon by the perpendicular bisector of two points.
80/// Keeps the half on the side of `keep`.
81fn clip_polygon_by_bisector(polygon: &[Vec2], keep: Vec2, other: Vec2) -> Vec<Vec2> {
82    if polygon.is_empty() { return Vec::new(); }
83    let mid = (keep + other) * 0.5;
84    let normal = (other - keep).normalize();
85
86    let mut output = Vec::new();
87    let n = polygon.len();
88
89    for i in 0..n {
90        let a = polygon[i];
91        let b = polygon[(i + 1) % n];
92        let da = (a - mid).dot(normal);
93        let db = (b - mid).dot(normal);
94
95        if da <= 0.0 { output.push(a); }
96        if (da > 0.0) != (db > 0.0) {
97            let t = da / (da - db);
98            output.push(a + (b - a) * t);
99        }
100    }
101    output
102}
103
104fn polygon_area(verts: &[Vec2]) -> f32 {
105    let n = verts.len();
106    if n < 3 { return 0.0; }
107    let mut area = 0.0;
108    for i in 0..n {
109        let j = (i + 1) % n;
110        area += verts[i].x * verts[j].y;
111        area -= verts[j].x * verts[i].y;
112    }
113    area.abs() * 0.5
114}
115
116// ── Delaunay Triangulation ──────────────────────────────────────────────────
117
118/// A triangle in the Delaunay triangulation (indices into the point array).
119#[derive(Debug, Clone, Copy)]
120pub struct DelaunayTriangle {
121    pub a: usize,
122    pub b: usize,
123    pub c: usize,
124}
125
126/// 2D Delaunay triangulation using the Bowyer-Watson algorithm.
127#[derive(Debug, Clone)]
128pub struct DelaunayTriangulation {
129    pub points: Vec<Vec2>,
130    pub triangles: Vec<DelaunayTriangle>,
131}
132
133impl DelaunayTriangulation {
134    /// Compute Delaunay triangulation of the given points.
135    pub fn compute(points: &[Vec2]) -> Self {
136        if points.len() < 3 {
137            return Self { points: points.to_vec(), triangles: Vec::new() };
138        }
139
140        // Super-triangle that contains all points
141        let (mut min, mut max) = (points[0], points[0]);
142        for p in points {
143            min = min.min(*p);
144            max = max.max(*p);
145        }
146        let d = (max - min).max_element() * 10.0;
147        let mid = (min + max) * 0.5;
148
149        let mut all_points = vec![
150            mid + Vec2::new(-d, -d),
151            mid + Vec2::new(d, -d),
152            mid + Vec2::new(0.0, d),
153        ];
154        let super_start = 0;
155        all_points.extend_from_slice(points);
156
157        let mut triangles = vec![DelaunayTriangle { a: 0, b: 1, c: 2 }];
158
159        // Insert each point
160        for pi in 3..all_points.len() {
161            let p = all_points[pi];
162            let mut bad_triangles = Vec::new();
163
164            for (ti, tri) in triangles.iter().enumerate() {
165                if circumcircle_contains(&all_points, tri, p) {
166                    bad_triangles.push(ti);
167                }
168            }
169
170            // Find boundary edges of the "hole"
171            let mut boundary: Vec<(usize, usize)> = Vec::new();
172            for &ti in &bad_triangles {
173                let tri = &triangles[ti];
174                let edges = [(tri.a, tri.b), (tri.b, tri.c), (tri.c, tri.a)];
175                for &(a, b) in &edges {
176                    let shared = bad_triangles.iter().any(|&oti| {
177                        if oti == ti { return false; }
178                        let ot = &triangles[oti];
179                        let oedges = [(ot.a, ot.b), (ot.b, ot.c), (ot.c, ot.a)];
180                        oedges.iter().any(|&(oa, ob)| (oa == b && ob == a) || (oa == a && ob == b))
181                    });
182                    if !shared {
183                        boundary.push((a, b));
184                    }
185                }
186            }
187
188            // Remove bad triangles (in reverse order to preserve indices)
189            bad_triangles.sort_unstable_by(|a, b| b.cmp(a));
190            for ti in bad_triangles {
191                triangles.swap_remove(ti);
192            }
193
194            // Create new triangles from boundary edges to new point
195            for (a, b) in boundary {
196                triangles.push(DelaunayTriangle { a, b, c: pi });
197            }
198        }
199
200        // Remove triangles that reference super-triangle vertices
201        triangles.retain(|t| t.a >= 3 && t.b >= 3 && t.c >= 3);
202
203        // Remap indices (subtract 3 to account for removed super-triangle)
204        for t in &mut triangles {
205            t.a -= 3;
206            t.b -= 3;
207            t.c -= 3;
208        }
209
210        Self {
211            points: points.to_vec(),
212            triangles,
213        }
214    }
215
216    pub fn triangle_count(&self) -> usize { self.triangles.len() }
217}
218
219fn circumcircle_contains(points: &[Vec2], tri: &DelaunayTriangle, p: Vec2) -> bool {
220    let a = points[tri.a];
221    let b = points[tri.b];
222    let c = points[tri.c];
223
224    let ax = a.x - p.x;
225    let ay = a.y - p.y;
226    let bx = b.x - p.x;
227    let by = b.y - p.y;
228    let cx = c.x - p.x;
229    let cy = c.y - p.y;
230
231    let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
232            - bx * (ay * (cx * cx + cy * cy) - cy * (ax * ax + ay * ay))
233            + cx * (ay * (bx * bx + by * by) - by * (ax * ax + ay * ay));
234
235    det > 0.0
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn voronoi_creates_cells() {
244        let sites = vec![Vec2::new(1.0, 1.0), Vec2::new(3.0, 1.0), Vec2::new(2.0, 3.0)];
245        let vor = VoronoiDiagram::compute(&sites, Vec2::ZERO, Vec2::splat(4.0));
246        assert_eq!(vor.cells.len(), 3);
247        for cell in &vor.cells {
248            assert!(!cell.vertices.is_empty());
249        }
250    }
251
252    #[test]
253    fn nearest_site_correct() {
254        let sites = vec![Vec2::ZERO, Vec2::new(10.0, 0.0)];
255        let vor = VoronoiDiagram::compute(&sites, Vec2::splat(-20.0), Vec2::splat(20.0));
256        assert_eq!(vor.nearest_site(Vec2::new(1.0, 0.0)), 0);
257        assert_eq!(vor.nearest_site(Vec2::new(9.0, 0.0)), 1);
258    }
259
260    #[test]
261    fn delaunay_basic() {
262        let points = vec![
263            Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0),
264            Vec2::new(0.0, 1.0), Vec2::new(1.0, 1.0),
265        ];
266        let dt = DelaunayTriangulation::compute(&points);
267        assert!(dt.triangle_count() >= 2);
268    }
269
270    #[test]
271    fn delaunay_too_few_points() {
272        let dt = DelaunayTriangulation::compute(&[Vec2::ZERO, Vec2::X]);
273        assert_eq!(dt.triangle_count(), 0);
274    }
275}