use crate::mesh::Mesh;
use glam::DVec3;
use pantometry_units::{Length, LengthVec, Volume};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Loss {
pub volume_error: f64,
pub boundary_fraction: f64,
pub thin_runs: usize,
pub small_triangles: usize,
pub retried_rows: usize,
pub ambiguous_rows: usize,
}
impl Loss {
pub fn is_clean(&self) -> bool {
self.volume_error.abs() < 0.02 && self.thin_runs == 0 && self.ambiguous_rows == 0
}
}
#[derive(Clone, Debug)]
pub struct Voxels {
counts: (usize, usize, usize),
dx: f64,
origin: DVec3,
inside: Vec<bool>,
loss: Loss,
}
impl Voxels {
pub fn of(mesh: &Mesh, cell: Length) -> Result<Voxels, String> {
let dx = cell.to_si();
if !(dx.is_finite() && dx > 0.0) {
return Err(format!("a cell size must be finite and positive, is {dx}"));
}
if !mesh.is_closed() {
return Err(
"the mesh is not closed: some edge is not shared by exactly two triangles, so a ray \
can pass through the surface and parity cannot say what is inside. STL stores no \
topology, so this is matched on the vertices as written — see `Mesh::is_closed`"
.to_string(),
);
}
let (low, high) = mesh
.bounds()
.ok_or_else(|| "an empty mesh has no bounds to rasterise".to_string())?;
let (low, high) = (low.to_si(), high.to_si());
let span = high - low;
let counts = (
((span.x / dx).ceil() as usize + 2).max(1),
((span.y / dx).ceil() as usize + 2).max(1),
((span.z / dx).ceil() as usize + 2).max(1),
);
let grid = DVec3::new(
counts.0 as f64 * dx,
counts.1 as f64 * dx,
counts.2 as f64 * dx,
);
let origin = low - (grid - span) * 0.5;
Ok(Voxels::rasterise(mesh, origin, counts, dx))
}
pub fn onto(
mesh: &Mesh,
origin: LengthVec,
counts: (usize, usize, usize),
cell: Length,
) -> Result<Voxels, String> {
let dx = cell.to_si();
if !(dx.is_finite() && dx > 0.0) {
return Err(format!("a cell size must be finite and positive, is {dx}"));
}
if counts.0 == 0 || counts.1 == 0 || counts.2 == 0 {
return Err(format!("a grid of {counts:?} cells holds nothing"));
}
if !mesh.is_closed() {
return Err(
"the mesh is not closed: some edge is not shared by exactly two triangles, so a \
ray can pass through the surface and parity cannot say what is inside"
.to_string(),
);
}
let (low, high) = mesh
.bounds()
.ok_or_else(|| "an empty mesh has no bounds to rasterise".to_string())?;
let (low, high) = (low.to_si(), high.to_si());
let o = origin.to_si();
let far = o + DVec3::new(
counts.0 as f64 * dx,
counts.1 as f64 * dx,
counts.2 as f64 * dx,
);
let fits = low.x >= o.x
&& low.y >= o.y
&& low.z >= o.z
&& high.x <= far.x
&& high.y <= far.y
&& high.z <= far.z;
if !fits {
return Err(format!(
"the mesh spans ({:.4}, {:.4}, {:.4}) to ({:.4}, {:.4}, {:.4}) m and the grid \
covers ({:.4}, {:.4}, {:.4}) to ({:.4}, {:.4}, {:.4}) m, so part of it would be \
cut off — a part with its corner missing runs and audits and answers about a \
different shape, so it is refused rather than cropped",
low.x, low.y, low.z, high.x, high.y, high.z, o.x, o.y, o.z, far.x, far.y, far.z
));
}
Ok(Voxels::rasterise(mesh, o, counts, dx))
}
fn rasterise(mesh: &Mesh, origin: DVec3, counts: (usize, usize, usize), dx: f64) -> Voxels {
let mut inside = vec![false; counts.0 * counts.1 * counts.2];
let mut ambiguous_rows = 0;
let mut retried_rows = 0;
const NUDGE: [(f64, f64); 4] = [(0.0, 0.0), (0.19, 0.07), (-0.11, 0.23), (0.31, -0.29)];
let mut crossings: Vec<f64> = Vec::new();
for k in 0..counts.2 {
for j in 0..counts.1 {
let mut filled = false;
for (attempt, (dy, dz)) in NUDGE.into_iter().enumerate() {
let y = origin.y + (j as f64 + 0.5 + dy) * dx;
let z = origin.z + (k as f64 + 0.5 + dz) * dx;
crossings.clear();
let mut degenerate = false;
for t in mesh.triangles() {
match hit_x(t.a, t.b, t.c, y, z) {
Hit::Miss => {}
Hit::At(x) => crossings.push(x),
Hit::Degenerate => {
degenerate = true;
break;
}
}
}
#[allow(clippy::manual_is_multiple_of)]
let odd = crossings.len() % 2 != 0;
if degenerate || odd {
continue;
}
crossings.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
for pair in crossings.chunks_exact(2) {
for i in 0..counts.0 {
let x = origin.x + (i as f64 + 0.5) * dx;
if x > pair[0] && x < pair[1] {
inside[i + counts.0 * (j + counts.1 * k)] = true;
}
}
}
if attempt > 0 {
retried_rows += 1;
}
filled = true;
break;
}
if !filled {
ambiguous_rows += 1;
}
}
}
let mut voxels = Voxels {
counts,
dx,
origin,
inside,
loss: Loss {
volume_error: 0.0,
boundary_fraction: 0.0,
thin_runs: 0,
small_triangles: mesh.triangles_below(Length::from_si(dx)),
retried_rows,
ambiguous_rows,
},
};
let meshed = mesh.volume().to_si();
voxels.loss.volume_error = if meshed != 0.0 {
voxels.volume().to_si() / meshed - 1.0
} else {
f64::NAN
};
voxels.loss.boundary_fraction = voxels.boundary_share();
voxels.loss.thin_runs = voxels.count_thin_runs();
voxels
}
pub fn counts(&self) -> (usize, usize, usize) {
self.counts
}
pub fn cell(&self) -> Length {
Length::from_si(self.dx)
}
pub fn origin(&self) -> LengthVec {
LengthVec::from_si(self.origin)
}
pub fn contains(&self, i: usize, j: usize, k: usize) -> bool {
if i >= self.counts.0 || j >= self.counts.1 || k >= self.counts.2 {
return false;
}
self.inside[i + self.counts.0 * (j + self.counts.1 * k)]
}
pub fn filled(&self) -> usize {
self.inside.iter().filter(|b| **b).count()
}
pub fn volume(&self) -> Volume {
Volume::from_si(self.filled() as f64 * self.dx.powi(3))
}
pub fn loss(&self) -> Loss {
self.loss
}
fn boundary_share(&self) -> f64 {
let filled = self.filled();
if filled == 0 {
return 0.0;
}
let (nx, ny, nz) = self.counts;
let mut on_surface = 0;
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
if !self.contains(i, j, k) {
continue;
}
let exposed = i == 0
|| j == 0
|| k == 0
|| !self.contains(i - 1, j, k)
|| !self.contains(i + 1, j, k)
|| !self.contains(i, j - 1, k)
|| !self.contains(i, j + 1, k)
|| !self.contains(i, j, k - 1)
|| !self.contains(i, j, k + 1);
if exposed {
on_surface += 1;
}
}
}
}
on_surface as f64 / filled as f64
}
fn count_thin_runs(&self) -> usize {
let (nx, ny, nz) = self.counts;
let mut thin = 0;
let mut tally = |run: usize| {
if run == 1 || run == 2 {
thin += 1;
}
};
for k in 0..nz {
for j in 0..ny {
let mut run = 0;
for i in 0..nx {
if self.contains(i, j, k) {
run += 1;
} else {
tally(run);
run = 0;
}
}
tally(run);
}
}
for k in 0..nz {
for i in 0..nx {
let mut run = 0;
for j in 0..ny {
if self.contains(i, j, k) {
run += 1;
} else {
tally(run);
run = 0;
}
}
tally(run);
}
}
for j in 0..ny {
for i in 0..nx {
let mut run = 0;
for k in 0..nz {
if self.contains(i, j, k) {
run += 1;
} else {
tally(run);
run = 0;
}
}
tally(run);
}
}
thin
}
}
enum Hit {
Miss,
At(f64),
Degenerate,
}
fn hit_x(a: DVec3, b: DVec3, c: DVec3, y: f64, z: f64) -> Hit {
const ON_EDGE: f64 = 1e-9;
const GRAZING: f64 = 1e-9;
let origin = DVec3::new(0.0, y, z);
let direction = DVec3::X;
let (e1, e2) = (b - a, c - a);
let twice_area = e1.cross(e2).length();
if twice_area == 0.0 {
return Hit::Miss;
}
let h = direction.cross(e2);
let det = e1.dot(h);
let along = det / twice_area;
if along == 0.0 {
return Hit::Miss;
}
if along.abs() < GRAZING {
return Hit::Degenerate;
}
let inv = 1.0 / det;
let s = origin - a;
let u = inv * s.dot(h);
let q = s.cross(e1);
let v = inv * direction.dot(q);
let w = 1.0 - u - v;
if u < -ON_EDGE || v < -ON_EDGE || w < -ON_EDGE {
return Hit::Miss;
}
if u < ON_EDGE || v < ON_EDGE || w < ON_EDGE {
return Hit::Degenerate;
}
Hit::At(inv * e2.dot(q))
}