use std::collections::VecDeque;
const NB: [(i32, i32, i32); 6] = [
(1, 0, 0),
(-1, 0, 0),
(0, 1, 0),
(0, -1, 0),
(0, 0, 1),
(0, 0, -1),
];
pub struct DistanceField {
min: (i32, i32, i32),
dims: (usize, usize, usize),
depth: Vec<i32>,
}
impl DistanceField {
fn lin(&self, dx: usize, dy: usize, dz: usize) -> usize {
let (w, h, _) = self.dims;
(dz * h + dy) * w + dx
}
fn local(&self, x: i32, y: i32, z: i32) -> Option<(usize, usize, usize)> {
let (dx, dy, dz) = (x - self.min.0, y - self.min.1, z - self.min.2);
if dx < 0 || dy < 0 || dz < 0 {
return None;
}
let (dx, dy, dz) = (dx as usize, dy as usize, dz as usize);
let (w, h, d) = self.dims;
if dx >= w || dy >= h || dz >= d {
return None;
}
Some((dx, dy, dz))
}
pub fn from_occupancy(
min: (i32, i32, i32),
max: (i32, i32, i32),
occupied: impl Fn(i32, i32, i32) -> bool,
) -> Self {
let dims = (
(max.0 - min.0 + 1).max(0) as usize,
(max.1 - min.1 + 1).max(0) as usize,
(max.2 - min.2 + 1).max(0) as usize,
);
let (w, h, d) = (dims.0 as i32, dims.1 as i32, dims.2 as i32);
let lin = |x: i32, y: i32, z: i32| ((z * h + y) * w + x) as usize;
let inb = |x: i32, y: i32, z: i32| x >= 0 && y >= 0 && z >= 0 && x < w && y < h && z < d;
let mut depth = vec![0i32; dims.0 * dims.1 * dims.2];
for z in 0..d {
for y in 0..h {
for x in 0..w {
if occupied(min.0 + x, min.1 + y, min.2 + z) {
depth[lin(x, y, z)] = -1;
}
}
}
}
let mut q: VecDeque<(i32, i32, i32)> = VecDeque::new();
for z in 0..d {
for y in 0..h {
for x in 0..w {
if depth[lin(x, y, z)] != -1 {
continue;
}
let surface = NB.iter().any(|&(dx, dy, dz)| {
let (nx, ny, nz) = (x + dx, y + dy, z + dz);
!inb(nx, ny, nz) || depth[lin(nx, ny, nz)] == 0
});
if surface {
depth[lin(x, y, z)] = 1;
q.push_back((x, y, z));
}
}
}
}
while let Some((x, y, z)) = q.pop_front() {
let cur = depth[lin(x, y, z)];
for &(dx, dy, dz) in &NB {
let (nx, ny, nz) = (x + dx, y + dy, z + dz);
if inb(nx, ny, nz) && depth[lin(nx, ny, nz)] == -1 {
depth[lin(nx, ny, nz)] = cur + 1;
q.push_back((nx, ny, nz));
}
}
}
DistanceField { min, dims, depth }
}
pub fn depth_at(&self, x: i32, y: i32, z: i32) -> i32 {
self.local(x, y, z)
.map(|(dx, dy, dz)| self.depth[self.lin(dx, dy, dz)].max(0))
.unwrap_or(0)
}
pub fn normal_at(&self, x: i32, y: i32, z: i32) -> (f64, f64, f64) {
let d = |xx, yy, zz| self.depth_at(xx, yy, zz) as f64;
let gx = d(x + 1, y, z) - d(x - 1, y, z);
let gy = d(x, y + 1, z) - d(x, y - 1, z);
let gz = d(x, y, z + 1) - d(x, y, z - 1);
let (nx, ny, nz) = (-gx, -gy, -gz);
let len = (nx * nx + ny * ny + nz * nz).sqrt();
if len < 1e-9 {
(0.0, 1.0, 0.0)
} else {
(nx / len, ny / len, nz / len)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn depth_grows_inward_and_top_normal_points_up() {
let f = DistanceField::from_occupancy((0, 0, 0), (6, 6, 6), |_, _, _| true);
assert_eq!(f.depth_at(0, 0, 0), 1); assert_eq!(f.depth_at(3, 3, 3), 4); assert_eq!(f.depth_at(3, 6, 3), 1); assert_eq!(f.depth_at(-1, 0, 0), 0); let (_, ny, _) = f.normal_at(3, 6, 3);
assert!(ny > 0.7, "top normal should point up, got ny={ny}");
}
}