Skip to main content

prime_voronoi/
lib.rs

1//! `prime-voronoi` — Voronoi diagrams and Lloyd relaxation.
2//!
3//! All public functions are pure (LOAD + COMPUTE only). No `&mut`, no side effects,
4//! no hidden state.
5//!
6//! # Temporal Assembly Model
7//! - **LOAD** — read parameters (query point, seed points, sample points)
8//! - **COMPUTE** — pure geometry (distance comparisons, centroid sums)
9//! - **APPEND** — return new state as owned value
10//!
11//! STORE and JUMP do not exist here.
12//!
13//! # Included
14//! - `voronoi_nearest_2d` — find nearest seed index and F1 distance
15//! - `voronoi_f1_f2_2d` — F1 (nearest) and F2 (second-nearest) distances
16//! - `lloyd_relax_step_2d` — one Lloyd relaxation step (sample-based)
17//!
18//! - `delaunay_2d` — Delaunay triangulation via Bowyer-Watson
19
20// ── Voronoi nearest ───────────────────────────────────────────────────────────
21
22/// Find the nearest seed and its distance from `query`.
23///
24/// Returns `None` if `seeds` is empty.
25///
26/// # Math
27///
28/// ```text
29/// (index, dist) = argmin_i  sqrt((query.x - seeds[i].x)² + (query.y - seeds[i].y)²)
30/// ```
31///
32/// Squared distances are compared during the scan to avoid unnecessary square roots.
33/// The final distance is returned as the true Euclidean distance.
34///
35/// # Arguments
36/// * `query`  — point to query `(x, y)`
37/// * `seeds`  — slice of `(x, y)` seed points
38///
39/// # Returns
40/// `Some((index, distance))` of the nearest seed, or `None` if `seeds` is empty.
41///
42/// # Edge cases
43/// * Empty `seeds` → `None`
44/// * Single seed → always returns `(0, dist_to_seed_0)`
45///
46/// # Example
47/// ```rust
48/// use prime_voronoi::voronoi_nearest_2d;
49/// let seeds = [(0.0_f32, 0.0), (1.0, 0.0), (0.0, 1.0)];
50/// let (idx, dist) = voronoi_nearest_2d((0.1, 0.1), &seeds).unwrap();
51/// assert_eq!(idx, 0);
52/// assert!(dist < 0.2);
53/// ```
54pub fn voronoi_nearest_2d(query: (f32, f32), seeds: &[(f32, f32)]) -> Option<(usize, f32)> {
55    seeds.iter().enumerate().fold(None, |best, (i, &(sx, sy))| {
56        let dx = query.0 - sx;
57        let dy = query.1 - sy;
58        let d2 = dx * dx + dy * dy;
59        match best {
60            None => Some((i, d2)),
61            Some((_bi, bd2)) if d2 < bd2 => Some((i, d2)),
62            other => other,
63        }
64    })
65    .map(|(i, d2)| (i, d2.sqrt()))
66}
67
68// ── Voronoi F1 + F2 ───────────────────────────────────────────────────────────
69
70/// Compute F1 (nearest) and F2 (second-nearest) Euclidean distances from `query`.
71///
72/// Returns `None` if `seeds` is empty. Returns `(f1, f1)` if only one seed.
73///
74/// F1 and F2 together are used to compute cellular noise patterns and edge
75/// detection on Voronoi diagrams.
76///
77/// # Math
78///
79/// ```text
80/// F1 = min_i  dist(query, seeds[i])
81/// F2 = min_i  dist(query, seeds[i])  where i ≠ argmin_j dist(query, seeds[j])
82/// ```
83///
84/// # Arguments
85/// * `query`  — query point `(x, y)`
86/// * `seeds`  — slice of seed points
87///
88/// # Returns
89/// `Some((f1, f2))` or `None` if empty.
90///
91/// # Example
92/// ```rust
93/// use prime_voronoi::voronoi_f1_f2_2d;
94/// let seeds = [(0.0_f32, 0.0), (1.0, 0.0)];
95/// let (f1, f2) = voronoi_f1_f2_2d((0.3, 0.0), &seeds).unwrap();
96/// assert!(f1 < f2);
97/// ```
98pub fn voronoi_f1_f2_2d(query: (f32, f32), seeds: &[(f32, f32)]) -> Option<(f32, f32)> {
99    if seeds.is_empty() {
100        return None;
101    }
102
103    let (f1_d2, f2_d2) = seeds.iter().fold(
104        (f32::MAX, f32::MAX),
105        |(f1, f2), &(sx, sy)| {
106            let dx = query.0 - sx;
107            let dy = query.1 - sy;
108            let d2 = dx * dx + dy * dy;
109            if d2 < f1 {
110                (d2, f1)
111            } else if d2 < f2 {
112                (f1, d2)
113            } else {
114                (f1, f2)
115            }
116        },
117    );
118
119    Some((f1_d2.sqrt(), f2_d2.min(f32::MAX).sqrt()))
120}
121
122// ── Lloyd relaxation ──────────────────────────────────────────────────────────
123
124/// One step of sample-based Lloyd relaxation in 2-D.
125///
126/// Moves each seed toward the centroid of its Voronoi cell, estimated by
127/// distributing a set of `samples` (e.g., a regular grid or stratified random
128/// points) among the seeds by nearest-neighbor assignment.
129///
130/// Seeds with no samples assigned to them remain at their original position
131/// (they are "empty cells" — this is the standard behaviour for boundary seeds
132/// that are out-competed by neighbours).
133///
134/// # Math
135///
136/// ```text
137/// For each sample s:
138///   assign s to nearest seed j
139///
140/// For each seed i:
141///   centroid_i = mean of all samples assigned to seed i
142///   if no samples → keep original position
143///
144/// new_seeds[i] = centroid_i
145/// ```
146///
147/// # Arguments
148/// * `seeds`   — current seed positions `&[(x, y)]`
149/// * `samples` — evaluation points used to estimate Voronoi cell centroids
150///
151/// # Returns
152/// New seed positions (same length as `seeds`).
153/// Returns an empty `Vec` if `seeds` is empty.
154///
155/// # Edge cases
156/// * Empty `seeds` → `vec![]`
157/// * Empty `samples` → all seeds unchanged
158///
159/// # Example
160/// ```rust
161/// use prime_voronoi::lloyd_relax_step_2d;
162///
163/// // Two seeds, 4 samples: samples cluster around (0,0) and (1,0)
164/// let seeds = vec![(0.1_f32, 0.0), (0.9, 0.0)];
165/// let samples = vec![(0.0, 0.0), (0.2, 0.0), (0.8, 0.0), (1.0, 0.0)];
166/// let relaxed = lloyd_relax_step_2d(&seeds, &samples);
167/// assert_eq!(relaxed.len(), 2);
168/// assert!((relaxed[0].0 - 0.1).abs() < 0.5);
169/// ```
170pub fn lloyd_relax_step_2d(seeds: &[(f32, f32)], samples: &[(f32, f32)]) -> Vec<(f32, f32)> {
171    if seeds.is_empty() {
172        return vec![];
173    }
174
175    // Accumulate (sum_x, sum_y, count) per seed.
176    let init: Vec<(f32, f32, u32)> = seeds.iter().map(|_| (0.0, 0.0, 0)).collect();
177
178    let accum = samples.iter().fold(init, |mut acc, &(sx, sy)| {
179        // Find nearest seed to this sample
180        let nearest = seeds.iter().enumerate().fold(
181            (0usize, f32::MAX),
182            |(bi, bd2), (i, &(px, py))| {
183                let dx = sx - px;
184                let dy = sy - py;
185                let d2 = dx * dx + dy * dy;
186                if d2 < bd2 { (i, d2) } else { (bi, bd2) }
187            },
188        ).0;
189
190        acc[nearest].0 += sx;
191        acc[nearest].1 += sy;
192        acc[nearest].2 += 1;
193        acc
194    });
195
196    // Compute centroids; fall back to original seed if no samples assigned.
197    accum
198        .iter()
199        .enumerate()
200        .map(|(i, &(sx, sy, count))| {
201            if count == 0 {
202                seeds[i]
203            } else {
204                (sx / count as f32, sy / count as f32)
205            }
206        })
207        .collect()
208}
209
210// ── Delaunay triangulation ────────────────────────────────────────────────────
211
212/// Circumcircle test: is point `(px, py)` strictly inside the circumcircle of
213/// triangle `(ax,ay)`, `(bx,by)`, `(cx,cy)`?
214///
215/// Orientation-independent: works for both CW and CCW triangles by checking
216/// the sign of the cross product and adjusting accordingly.
217///
218/// # Math
219/// ```text
220/// | dx  dy  dx²+dy² |
221/// | ex  ey  ex²+ey² | > 0  ⟹  point inside circumcircle (CCW triangle)
222/// | fx  fy  fx²+fy² |
223/// ```
224/// where `(dx,dy) = a - p`, `(ex,ey) = b - p`, `(fx,fy) = c - p`.
225/// For CW triangles the sign is flipped.
226#[allow(clippy::too_many_arguments)]
227pub(crate) fn in_circumcircle(
228    px: f32, py: f32,
229    ax: f32, ay: f32,
230    bx: f32, by: f32,
231    cx: f32, cy: f32,
232) -> bool {
233    let dx = ax - px;
234    let dy = ay - py;
235    let ex = bx - px;
236    let ey = by - py;
237    let fx = cx - px;
238    let fy = cy - py;
239
240    let dx2_dy2 = dx * dx + dy * dy;
241    let ex2_ey2 = ex * ex + ey * ey;
242    let fx2_fy2 = fx * fx + fy * fy;
243
244    let det = dx * (ey * fx2_fy2 - fy * ex2_ey2)
245            - dy * (ex * fx2_fy2 - fx * ex2_ey2)
246            + dx2_dy2 * (ex * fy - ey * fx);
247
248    // Check triangle orientation (sign of cross product)
249    let orient = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
250
251    // If CCW (orient > 0): det > 0 means inside
252    // If CW  (orient < 0): det < 0 means inside
253    if orient > 0.0 { det > 0.0 } else { det < 0.0 }
254}
255
256/// Delaunay triangulation via Bowyer-Watson. Returns list of triangle index triples.
257///
258/// Each triangle is `(i, j, k)` where `i`, `j`, `k` are indices into the input `points` slice.
259/// Points on the super-triangle boundary are excluded from the output.
260///
261/// # Math
262/// Bowyer-Watson incrementally inserts points, removing triangles whose circumcircle
263/// contains the new point, then re-triangulates the resulting polygonal hole.
264///
265/// # Example
266/// ```rust
267/// use prime_voronoi::delaunay_2d;
268/// let points = vec![(0.0_f32, 0.0), (1.0, 0.0), (0.5, 1.0)];
269/// let tris = delaunay_2d(&points);
270/// assert_eq!(tris.len(), 1);
271/// // Triangle indices reference the input points (order may vary)
272/// let (a, b, c) = tris[0];
273/// assert!(a < 3 && b < 3 && c < 3);
274/// assert_ne!(a, b);
275/// assert_ne!(b, c);
276/// ```
277pub fn delaunay_2d(points: &[(f32, f32)]) -> Vec<(usize, usize, usize)> {
278    if points.len() < 3 {
279        return vec![];
280    }
281
282    // ADVANCE-EXCEPTION: Bowyer-Watson requires triangle set mutation.
283    // Internal only — public API is pure: &[(f32,f32)] -> Vec<(usize,usize,usize)>
284
285    // Compute bounding box
286    let (mut min_x, mut min_y, mut max_x, mut max_y) =
287        (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
288    for &(x, y) in points {
289        if x < min_x { min_x = x; }
290        if y < min_y { min_y = y; }
291        if x > max_x { max_x = x; }
292        if y > max_y { max_y = y; }
293    }
294
295    let dx = max_x - min_x;
296    let dy = max_y - min_y;
297    let d_max = dx.max(dy).max(1e-6);
298    let mid_x = (min_x + max_x) * 0.5;
299    let mid_y = (min_y + max_y) * 0.5;
300
301    // Super-triangle vertices (indices: n, n+1, n+2)
302    let n = points.len();
303    let s0 = (mid_x - 20.0 * d_max, mid_y - d_max);
304    let s1 = (mid_x, mid_y + 20.0 * d_max);
305    let s2 = (mid_x + 20.0 * d_max, mid_y - d_max);
306
307    // Extended point list: original points + 3 super-triangle vertices
308    let mut all_points: Vec<(f32, f32)> = points.to_vec();
309    all_points.push(s0);
310    all_points.push(s1);
311    all_points.push(s2);
312
313    // Start with super-triangle
314    let mut triangles: Vec<(usize, usize, usize)> = vec![(n, n + 1, n + 2)];
315
316    for i in 0..n {
317        let (px, py) = all_points[i];
318
319        // Find bad triangles (circumcircle contains point i)
320        let mut bad: Vec<usize> = Vec::new();
321        for (t, &(a, b, c)) in triangles.iter().enumerate() {
322            let (ax, ay) = all_points[a];
323            let (bx, by) = all_points[b];
324            let (cx, cy) = all_points[c];
325            if in_circumcircle(px, py, ax, ay, bx, by, cx, cy) {
326                bad.push(t);
327            }
328        }
329
330        // Find boundary polygon (edges that appear in exactly one bad triangle)
331        let mut edges: Vec<(usize, usize)> = Vec::new();
332        for &t in &bad {
333            let (a, b, c) = triangles[t];
334            let tri_edges = [(a, b), (b, c), (c, a)];
335            for &(e0, e1) in &tri_edges {
336                // Check if this edge is shared with another bad triangle
337                let shared = bad.iter().any(|&other| {
338                    other != t && {
339                        let (oa, ob, oc) = triangles[other];
340                        let other_edges = [(oa, ob), (ob, oc), (oc, oa)];
341                        other_edges.iter().any(|&(o0, o1)| {
342                            (e0 == o0 && e1 == o1) || (e0 == o1 && e1 == o0)
343                        })
344                    }
345                });
346                if !shared {
347                    edges.push((e0, e1));
348                }
349            }
350        }
351
352        // Remove bad triangles (reverse order to preserve indices)
353        bad.sort_unstable();
354        for &t in bad.iter().rev() {
355            triangles.swap_remove(t);
356        }
357
358        // Create new triangles from boundary edges to inserted point
359        for &(e0, e1) in &edges {
360            triangles.push((i, e0, e1));
361        }
362    }
363
364    // Remove triangles that reference super-triangle vertices
365    triangles
366        .into_iter()
367        .filter(|&(a, b, c)| a < n && b < n && c < n)
368        .collect()
369}
370
371// ── Tests ─────────────────────────────────────────────────────────────────────
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    const EPSILON: f32 = 1e-4;
378
379    // ── voronoi_nearest_2d ────────────────────────────────────────────────────
380
381    #[test]
382    fn voronoi_nearest_empty_is_none() {
383        assert!(voronoi_nearest_2d((0.0, 0.0), &[]).is_none());
384    }
385
386    #[test]
387    fn voronoi_nearest_single_seed() {
388        let (idx, dist) = voronoi_nearest_2d((1.0, 0.0), &[(0.0, 0.0)]).unwrap();
389        assert_eq!(idx, 0);
390        assert!((dist - 1.0).abs() < EPSILON);
391    }
392
393    #[test]
394    fn voronoi_nearest_selects_closest() {
395        let seeds = [(0.0_f32, 0.0), (1.0, 0.0), (0.0, 1.0)];
396        let (idx, _) = voronoi_nearest_2d((0.1, 0.1), &seeds).unwrap();
397        assert_eq!(idx, 0);
398    }
399
400    #[test]
401    fn voronoi_nearest_boundary_case() {
402        // Query exactly on seed 1
403        let seeds = [(0.0_f32, 0.0), (1.0, 0.0)];
404        let (idx, dist) = voronoi_nearest_2d((1.0, 0.0), &seeds).unwrap();
405        assert_eq!(idx, 1);
406        assert!(dist.abs() < EPSILON);
407    }
408
409    #[test]
410    fn voronoi_nearest_deterministic() {
411        let seeds = [(0.0_f32, 0.0), (1.0, 0.0), (0.5, 0.5)];
412        let a = voronoi_nearest_2d((0.3, 0.3), &seeds);
413        let b = voronoi_nearest_2d((0.3, 0.3), &seeds);
414        assert_eq!(a, b);
415    }
416
417    // ── voronoi_f1_f2_2d ──────────────────────────────────────────────────────
418
419    #[test]
420    fn voronoi_f1_f2_empty_is_none() {
421        assert!(voronoi_f1_f2_2d((0.0, 0.0), &[]).is_none());
422    }
423
424    #[test]
425    fn voronoi_f1_f2_two_seeds_ordering() {
426        let seeds = [(0.0_f32, 0.0), (1.0, 0.0)];
427        let (f1, f2) = voronoi_f1_f2_2d((0.3, 0.0), &seeds).unwrap();
428        assert!(f1 < f2, "f1={f1} f2={f2}");
429        assert!((f1 - 0.3).abs() < EPSILON, "f1={f1}");
430        assert!((f2 - 0.7).abs() < EPSILON, "f2={f2}");
431    }
432
433    #[test]
434    fn voronoi_f1_f2_deterministic() {
435        let seeds = [(0.0_f32, 0.0), (1.0, 0.0), (0.5, 0.5)];
436        let a = voronoi_f1_f2_2d((0.3, 0.3), &seeds);
437        let b = voronoi_f1_f2_2d((0.3, 0.3), &seeds);
438        assert_eq!(a, b);
439    }
440
441    // ── lloyd_relax_step_2d ───────────────────────────────────────────────────
442
443    #[test]
444    fn lloyd_relax_empty_seeds() {
445        assert!(lloyd_relax_step_2d(&[], &[(0.5, 0.5)]).is_empty());
446    }
447
448    #[test]
449    fn lloyd_relax_no_samples_unchanged() {
450        let seeds = vec![(0.0_f32, 0.0), (1.0, 0.0)];
451        let relaxed = lloyd_relax_step_2d(&seeds, &[]);
452        assert_eq!(relaxed[0], seeds[0]);
453        assert_eq!(relaxed[1], seeds[1]);
454    }
455
456    #[test]
457    fn lloyd_relax_preserves_length() {
458        let seeds: Vec<(f32, f32)> = (0..5).map(|i| (i as f32 * 0.2, 0.0)).collect();
459        let samples: Vec<(f32, f32)> = (0..100).map(|i| (i as f32 * 0.01, 0.0)).collect();
460        let relaxed = lloyd_relax_step_2d(&seeds, &samples);
461        assert_eq!(relaxed.len(), seeds.len());
462    }
463
464    #[test]
465    fn lloyd_relax_two_seeds_move_toward_centroids() {
466        // Seeds at 0.1 and 0.9; symmetric samples in [0,1] on x-axis
467        let seeds = vec![(0.1_f32, 0.0), (0.9_f32, 0.0)];
468        let samples: Vec<(f32, f32)> = (0..=100).map(|i| (i as f32 / 100.0, 0.0)).collect();
469        let relaxed = lloyd_relax_step_2d(&seeds, &samples);
470        // Each seed should move toward 0.25 and 0.75 (centroids of [0,0.5] and [0.5,1])
471        assert!((relaxed[0].0 - 0.25).abs() < 0.05, "seed0={}", relaxed[0].0);
472        assert!((relaxed[1].0 - 0.75).abs() < 0.05, "seed1={}", relaxed[1].0);
473    }
474
475    #[test]
476    fn lloyd_relax_deterministic() {
477        let seeds = vec![(0.1_f32, 0.2), (0.8_f32, 0.7)];
478        let samples: Vec<(f32, f32)> = (0..5).flat_map(|i| {
479            (0..5).map(move |j| (i as f32 * 0.25, j as f32 * 0.25))
480        }).collect();
481        let a = lloyd_relax_step_2d(&seeds, &samples);
482        let b = lloyd_relax_step_2d(&seeds, &samples);
483        assert_eq!(a, b);
484    }
485
486    // ── delaunay_2d ──────────────────────────────────────────────────────────
487
488    #[test]
489    fn delaunay_single_triangle() {
490        let pts = vec![(0.0_f32, 0.0), (1.0, 0.0), (0.5, 1.0)];
491        let tris = delaunay_2d(&pts);
492        assert_eq!(tris.len(), 1);
493    }
494
495    #[test]
496    fn delaunay_four_points_two_triangles() {
497        let pts = vec![(0.0_f32, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
498        let tris = delaunay_2d(&pts);
499        assert_eq!(tris.len(), 2);
500    }
501
502    #[test]
503    fn delaunay_empty() {
504        let pts: Vec<(f32, f32)> = vec![];
505        let tris = delaunay_2d(&pts);
506        assert!(tris.is_empty());
507    }
508
509    #[test]
510    fn delaunay_two_points() {
511        let pts = vec![(0.0_f32, 0.0), (1.0, 0.0)];
512        let tris = delaunay_2d(&pts);
513        assert!(tris.is_empty());
514    }
515
516    #[test]
517    fn delaunay_circumcircle_property() {
518        // For every Delaunay triangle, no other point should be inside its circumcircle
519        let pts = vec![(0.0_f32, 0.0), (4.0, 0.0), (2.0, 3.0), (1.0, 1.0), (3.0, 1.0)];
520        let tris = delaunay_2d(&pts);
521        assert!(!tris.is_empty());
522        for &(i, j, k) in &tris {
523            let (ax, ay) = pts[i];
524            let (bx, by) = pts[j];
525            let (cx, cy) = pts[k];
526            for (m, &(px, py)) in pts.iter().enumerate() {
527                if m == i || m == j || m == k { continue; }
528                assert!(
529                    !in_circumcircle(px, py, ax, ay, bx, by, cx, cy),
530                    "point {m} inside circumcircle of triangle ({i},{j},{k})"
531                );
532            }
533        }
534    }
535
536    #[test]
537    fn delaunay_deterministic() {
538        let pts = vec![(0.0_f32, 0.0), (1.0, 0.0), (0.5, 1.0), (0.5, 0.5)];
539        let a = delaunay_2d(&pts);
540        let b = delaunay_2d(&pts);
541        assert_eq!(a, b);
542    }
543}