use crate::rng::DetRng;
pub type Point = [f64; 2];
pub fn poisson_disk(w: f64, h: f64, radius: f64, k: usize, rng: &mut impl DetRng) -> Vec<Point> {
let cell = radius / std::f64::consts::SQRT_2;
let gw = ((w / cell).ceil() as usize).max(1);
let gh = ((h / cell).ceil() as usize).max(1);
let mut grid: Vec<Option<usize>> = vec![None; gw * gh];
let mut samples: Vec<Point> = Vec::new();
let mut active: Vec<usize> = Vec::new();
let grid_cell = |p: Point| -> (usize, usize) {
let gx = ((p[0] / cell) as usize).min(gw - 1);
let gy = ((p[1] / cell) as usize).min(gh - 1);
(gx, gy)
};
let fits = |p: Point, samples: &[Point], grid: &[Option<usize>]| -> bool {
let (cx, cy) = grid_cell(p);
for dy in -2i32..=2 {
for dx in -2i32..=2 {
let (nx, ny) = (cx as i32 + dx, cy as i32 + dy);
if nx < 0 || ny < 0 || nx >= gw as i32 || ny >= gh as i32 {
continue;
}
if let Some(si) = grid[ny as usize * gw + nx as usize] {
let s = samples[si];
if (s[0] - p[0]).powi(2) + (s[1] - p[1]).powi(2) < radius * radius {
return false;
}
}
}
}
true
};
let first = [rng.unit() * w, rng.unit() * h];
let (fx, fy) = grid_cell(first);
grid[fy * gw + fx] = Some(0);
samples.push(first);
active.push(0);
while !active.is_empty() {
let ai = rng.below(active.len());
let center = samples[active[ai]];
let mut placed = false;
for _ in 0..k {
let ang = rng.unit() * std::f64::consts::TAU;
let rad = radius * (1.0 + rng.unit());
let cand = [center[0] + rad * ang.cos(), center[1] + rad * ang.sin()];
if cand[0] < 0.0 || cand[0] >= w || cand[1] < 0.0 || cand[1] >= h {
continue;
}
if fits(cand, &samples, &grid) {
let idx = samples.len();
let (cx, cy) = grid_cell(cand);
grid[cy * gw + cx] = Some(idx);
samples.push(cand);
active.push(idx);
placed = true;
break;
}
}
if !placed {
active.swap_remove(ai); }
}
samples
}
pub fn delaunay_edges(points: &[Point]) -> Vec<(usize, usize)> {
let n = points.len();
if n < 2 {
return Vec::new();
}
if n == 2 {
return vec![(0, 1)];
}
let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in points {
minx = minx.min(p[0]);
miny = miny.min(p[1]);
maxx = maxx.max(p[0]);
maxy = maxy.max(p[1]);
}
let dmax = (maxx - minx).max(maxy - miny).max(1.0);
let (midx, midy) = ((minx + maxx) / 2.0, (miny + maxy) / 2.0);
let mut pts: Vec<Point> = points.to_vec();
pts.push([midx - 20.0 * dmax, midy - dmax]);
pts.push([midx, midy + 20.0 * dmax]);
pts.push([midx + 20.0 * dmax, midy - dmax]);
let mut tris: Vec<[usize; 3]> = vec![[n, n + 1, n + 2]];
for pi in 0..n {
let p = pts[pi];
let mut is_bad = vec![false; tris.len()];
for (ti, t) in tris.iter().enumerate() {
if in_circumcircle(pts[t[0]], pts[t[1]], pts[t[2]], p) {
is_bad[ti] = true;
}
}
let mut boundary: Vec<(usize, usize)> = Vec::new();
for ti in 0..tris.len() {
if !is_bad[ti] {
continue;
}
let t = tris[ti];
for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
let shared = (0..tris.len())
.any(|oj| oj != ti && is_bad[oj] && triangle_has_edge(tris[oj], a, b));
if !shared {
boundary.push((a, b));
}
}
}
let mut next: Vec<[usize; 3]> = Vec::with_capacity(tris.len());
for ti in 0..tris.len() {
if !is_bad[ti] {
next.push(tris[ti]);
}
}
for (a, b) in boundary {
next.push([a, b, pi]);
}
tris = next;
}
let mut edges: Vec<(usize, usize)> = Vec::new();
for t in &tris {
if t[0] >= n || t[1] >= n || t[2] >= n {
continue;
}
for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
edges.push((a.min(b), a.max(b)));
}
}
edges.sort_unstable();
edges.dedup();
edges
}
pub fn prune_to_max_degree(
points: &[Point],
edges: &[(usize, usize)],
max_degree: usize,
) -> Vec<(usize, usize)> {
let len2 = |e: &(usize, usize)| {
let (a, b) = (points[e.0], points[e.1]);
(a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
};
let mut degree = vec![0usize; points.len()];
for &(a, b) in edges {
degree[a] += 1;
degree[b] += 1;
}
let mut order: Vec<usize> = (0..edges.len()).collect();
order.sort_by(|&i, &j| {
len2(&edges[j])
.total_cmp(&len2(&edges[i]))
.then(edges[i].cmp(&edges[j]))
});
let mut removed = vec![false; edges.len()];
for &ei in &order {
let (a, b) = edges[ei];
if degree[a] > max_degree || degree[b] > max_degree {
removed[ei] = true;
degree[a] -= 1;
degree[b] -= 1;
}
}
edges
.iter()
.enumerate()
.filter(|(i, _)| !removed[*i])
.map(|(_, &e)| e)
.collect()
}
fn in_circumcircle(a: Point, b: Point, c: Point, p: Point) -> bool {
let orient = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
if orient.abs() < 1e-12 {
return false; }
let (ax, ay) = (a[0] - p[0], a[1] - p[1]);
let (bx, by) = (b[0] - p[0], b[1] - p[1]);
let (cx, cy) = (c[0] - p[0], c[1] - p[1]);
let det = (ax * ax + ay * ay) * (bx * cy - cx * by)
- (bx * bx + by * by) * (ax * cy - cx * ay)
+ (cx * cx + cy * cy) * (ax * by - bx * ay);
if orient > 0.0 {
det > 0.0
} else {
det < 0.0
}
}
fn triangle_has_edge(t: [usize; 3], a: usize, b: usize) -> bool {
let has = |x: usize| t[0] == x || t[1] == x || t[2] == x;
has(a) && has(b)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::seeded;
fn dist2(a: Point, b: Point) -> f64 {
(a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
}
#[test]
fn poisson_is_deterministic() {
let a = poisson_disk(200.0, 200.0, 16.0, 30, &mut seeded(9));
let b = poisson_disk(200.0, 200.0, 16.0, 30, &mut seeded(9));
assert_eq!(a, b, "same seed must reproduce the same sites");
}
#[test]
fn poisson_respects_spacing_and_bounds() {
let radius = 16.0;
let pts = poisson_disk(200.0, 200.0, radius, 30, &mut seeded(3));
assert!(pts.len() > 20, "should fill a 200x200 rect at spacing 16, got {}", pts.len());
for &p in &pts {
assert!(p[0] >= 0.0 && p[0] < 200.0 && p[1] >= 0.0 && p[1] < 200.0, "point out of rect");
}
for (i, &a) in pts.iter().enumerate() {
for &b in &pts[i + 1..] {
assert!(dist2(a, b) >= radius * radius * (1.0 - 1e-6), "two sites closer than radius");
}
}
}
#[test]
fn delaunay_small_cases() {
assert_eq!(delaunay_edges(&[[0.0, 0.0]]), Vec::new());
assert_eq!(delaunay_edges(&[[0.0, 0.0], [1.0, 0.0]]), vec![(0, 1)]);
let sq = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let e = delaunay_edges(&sq);
assert_eq!(e.len(), 5, "square triangulation has 5 edges, got {e:?}");
}
#[test]
fn delaunay_every_point_is_a_vertex() {
let pts = poisson_disk(180.0, 180.0, 20.0, 30, &mut seeded(11));
let edges = delaunay_edges(&pts);
let mut seen = vec![false; pts.len()];
for &(a, b) in &edges {
seen[a] = true;
seen[b] = true;
}
assert!(seen.iter().all(|&s| s), "every site must be a triangulation vertex");
}
#[test]
fn delaunay_is_deterministic() {
let pts = poisson_disk(180.0, 180.0, 20.0, 30, &mut seeded(11));
assert_eq!(delaunay_edges(&pts), delaunay_edges(&pts));
}
#[test]
fn prune_caps_degree_and_is_deterministic() {
let pts = poisson_disk(180.0, 180.0, 18.0, 30, &mut seeded(5));
let edges = delaunay_edges(&pts);
let pruned = prune_to_max_degree(&pts, &edges, 5);
let mut degree = vec![0usize; pts.len()];
for &(a, b) in &pruned {
degree[a] += 1;
degree[b] += 1;
}
assert!(degree.iter().all(|&d| d <= 5), "no node may exceed degree 5 after prune");
assert!(pruned.len() <= edges.len());
assert_eq!(pruned, prune_to_max_degree(&pts, &edges, 5), "prune must be deterministic");
}
#[test]
fn prune_leaves_already_bounded_graph_untouched() {
let pts = [[0.0, 0.0], [10.0, 0.0], [20.0, 0.0]];
let edges = vec![(0, 1), (1, 2)];
assert_eq!(prune_to_max_degree(&pts, &edges, 5), edges);
}
}