use alloc::vec::Vec;
use core::fmt;
use crate::cube::is_inside;
use crate::{Real, Sdf, Shape3, vec3};
const SAME_POINT_REL: f64 = super::ValidateConfig::WELD_EPSILON_REL;
const PARALLEL_REL: f64 = 1e-12;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SealingReport {
pub samples: u64,
pub air_samples: u64,
pub probes: u64,
pub field_walls: u64,
pub mesh_walls: u64,
pub unsealed_walls: u64,
pub unsealed_on_domain_face: u64,
pub spurious_walls: u64,
pub field_air_components: u64,
pub mesh_air_components: u64,
pub mesh_regions: u64,
pub mixed_regions: u64,
pub merged_crossings: u64,
pub coplanar_probes: u64,
pub degenerate_triangles: u64,
pub boundary_samples: u64,
pub endpoint_crossings: u64,
pub degenerate_probes: u64,
}
impl SealingReport {
#[must_use]
pub const fn agrees(&self) -> bool {
self.unsealed_walls == 0
&& self.spurious_walls == 0
&& self.mixed_regions == 0
&& self.field_air_components == self.mesh_air_components
}
}
impl fmt::Display for SealingReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"sealing: {} probes, {} field walls, {} mesh walls",
self.probes, self.field_walls, self.mesh_walls
)?;
writeln!(
f,
" holes (unsealed) {} ({} on a domain face) membranes (spurious) {} \
mixed regions {}",
self.unsealed_walls,
self.unsealed_on_domain_face,
self.spurious_walls,
self.mixed_regions
)?;
writeln!(
f,
" air components: field {} / mesh {} ({} regions over {} samples)",
self.field_air_components, self.mesh_air_components, self.mesh_regions, self.samples
)?;
writeln!(
f,
" merged crossings {} coplanar probes {} zero-area triangles {}",
self.merged_crossings, self.coplanar_probes, self.degenerate_triangles
)?;
write!(
f,
" {} samples on the surface, {} endpoint crossings, {} probes set aside -> {}",
self.boundary_samples,
self.endpoint_crossings,
self.degenerate_probes,
if self.agrees() { "SEALED" } else { "DISAGREES" }
)
}
}
#[must_use]
pub fn sealing<R, S>(
field: &S,
shape: &impl Shape3,
origin: [R; 3],
cell_size: R,
positions: &[[R; 3]],
indices: &[u32],
) -> SealingReport
where
R: Real,
S: Sdf<Scalar = R>,
{
let size = shape.size();
let count = shape.element_count();
let at = |i: [u32; 3]| {
[
origin[0] + cell_size * R::from_f64(f64::from(i[0])),
origin[1] + cell_size * R::from_f64(f64::from(i[1])),
origin[2] + cell_size * R::from_f64(f64::from(i[2])),
]
};
let mut air = Vec::with_capacity(count);
let mut on_surface = Vec::with_capacity(count);
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
let v = field.sample(at([x, y, z]));
air.push(!is_inside(v));
#[allow(
clippy::float_cmp,
reason = "the exact tie is the condition being detected"
)]
on_surface.push(v == R::ZERO);
}
}
}
let bins = CellBins::build(size, origin, cell_size, positions, indices);
let mut field_uf = UnionFind::new(count);
let mut mesh_uf = UnionFind::new(count);
let mut air_uf = UnionFind::new(count);
let mut report = SealingReport {
samples: count as u64,
air_samples: air.iter().filter(|a| **a).count() as u64,
probes: 0,
field_walls: 0,
mesh_walls: 0,
unsealed_walls: 0,
unsealed_on_domain_face: 0,
spurious_walls: 0,
field_air_components: 0,
mesh_air_components: 0,
mesh_regions: 0,
mixed_regions: 0,
merged_crossings: 0,
coplanar_probes: 0,
degenerate_triangles: bins.degenerate,
boundary_samples: on_surface.iter().filter(|b| **b).count() as u64,
endpoint_crossings: 0,
degenerate_probes: 0,
};
let mut probe = Probe::new(bins.triangles);
let same_point = R::from_f64(SAME_POINT_REL);
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
let from = [x, y, z];
let i = shape.linearize(from) as usize;
for axis in 0..3 {
let mut to = from;
to[axis] += 1;
if to[axis] >= size[axis] {
continue;
}
let j = shape.linearize(to) as usize;
report.probes += 1;
let (crossings, merged, coplanar, on_endpoint) = probe
.crossings(&bins, positions, indices, size, from, axis, at, same_point);
report.merged_crossings += merged;
report.coplanar_probes += coplanar;
if on_endpoint {
report.endpoint_crossings += 1;
}
if on_endpoint
|| on_surface.get(i) == Some(&true)
|| on_surface.get(j) == Some(&true)
{
report.degenerate_probes += 1;
continue;
}
let mesh_separates = crossings % 2 == 1;
let field_separates = air[i] != air[j];
if field_separates {
report.field_walls += 1;
}
if mesh_separates {
report.mesh_walls += 1;
}
match (field_separates, mesh_separates) {
(true, false) => {
report.unsealed_walls += 1;
if (0..3).any(|a| from[a] == 0 || to[a] == size[a] - 1) {
report.unsealed_on_domain_face += 1;
}
}
(false, true) => report.spurious_walls += 1,
_ => {}
}
if !field_separates && air[i] {
field_uf.union(i, j);
}
if !mesh_separates {
mesh_uf.union(i, j);
if air[i] && air[j] {
air_uf.union(i, j);
}
}
}
}
}
}
for (i, &is_air) in air.iter().enumerate() {
if is_air && field_uf.find(i) == i {
report.field_air_components += 1;
}
if is_air && air_uf.find(i) == i {
report.mesh_air_components += 1;
}
}
let mut seen_air = alloc::vec![false; count];
let mut seen_solid = alloc::vec![false; count];
for (i, &is_air) in air.iter().enumerate() {
let root = mesh_uf.find(i);
let side = if is_air {
&mut seen_air
} else {
&mut seen_solid
};
if let Some(slot) = side.get_mut(root) {
*slot = true;
}
}
for i in 0..count {
if mesh_uf.find(i) == i {
report.mesh_regions += 1;
if seen_air.get(i) == Some(&true) && seen_solid.get(i) == Some(&true) {
report.mixed_regions += 1;
}
}
}
report
}
struct CellBins {
cells: [u32; 3],
starts: Vec<u32>,
items: Vec<u32>,
triangles: usize,
degenerate: u64,
}
impl CellBins {
fn build<R: Real>(
size: [u32; 3],
origin: [R; 3],
cell_size: R,
positions: &[[R; 3]],
indices: &[u32],
) -> Self {
let cells = [
size[0].saturating_sub(1).max(1),
size[1].saturating_sub(1).max(1),
size[2].saturating_sub(1).max(1),
];
let bin_count = cells[0] as usize * cells[1] as usize * cells[2] as usize;
let triangles = indices.len() / 3;
let range = |t: usize| -> Option<([u32; 3], [u32; 3])> {
let tri = [
*positions.get(*indices.get(t * 3)? as usize)?,
*positions.get(*indices.get(t * 3 + 1)? as usize)?,
*positions.get(*indices.get(t * 3 + 2)? as usize)?,
];
if has_no_area(tri) {
return None;
}
let mut lo = [0u32; 3];
let mut hi = [0u32; 3];
for a in 0..3 {
let min = tri[0][a].min(tri[1][a]).min(tri[2][a]);
let max = tri[0][a].max(tri[1][a]).max(tri[2][a]);
if !min.is_finite() || !max.is_finite() {
return None;
}
let to_cell = |v: R| {
let s = ((v - origin[a]) / cell_size).floor().as_f64();
s.clamp(0.0, f64::from(cells[a] - 1)) as u32
};
lo[a] = to_cell(min).saturating_sub(1);
hi[a] = (to_cell(max) + 1).min(cells[a] - 1);
}
Some((lo, hi))
};
let index = |c: [u32; 3]| {
(c[2] as usize * cells[1] as usize + c[1] as usize) * cells[0] as usize + c[0] as usize
};
let mut counts = alloc::vec![0u32; bin_count + 1];
for t in 0..triangles {
if let Some((lo, hi)) = range(t) {
for cz in lo[2]..=hi[2] {
for cy in lo[1]..=hi[1] {
for cx in lo[0]..=hi[0] {
counts[index([cx, cy, cz]) + 1] += 1;
}
}
}
}
}
for i in 1..counts.len() {
counts[i] += counts[i - 1];
}
let starts = counts.clone();
let total = *counts.last().unwrap_or(&0) as usize;
let mut items = alloc::vec![0u32; total];
let mut cursor = counts;
for t in 0..triangles {
if let Some((lo, hi)) = range(t) {
for cz in lo[2]..=hi[2] {
for cy in lo[1]..=hi[1] {
for cx in lo[0]..=hi[0] {
let b = index([cx, cy, cz]);
if let Some(slot) = items.get_mut(cursor[b] as usize) {
*slot = t as u32;
}
cursor[b] += 1;
}
}
}
}
}
let degenerate = (0..triangles).filter(|t| range(*t).is_none()).count() as u64;
Self {
cells,
starts,
items,
triangles,
degenerate,
}
}
fn cell(&self, c: [u32; 3]) -> &[u32] {
let b = (c[2] as usize * self.cells[1] as usize + c[1] as usize) * self.cells[0] as usize
+ c[0] as usize;
let (Some(&lo), Some(&hi)) = (self.starts.get(b), self.starts.get(b + 1)) else {
return &[];
};
self.items.get(lo as usize..hi as usize).unwrap_or(&[])
}
}
struct Probe {
stamp: Vec<u64>,
epoch: u64,
hits: Vec<f64>,
}
impl Probe {
fn new(triangles: usize) -> Self {
Self {
stamp: alloc::vec![0u64; triangles],
epoch: 0,
hits: Vec::new(),
}
}
#[allow(clippy::too_many_arguments, reason = "one probe, fully specified")]
fn crossings<R: Real>(
&mut self,
bins: &CellBins,
positions: &[[R; 3]],
indices: &[u32],
size: [u32; 3],
from: [u32; 3],
axis: usize,
at: impl Fn([u32; 3]) -> [R; 3],
same_point: R,
) -> (u64, u64, u64, bool) {
self.epoch += 1;
self.hits.clear();
let mut to = from;
to[axis] += 1;
let p0 = at(from);
let p1 = at(to);
let d = vec3::sub(p1, p0);
let mut coplanar = 0u64;
let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
let mut cell = [0u32; 3];
cell[axis] = from[axis].min(bins.cells[axis] - 1);
for du in 0..2u32 {
let Some(cu) = (from[u] + du).checked_sub(1) else {
continue;
};
if cu >= bins.cells[u] || from[u] + du > size[u] {
continue;
}
for dv in 0..2u32 {
let Some(cv) = (from[v] + dv).checked_sub(1) else {
continue;
};
if cv >= bins.cells[v] || from[v] + dv > size[v] {
continue;
}
cell[u] = cu;
cell[v] = cv;
for &t in bins.cell(cell) {
let slot = t as usize;
if self.stamp.get(slot).copied() == Some(self.epoch) {
continue;
}
if let Some(s) = self.stamp.get_mut(slot) {
*s = self.epoch;
}
match segment_triangle(p0, d, positions, indices, slot) {
Hit::At(t) => self.hits.push(t.as_f64()),
Hit::Coplanar => coplanar += 1,
Hit::Miss => {}
}
}
}
}
let raw = self.hits.len() as u64;
self.hits.sort_by(f64::total_cmp);
let tol = same_point.as_f64();
let mut distinct = 0u64;
let mut last = f64::NEG_INFINITY;
let mut on_endpoint = false;
for &t in &self.hits {
if distinct == 0 || t - last > tol {
distinct += 1;
last = t;
on_endpoint |= t <= tol || t >= 1.0 - tol;
}
}
(distinct, raw - distinct, coplanar, on_endpoint)
}
}
fn has_no_area<R: Real>(tri: [[R; 3]; 3]) -> bool {
let e1 = vec3::sub(tri[1], tri[0]);
let e2 = vec3::sub(tri[2], tri[0]);
vec3::length(vec3::cross(e1, e2))
<= R::from_f64(PARALLEL_REL) * vec3::length(e1) * vec3::length(e2)
}
enum Hit<R> {
At(R),
Coplanar,
Miss,
}
fn segment_triangle<R: Real>(
p0: [R; 3],
d: [R; 3],
positions: &[[R; 3]],
indices: &[u32],
t: usize,
) -> Hit<R> {
let (Some(&ia), Some(&ib), Some(&ic)) = (
indices.get(t * 3),
indices.get(t * 3 + 1),
indices.get(t * 3 + 2),
) else {
return Hit::Miss;
};
let (Some(&a), Some(&b), Some(&c)) = (
positions.get(ia as usize),
positions.get(ib as usize),
positions.get(ic as usize),
) else {
return Hit::Miss;
};
let e1 = vec3::sub(b, a);
let e2 = vec3::sub(c, a);
let h = vec3::cross(d, e2);
let det = vec3::dot(e1, h);
let scale = vec3::length(d) * vec3::length(e1) * vec3::length(e2);
if det.abs() <= R::from_f64(PARALLEL_REL) * scale {
let s = vec3::sub(p0, a);
let n = vec3::cross(e1, e2);
return if vec3::dot(s, n).abs()
<= R::from_f64(PARALLEL_REL) * vec3::length(n) * vec3::length(d)
{
Hit::Coplanar
} else {
Hit::Miss
};
}
let inv = det.recip();
let s = vec3::sub(p0, a);
let u = vec3::dot(s, h) * inv;
if u < R::ZERO || u > R::ONE {
return Hit::Miss;
}
let q = vec3::cross(s, e1);
let v = vec3::dot(d, q) * inv;
if v < R::ZERO || u + v > R::ONE {
return Hit::Miss;
}
let t = vec3::dot(e2, q) * inv;
if t < R::ZERO || t > R::ONE {
return Hit::Miss;
}
Hit::At(t)
}
struct UnionFind {
parent: Vec<u32>,
size: Vec<u32>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n as u32).collect(),
size: alloc::vec![1u32; n],
}
}
fn find(&mut self, mut i: usize) -> usize {
while self.parent.get(i).copied().map(|p| p as usize) != Some(i) {
let Some(p) = self.parent.get(i).copied() else {
return i;
};
let grand = self.parent.get(p as usize).copied().unwrap_or(p);
if let Some(slot) = self.parent.get_mut(i) {
*slot = grand;
}
i = grand as usize;
}
i
}
fn union(&mut self, a: usize, b: usize) {
let (mut ra, mut rb) = (self.find(a), self.find(b));
if ra == rb {
return;
}
if self.size.get(ra).copied().unwrap_or(0) < self.size.get(rb).copied().unwrap_or(0) {
core::mem::swap(&mut ra, &mut rb);
}
if let Some(slot) = self.parent.get_mut(rb) {
*slot = ra as u32;
}
let grew = self.size.get(rb).copied().unwrap_or(0);
if let Some(slot) = self.size.get_mut(ra) {
*slot += grew;
}
}
}
#[cfg(test)]
mod tests;