Skip to main content

oxihuman_core/
voronoi_2d.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! 2D Voronoi diagram cells using brute-force nearest-seed computation.
6
7/// A 2D point.
8#[derive(Debug, Clone, Copy, PartialEq)]
9#[allow(dead_code)]
10pub struct Point2 {
11    pub x: f32,
12    pub y: f32,
13}
14
15impl Point2 {
16    #[allow(dead_code)]
17    pub fn new(x: f32, y: f32) -> Self {
18        Self { x, y }
19    }
20
21    #[allow(dead_code)]
22    pub fn dist_sq(self, other: Self) -> f32 {
23        let dx = self.x - other.x;
24        let dy = self.y - other.y;
25        dx * dx + dy * dy
26    }
27}
28
29/// A Voronoi cell: seed point + index of nearest seed for queries.
30#[derive(Debug, Clone)]
31#[allow(dead_code)]
32pub struct VoronoiCell2D {
33    pub seed: Point2,
34    pub id: usize,
35}
36
37/// A 2D Voronoi diagram.
38#[derive(Debug, Clone, Default)]
39#[allow(dead_code)]
40pub struct Voronoi2D {
41    pub seeds: Vec<Point2>,
42}
43
44impl Voronoi2D {
45    /// Create an empty diagram.
46    #[allow(dead_code)]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Add a seed point.
52    #[allow(dead_code)]
53    pub fn add_seed(&mut self, p: Point2) {
54        self.seeds.push(p);
55    }
56
57    /// Find the index of the nearest seed to query point `q`.
58    /// Returns `None` if there are no seeds.
59    #[allow(dead_code)]
60    pub fn nearest_seed(&self, q: Point2) -> Option<usize> {
61        if self.seeds.is_empty() {
62            return None;
63        }
64        let mut best = 0;
65        let mut best_d = q.dist_sq(self.seeds[0]);
66        for (i, &s) in self.seeds.iter().enumerate().skip(1) {
67            let d = q.dist_sq(s);
68            if d < best_d {
69                best_d = d;
70                best = i;
71            }
72        }
73        Some(best)
74    }
75
76    /// Rasterize the Voronoi diagram into a `width x height` grid.
77    /// Each cell value is the index of the nearest seed (or 0 if no seeds).
78    #[allow(dead_code)]
79    pub fn rasterize(&self, width: usize, height: usize) -> Vec<usize> {
80        let mut grid = vec![0usize; width * height];
81        if self.seeds.is_empty() {
82            return grid;
83        }
84        for row in 0..height {
85            for col in 0..width {
86                let q = Point2::new(col as f32, row as f32);
87                let idx = self.nearest_seed(q).unwrap_or(0);
88                grid[row * width + col] = idx;
89            }
90        }
91        grid
92    }
93
94    /// Number of seeds.
95    #[allow(dead_code)]
96    pub fn seed_count(&self) -> usize {
97        self.seeds.len()
98    }
99
100    /// Clear all seeds.
101    #[allow(dead_code)]
102    pub fn clear(&mut self) {
103        self.seeds.clear();
104    }
105
106    /// Build a list of `VoronoiCell2D` from current seeds.
107    #[allow(dead_code)]
108    pub fn cells(&self) -> Vec<VoronoiCell2D> {
109        self.seeds
110            .iter()
111            .enumerate()
112            .map(|(id, &seed)| VoronoiCell2D { seed, id })
113            .collect()
114    }
115}
116
117/// Compute the Voronoi cell assignment for a set of query points.
118#[allow(dead_code)]
119pub fn voronoi_assign(diagram: &Voronoi2D, queries: &[Point2]) -> Vec<usize> {
120    queries
121        .iter()
122        .map(|&q| diagram.nearest_seed(q).unwrap_or(0))
123        .collect()
124}
125
126/// Build a `Voronoi2D` from a slice of seed points.
127#[allow(dead_code)]
128pub fn build_voronoi(seeds: &[Point2]) -> Voronoi2D {
129    let mut v = Voronoi2D::new();
130    for &s in seeds {
131        v.add_seed(s);
132    }
133    v
134}
135
136/// Compute the centroid of seed points assigned to a given cell.
137#[allow(dead_code)]
138pub fn cell_centroid(diagram: &Voronoi2D, cell_id: usize, samples: &[Point2]) -> Option<Point2> {
139    let pts: Vec<Point2> = samples
140        .iter()
141        .copied()
142        .filter(|&q| diagram.nearest_seed(q) == Some(cell_id))
143        .collect();
144    if pts.is_empty() {
145        return None;
146    }
147    let n = pts.len() as f32;
148    let sx: f32 = pts.iter().map(|p| p.x).sum();
149    let sy: f32 = pts.iter().map(|p| p.y).sum();
150    Some(Point2::new(sx / n, sy / n))
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    fn make_seeds() -> Voronoi2D {
158        let mut v = Voronoi2D::new();
159        v.add_seed(Point2::new(0.0, 0.0));
160        v.add_seed(Point2::new(10.0, 0.0));
161        v.add_seed(Point2::new(5.0, 10.0));
162        v
163    }
164
165    #[test]
166    fn nearest_seed_returns_none_for_empty() {
167        let v = Voronoi2D::new();
168        assert!(v.nearest_seed(Point2::new(1.0, 1.0)).is_none());
169    }
170
171    #[test]
172    fn nearest_seed_basic() {
173        let v = make_seeds();
174        assert_eq!(v.nearest_seed(Point2::new(0.5, 0.5)), Some(0));
175        assert_eq!(v.nearest_seed(Point2::new(9.5, 0.5)), Some(1));
176        assert_eq!(v.nearest_seed(Point2::new(5.0, 9.5)), Some(2));
177    }
178
179    #[test]
180    fn seed_count_matches() {
181        let v = make_seeds();
182        assert_eq!(v.seed_count(), 3);
183    }
184
185    #[test]
186    fn clear_removes_all_seeds() {
187        let mut v = make_seeds();
188        v.clear();
189        assert_eq!(v.seed_count(), 0);
190    }
191
192    #[test]
193    fn cells_returns_correct_ids() {
194        let v = make_seeds();
195        let cells = v.cells();
196        assert_eq!(cells.len(), 3);
197        assert_eq!(cells[0].id, 0);
198        assert_eq!(cells[2].id, 2);
199    }
200
201    #[test]
202    fn rasterize_size_matches() {
203        let v = make_seeds();
204        let grid = v.rasterize(4, 3);
205        assert_eq!(grid.len(), 12);
206    }
207
208    #[test]
209    fn rasterize_empty_returns_zeros() {
210        let v = Voronoi2D::new();
211        let grid = v.rasterize(3, 3);
212        assert!(grid.iter().all(|&x| x == 0));
213    }
214
215    #[test]
216    fn voronoi_assign_empty_seeds() {
217        let v = Voronoi2D::new();
218        let pts = vec![Point2::new(0.0, 0.0)];
219        let out = voronoi_assign(&v, &pts);
220        assert_eq!(out, vec![0]);
221    }
222
223    #[test]
224    fn build_voronoi_seed_count() {
225        let seeds = vec![Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)];
226        let v = build_voronoi(&seeds);
227        assert_eq!(v.seed_count(), 2);
228    }
229
230    #[test]
231    fn cell_centroid_none_when_no_samples() {
232        let v = make_seeds();
233        let c = cell_centroid(&v, 0, &[]);
234        assert!(c.is_none());
235    }
236}