use ahash::AHashMap;
use axiolid_core::{Aabb, Point3, Scalar};
use axiolid_mesh::TriMesh;
#[derive(Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum LevelSetError {
#[error("edge length {0} is not a positive finite length")]
InvalidEdgeLength(Scalar),
#[error("bounds are empty or non-finite along at least one axis")]
InvalidBounds,
#[error("level {0} is not finite")]
InvalidLevel(Scalar),
#[error("the field does not cross level {level} anywhere in the bounds")]
NoCrossing {
level: Scalar,
},
#[error("the field returned a non-finite value at {point:?}")]
NonFiniteSample {
point: Point3,
},
#[error("the requested grid needs {requested} samples, over the {limit} budget")]
BudgetExceeded {
requested: usize,
limit: usize,
},
}
const SOS_DELTA: Scalar = 1.0e-3;
const MAX_SAMPLES: usize = 64_000_000;
const KUHN_TETRAHEDRA: [[usize; 4]; 6] = [
[0, 1, 3, 7],
[0, 1, 5, 7],
[0, 2, 3, 7],
[0, 2, 6, 7],
[0, 4, 5, 7],
[0, 4, 6, 7],
];
pub fn level_set<F>(
field: F,
bounds: Aabb,
edge_length: Scalar,
level: Scalar,
) -> Result<TriMesh, LevelSetError>
where
F: Fn(Point3) -> Scalar,
{
if !edge_length.is_finite() || edge_length <= 0.0 {
return Err(LevelSetError::InvalidEdgeLength(edge_length));
}
if !level.is_finite() {
return Err(LevelSetError::InvalidLevel(level));
}
let (min, max) = (bounds.min, bounds.max);
if !min.is_finite() || !max.is_finite() || max.x <= min.x || max.y <= min.y || max.z <= min.z {
return Err(LevelSetError::InvalidBounds);
}
let counts = [
((max.x - min.x) / edge_length).ceil() as usize + 3,
((max.y - min.y) / edge_length).ceil() as usize + 3,
((max.z - min.z) / edge_length).ceil() as usize + 3,
];
let requested = counts[0]
.saturating_mul(counts[1])
.saturating_mul(counts[2]);
if requested > MAX_SAMPLES {
return Err(LevelSetError::BudgetExceeded {
requested,
limit: MAX_SAMPLES,
});
}
let origin = Point3::new(
min.x - edge_length,
min.y - edge_length,
min.z - edge_length,
);
let at = |i: usize, j: usize, k: usize| {
Point3::new(
origin.x + (i as Scalar) * edge_length,
origin.y + (j as Scalar) * edge_length,
origin.z + (k as Scalar) * edge_length,
)
};
let index_of = |i: usize, j: usize, k: usize| (k * counts[1] + j) * counts[0] + i;
let mut samples = vec![0.0 as Scalar; requested];
for k in 0..counts[2] {
for j in 0..counts[1] {
for i in 0..counts[0] {
let point = at(i, j, k);
let on_shell = i == 0
|| j == 0
|| k == 0
|| i == counts[0] - 1
|| j == counts[1] - 1
|| k == counts[2] - 1;
let value = if on_shell {
1.0
} else {
let raw = field(point);
if !raw.is_finite() {
return Err(LevelSetError::NonFiniteSample { point });
}
raw - level
};
samples[index_of(i, j, k)] = value;
}
}
}
let mut positions: Vec<Point3> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut vertices: AHashMap<(usize, usize), u32> = AHashMap::new();
let mut welded: AHashMap<[u64; 3], u32> = AHashMap::new();
for k in 0..counts[2] - 1 {
for j in 0..counts[1] - 1 {
for i in 0..counts[0] - 1 {
let corner = |bit: usize| {
let (dx, dy, dz) = (bit & 1, (bit >> 1) & 1, (bit >> 2) & 1);
index_of(i + dx, j + dy, k + dz)
};
for tetrahedron in KUHN_TETRAHEDRA {
let nodes = tetrahedron.map(corner);
emit_tetrahedron(
nodes,
&samples,
&counts,
origin,
edge_length,
&mut positions,
&mut indices,
&mut vertices,
&mut welded,
);
}
}
}
}
if indices.is_empty() {
return Err(LevelSetError::NoCrossing { level });
}
Ok(TriMesh::new(positions, indices))
}
#[allow(clippy::too_many_arguments)]
fn emit_tetrahedron(
nodes: [usize; 4],
samples: &[Scalar],
counts: &[usize; 3],
origin: Point3,
edge_length: Scalar,
positions: &mut Vec<Point3>,
indices: &mut Vec<u32>,
vertices: &mut AHashMap<(usize, usize), u32>,
welded: &mut AHashMap<[u64; 3], u32>,
) {
let inside = nodes.map(|node| samples[node] < 0.0);
let count = inside.iter().filter(|&&flag| flag).count();
if count == 0 || count == 4 {
return;
}
let mut interpolate = |a: usize, b: usize, positions: &mut Vec<Point3>| -> u32 {
let key = if a < b { (a, b) } else { (b, a) };
if let Some(&existing) = vertices.get(&key) {
return existing;
}
let (va, vb) = (samples[key.0], samples[key.1]);
let (pa, pb) = (
grid_point(key.0, counts, origin, edge_length),
grid_point(key.1, counts, origin, edge_length),
);
let span = vb - va;
let raw = if span.abs() > Scalar::EPSILON {
(-va / span).clamp(0.0, 1.0)
} else {
0.5
};
let t = raw.clamp(SOS_DELTA, 1.0 - SOS_DELTA);
let point = pa + (pb - pa) * t;
let bits = [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()];
let index = match welded.get(&bits) {
Some(&existing) => existing,
None => {
let fresh = positions.len() as u32;
positions.push(point);
welded.insert(bits, fresh);
fresh
}
};
vertices.insert(key, index);
index
};
let mut ordered = [0usize; 4];
let (mut head, mut tail) = (0, 3);
for (slot, &node) in nodes.iter().enumerate() {
if inside[slot] {
ordered[head] = node;
head += 1;
} else {
ordered[tail] = node;
tail = tail.wrapping_sub(1);
}
}
let centroid = |nodes: &[usize]| {
let mut sum = Point3::ZERO;
for &node in nodes {
sum += grid_point(node, counts, origin, edge_length);
}
sum / (nodes.len() as Scalar)
};
let outward = centroid(&ordered[count..]) - centroid(&ordered[..count]);
match count {
1 => {
let a = interpolate(ordered[0], ordered[1], positions);
let b = interpolate(ordered[0], ordered[2], positions);
let c = interpolate(ordered[0], ordered[3], positions);
push_oriented(indices, positions, [a, b, c], outward);
}
3 => {
let a = interpolate(ordered[3], ordered[0], positions);
let b = interpolate(ordered[3], ordered[1], positions);
let c = interpolate(ordered[3], ordered[2], positions);
push_oriented(indices, positions, [a, b, c], outward);
}
_ => {
let a = interpolate(ordered[0], ordered[2], positions);
let b = interpolate(ordered[0], ordered[3], positions);
let c = interpolate(ordered[1], ordered[3], positions);
let d = interpolate(ordered[1], ordered[2], positions);
push_oriented(indices, positions, [a, b, c], outward);
push_oriented(indices, positions, [a, c, d], outward);
}
}
}
fn push_oriented(
indices: &mut Vec<u32>,
positions: &[Point3],
triangle: [u32; 3],
outward: axiolid_core::Vec3,
) {
let [a, b, c] = triangle;
if a == b || b == c || a == c {
return;
}
let (pa, pb, pc) = (
positions[a as usize],
positions[b as usize],
positions[c as usize],
);
let normal = (pb - pa).cross(pc - pa);
if normal.dot(outward) >= 0.0 {
indices.extend_from_slice(&[a, b, c]);
} else {
indices.extend_from_slice(&[a, c, b]);
}
}
fn grid_point(index: usize, counts: &[usize; 3], origin: Point3, edge_length: Scalar) -> Point3 {
let i = index % counts[0];
let j = (index / counts[0]) % counts[1];
let k = index / (counts[0] * counts[1]);
Point3::new(
origin.x + (i as Scalar) * edge_length,
origin.y + (j as Scalar) * edge_length,
origin.z + (k as Scalar) * edge_length,
)
}