use std::collections::HashMap;
type EdgeFeatureLists = (Vec<(usize, usize)>, Vec<(usize, usize)>);
type EdgeFaceNormalMap = HashMap<(usize, usize), Vec<([f64; 3], [f64; 3])>>;
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
fn length(a: [f64; 3]) -> f64 {
dot(a, a).sqrt()
}
fn normalize(a: [f64; 3]) -> [f64; 3] {
let len = length(a);
if len < 1e-300 {
[0.0, 0.0, 0.0]
} else {
scale(a, 1.0 / len)
}
}
pub trait Sdf: Send + Sync {
fn distance(&self, p: [f64; 3]) -> f64;
fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
let eps = 1e-5;
let dx = self.distance([p[0] + eps, p[1], p[2]]) - self.distance([p[0] - eps, p[1], p[2]]);
let dy = self.distance([p[0], p[1] + eps, p[2]]) - self.distance([p[0], p[1] - eps, p[2]]);
let dz = self.distance([p[0], p[1], p[2] + eps]) - self.distance([p[0], p[1], p[2] - eps]);
[dx / (2.0 * eps), dy / (2.0 * eps), dz / (2.0 * eps)]
}
fn normal(&self, p: [f64; 3]) -> [f64; 3] {
normalize(self.gradient(p))
}
}
pub struct SdfSphere {
pub center: [f64; 3],
pub radius: f64,
}
impl SdfSphere {
pub fn new(center: [f64; 3], radius: f64) -> Self {
Self { center, radius }
}
}
impl Sdf for SdfSphere {
fn distance(&self, p: [f64; 3]) -> f64 {
length(sub(p, self.center)) - self.radius
}
}
pub struct SdfBox {
pub center: [f64; 3],
pub half_extents: [f64; 3],
}
impl SdfBox {
pub fn new(hx: f64, hy: f64, hz: f64) -> Self {
Self {
center: [0.0, 0.0, 0.0],
half_extents: [hx, hy, hz],
}
}
pub fn new_centered(center: [f64; 3], half_extents: [f64; 3]) -> Self {
Self {
center,
half_extents,
}
}
}
impl Sdf for SdfBox {
fn distance(&self, p: [f64; 3]) -> f64 {
let q = sub(p, self.center);
let qx = q[0].abs() - self.half_extents[0];
let qy = q[1].abs() - self.half_extents[1];
let qz = q[2].abs() - self.half_extents[2];
let outside = length([qx.max(0.0), qy.max(0.0), qz.max(0.0)]);
let inside = qx.max(qy).max(qz).min(0.0);
outside + inside
}
}
pub struct SdfCapsule {
pub a: [f64; 3],
pub b: [f64; 3],
pub radius: f64,
}
impl Sdf for SdfCapsule {
fn distance(&self, p: [f64; 3]) -> f64 {
let ab = sub(self.b, self.a);
let ap = sub(p, self.a);
let t = (dot(ap, ab) / dot(ab, ab)).clamp(0.0, 1.0);
let closest = add(self.a, scale(ab, t));
length(sub(p, closest)) - self.radius
}
}
pub struct SdfPlane {
pub normal: [f64; 3],
pub offset: f64,
}
impl SdfPlane {
pub fn new(normal: [f64; 3], offset: f64) -> Self {
Self { normal, offset }
}
}
impl Sdf for SdfPlane {
fn distance(&self, p: [f64; 3]) -> f64 {
dot(self.normal, p) - self.offset
}
}
pub struct SdfTorus {
pub center: [f64; 3],
pub major_radius: f64,
pub minor_radius: f64,
}
impl Sdf for SdfTorus {
fn distance(&self, p: [f64; 3]) -> f64 {
let q = sub(p, self.center);
let r_xz = (q[0] * q[0] + q[2] * q[2]).sqrt();
let d_xz = r_xz - self.major_radius;
(d_xz * d_xz + q[1] * q[1]).sqrt() - self.minor_radius
}
}
pub struct SdfUnion {
pub a: Box<dyn Sdf>,
pub b: Box<dyn Sdf>,
}
impl Sdf for SdfUnion {
fn distance(&self, p: [f64; 3]) -> f64 {
self.a.distance(p).min(self.b.distance(p))
}
}
pub struct SdfIntersection {
pub a: Box<dyn Sdf>,
pub b: Box<dyn Sdf>,
}
impl Sdf for SdfIntersection {
fn distance(&self, p: [f64; 3]) -> f64 {
self.a.distance(p).max(self.b.distance(p))
}
}
pub struct SdfDifference {
pub a: Box<dyn Sdf>,
pub b: Box<dyn Sdf>,
}
impl Sdf for SdfDifference {
fn distance(&self, p: [f64; 3]) -> f64 {
self.a.distance(p).max(-self.b.distance(p))
}
}
pub struct SdfOffset {
pub inner: Box<dyn Sdf>,
pub offset: f64,
}
impl Sdf for SdfOffset {
fn distance(&self, p: [f64; 3]) -> f64 {
self.inner.distance(p) - self.offset
}
}
pub struct SdfSmoothUnion {
pub a: Box<dyn Sdf>,
pub b: Box<dyn Sdf>,
pub k: f64,
}
impl Sdf for SdfSmoothUnion {
fn distance(&self, p: [f64; 3]) -> f64 {
let da = self.a.distance(p);
let db = self.b.distance(p);
let h = (0.5 + 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
db + (da - db) * h - self.k * h * (1.0 - h)
}
}
pub struct SdfSmoothIntersection {
pub a: Box<dyn Sdf>,
pub b: Box<dyn Sdf>,
pub k: f64,
}
impl Sdf for SdfSmoothIntersection {
fn distance(&self, p: [f64; 3]) -> f64 {
let da = self.a.distance(p);
let db = self.b.distance(p);
let h = (0.5 - 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
db + (da - db) * h + self.k * h * (1.0 - h)
}
}
pub struct OffsetMesh {
pub vertices: Vec<[f64; 3]>,
pub normals: Vec<[f64; 3]>,
pub faces: Vec<[usize; 3]>,
}
impl OffsetMesh {
pub fn from_triangle_soup(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Self {
let normals = Self::compute_vertex_normals(verts, faces);
Self {
vertices: verts.to_vec(),
normals,
faces: faces.to_vec(),
}
}
pub fn compute_vertex_normals(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Vec<[f64; 3]> {
let n = verts.len();
let mut accum = vec![[0.0f64; 3]; n];
for f in faces {
let v0 = verts[f[0]];
let v1 = verts[f[1]];
let v2 = verts[f[2]];
let e1 = sub(v1, v0);
let e2 = sub(v2, v0);
let face_normal = cross(e1, e2); for &vi in f {
accum[vi] = add(accum[vi], face_normal);
}
}
accum.iter().map(|&n| normalize(n)).collect()
}
pub fn offset(&self, d: f64) -> OffsetMesh {
let new_vertices: Vec<[f64; 3]> = self
.vertices
.iter()
.zip(self.normals.iter())
.map(|(&v, &n)| add(v, scale(n, d)))
.collect();
OffsetMesh {
vertices: new_vertices,
normals: self.normals.clone(),
faces: self.faces.clone(),
}
}
}
pub struct VoxelSdf {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub origin: [f64; 3],
pub dx: f64,
pub values: Vec<f64>,
}
impl VoxelSdf {
pub fn new(nx: usize, ny: usize, nz: usize, origin: [f64; 3], dx: f64) -> Self {
Self {
nx,
ny,
nz,
origin,
dx,
values: vec![0.0; nx * ny * nz],
}
}
pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
ix + self.nx * (iy + self.ny * iz)
}
pub fn world_to_grid(&self, p: [f64; 3]) -> [f64; 3] {
[
(p[0] - self.origin[0]) / self.dx,
(p[1] - self.origin[1]) / self.dx,
(p[2] - self.origin[2]) / self.dx,
]
}
pub fn sample_trilinear(&self, p: [f64; 3]) -> f64 {
let g = self.world_to_grid(p);
let x0 = g[0].floor() as isize;
let y0 = g[1].floor() as isize;
let z0 = g[2].floor() as isize;
let fx = g[0] - x0 as f64;
let fy = g[1] - y0 as f64;
let fz = g[2] - z0 as f64;
let nx = self.nx as isize;
let ny = self.ny as isize;
let nz = self.nz as isize;
let clamp_x = |i: isize| i.clamp(0, nx - 1) as usize;
let clamp_y = |i: isize| i.clamp(0, ny - 1) as usize;
let clamp_z = |i: isize| i.clamp(0, nz - 1) as usize;
let v = |dx: isize, dy: isize, dz: isize| -> f64 {
self.values[self.idx(clamp_x(x0 + dx), clamp_y(y0 + dy), clamp_z(z0 + dz))]
};
let c00 = v(0, 0, 0) * (1.0 - fx) + v(1, 0, 0) * fx;
let c01 = v(0, 0, 1) * (1.0 - fx) + v(1, 0, 1) * fx;
let c10 = v(0, 1, 0) * (1.0 - fx) + v(1, 1, 0) * fx;
let c11 = v(0, 1, 1) * (1.0 - fx) + v(1, 1, 1) * fx;
let c0 = c00 * (1.0 - fy) + c10 * fy;
let c1 = c01 * (1.0 - fy) + c11 * fy;
c0 * (1.0 - fz) + c1 * fz
}
pub fn from_sdf(
sdf: &dyn Sdf,
nx: usize,
ny: usize,
nz: usize,
origin: [f64; 3],
dx: f64,
) -> Self {
let mut grid = Self::new(nx, ny, nz, origin, dx);
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let p = [
origin[0] + (ix as f64 + 0.5) * dx,
origin[1] + (iy as f64 + 0.5) * dx,
origin[2] + (iz as f64 + 0.5) * dx,
];
let i = grid.idx(ix, iy, iz);
grid.values[i] = sdf.distance(p);
}
}
}
grid
}
pub fn gradient_central(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
let nx = self.nx;
let ny = self.ny;
let nz = self.nz;
let xp = if ix + 1 < nx { ix + 1 } else { ix };
let xm = if ix > 0 { ix - 1 } else { ix };
let yp = if iy + 1 < ny { iy + 1 } else { iy };
let ym = if iy > 0 { iy - 1 } else { iy };
let zp = if iz + 1 < nz { iz + 1 } else { iz };
let zm = if iz > 0 { iz - 1 } else { iz };
let step_x = (xp - xm) as f64;
let step_y = (yp - ym) as f64;
let step_z = (zp - zm) as f64;
let gx = (self.values[self.idx(xp, iy, iz)] - self.values[self.idx(xm, iy, iz)])
/ (step_x * self.dx);
let gy = (self.values[self.idx(ix, yp, iz)] - self.values[self.idx(ix, ym, iz)])
/ (step_y * self.dx);
let gz = (self.values[self.idx(ix, iy, zp)] - self.values[self.idx(ix, iy, zm)])
/ (step_z * self.dx);
[gx, gy, gz]
}
}
pub fn outward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
OffsetMesh::from_triangle_soup(verts, faces).offset(d)
}
pub fn inward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
OffsetMesh::from_triangle_soup(verts, faces).offset(-d)
}
impl OffsetMesh {
pub fn collision_offset_shell(&self, d: f64) -> OffsetMesh {
let n_faces = self.faces.len();
let mut new_verts = Vec::with_capacity(n_faces * 3);
let mut new_faces = Vec::with_capacity(n_faces);
for (fi, face) in self.faces.iter().enumerate() {
let v0 = self.vertices[face[0]];
let v1 = self.vertices[face[1]];
let v2 = self.vertices[face[2]];
let e1 = sub(v1, v0);
let e2 = sub(v2, v0);
let face_n = normalize(cross(e1, e2));
let base = fi * 3;
new_verts.push(add(v0, scale(face_n, d)));
new_verts.push(add(v1, scale(face_n, d)));
new_verts.push(add(v2, scale(face_n, d)));
new_faces.push([base, base + 1, base + 2]);
}
let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
OffsetMesh {
vertices: new_verts,
normals,
faces: new_faces,
}
}
pub fn normals_are_unit(&self) -> bool {
self.normals.iter().all(|&n| (length(n) - 1.0).abs() < 1e-6)
}
pub fn surface_area(&self) -> f64 {
self.faces
.iter()
.map(|f| {
let v0 = self.vertices[f[0]];
let v1 = self.vertices[f[1]];
let v2 = self.vertices[f[2]];
length(cross(sub(v1, v0), sub(v2, v0))) * 0.5
})
.sum()
}
pub fn centroid(&self) -> [f64; 3] {
if self.vertices.is_empty() {
return [0.0; 3];
}
let sum = self
.vertices
.iter()
.fold([0.0f64; 3], |acc, &v| add(acc, v));
scale(sum, 1.0 / self.vertices.len() as f64)
}
}
pub fn approximate_medial_axis(grid: &VoxelSdf, gradient_threshold: f64) -> Vec<[f64; 3]> {
let mut points = Vec::new();
for iz in 0..grid.nz {
for iy in 0..grid.ny {
for ix in 0..grid.nx {
let val = grid.values[grid.idx(ix, iy, iz)];
if val >= 0.0 {
continue;
}
let g = grid.gradient_central(ix, iy, iz);
let grad_mag = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
if grad_mag < gradient_threshold {
let p = [
grid.origin[0] + (ix as f64 + 0.5) * grid.dx,
grid.origin[1] + (iy as f64 + 0.5) * grid.dx,
grid.origin[2] + (iz as f64 + 0.5) * grid.dx,
];
points.push(p);
}
}
}
}
points
}
pub struct SdfCylinder {
pub center: [f64; 3],
pub radius: f64,
pub half_height: f64,
}
impl Sdf for SdfCylinder {
fn distance(&self, p: [f64; 3]) -> f64 {
let q = sub(p, self.center);
let xz_dist = (q[0] * q[0] + q[2] * q[2]).sqrt() - self.radius;
let y_dist = q[1].abs() - self.half_height;
let outside =
(xz_dist.max(0.0) * xz_dist.max(0.0) + y_dist.max(0.0) * y_dist.max(0.0)).sqrt();
outside + xz_dist.max(y_dist).min(0.0)
}
}
pub struct SdfCone {
pub apex: [f64; 3],
pub height: f64,
pub angle_rad: f64,
}
impl Sdf for SdfCone {
fn distance(&self, p: [f64; 3]) -> f64 {
let q = sub(p, self.apex);
let r = (q[0] * q[0] + q[2] * q[2]).sqrt();
let sin_a = self.angle_rad.sin();
let cos_a = self.angle_rad.cos();
let dist_axis = r * cos_a + q[1] * sin_a; let dist_cap = q[1] + self.height; dist_axis.max(-dist_cap)
}
}
pub struct SdfTranslated {
pub inner: Box<dyn Sdf>,
pub offset: [f64; 3],
}
impl Sdf for SdfTranslated {
fn distance(&self, p: [f64; 3]) -> f64 {
let local = sub(p, self.offset);
self.inner.distance(local)
}
}
pub struct SdfScaled {
pub inner: Box<dyn Sdf>,
pub scale_factor: f64,
}
impl Sdf for SdfScaled {
fn distance(&self, p: [f64; 3]) -> f64 {
if self.scale_factor.abs() < 1e-300 {
return f64::INFINITY;
}
self.inner.distance(scale(p, 1.0 / self.scale_factor)) * self.scale_factor
}
}
pub fn extract_zero_crossings_slice(grid: &VoxelSdf, iz: usize) -> Vec<[f64; 2]> {
let mut crossings = Vec::new();
let nz = grid.nz;
if iz >= nz {
return crossings;
}
for iy in 0..grid.ny.saturating_sub(1) {
for ix in 0..grid.nx.saturating_sub(1) {
let v00 = grid.values[grid.idx(ix, iy, iz)];
let v10 = grid.values[grid.idx(ix + 1, iy, iz)];
let v01 = grid.values[grid.idx(ix, iy + 1, iz)];
let x0 = grid.origin[0] + (ix as f64 + 0.5) * grid.dx;
let x1 = grid.origin[0] + (ix as f64 + 1.5) * grid.dx;
let y0 = grid.origin[1] + (iy as f64 + 0.5) * grid.dx;
let y1 = grid.origin[1] + (iy as f64 + 1.5) * grid.dx;
if (v00 < 0.0) != (v10 < 0.0) {
let t = v00 / (v00 - v10);
crossings.push([x0 + t * (x1 - x0), y0]);
}
if (v00 < 0.0) != (v01 < 0.0) {
let t = v00 / (v00 - v01);
crossings.push([x0, y0 + t * (y1 - y0)]);
}
}
}
crossings
}
pub fn variable_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], weights: &[f64]) -> OffsetMesh {
let normals = OffsetMesh::compute_vertex_normals(verts, faces);
let new_verts: Vec<[f64; 3]> = verts
.iter()
.enumerate()
.map(|(i, &v)| {
let w = if i < weights.len() { weights[i] } else { 0.0 };
let n = if i < normals.len() {
normals[i]
} else {
[0.0; 3]
};
add(v, scale(n, w))
})
.collect();
OffsetMesh {
normals: OffsetMesh::compute_vertex_normals(&new_verts, faces),
vertices: new_verts,
faces: faces.to_vec(),
}
}
pub fn offset_polyhedron(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
OffsetMesh::from_triangle_soup(verts, faces).offset(d)
}
pub fn detect_edge_features(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> EdgeFeatureLists {
let mut edge_data: EdgeFaceNormalMap = HashMap::new();
for face in faces {
let v0 = verts[face[0]];
let v1 = verts[face[1]];
let v2 = verts[face[2]];
let e1 = sub(v1, v0);
let e2 = sub(v2, v0);
let face_n = normalize(cross(e1, e2));
let face_centroid = scale(add(add(v0, v1), v2), 1.0 / 3.0);
for k in 0..3 {
let ea = face[k];
let eb = face[(k + 1) % 3];
let key = (ea.min(eb), ea.max(eb));
let mid_edge = scale(add(verts[ea], verts[eb]), 0.5);
let ct_to_edge = sub(mid_edge, face_centroid);
edge_data.entry(key).or_default().push((face_n, ct_to_edge));
}
}
let mut convex = Vec::new();
let mut concave = Vec::new();
for (&(ea, eb), data) in &edge_data {
if data.len() != 2 {
continue; }
let n0 = data[0].0;
let n1 = data[1].0;
let ct0 = data[0].1;
let dot_nn = dot(n0, n1);
let sign = dot(cross(n0, n1), ct0);
if dot_nn < 0.9999 {
if sign >= 0.0 {
convex.push((ea, eb));
} else {
concave.push((ea, eb));
}
}
}
(convex, concave)
}
pub fn offset_curve_3d(pts: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
let n = normalize(normal);
pts.iter().map(|&p| add(p, scale(n, d))).collect()
}
pub fn generate_shell(verts: &[[f64; 3]], faces: &[[usize; 3]], thickness: f64) -> OffsetMesh {
let n_orig = verts.len();
let outer = OffsetMesh::from_triangle_soup(verts, faces).offset(thickness);
let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
new_verts.extend_from_slice(&outer.vertices);
let mut new_faces: Vec<[usize; 3]> = faces.iter().map(|f| [f[2], f[1], f[0]]).collect();
new_faces.extend(
faces
.iter()
.map(|f| [f[0] + n_orig, f[1] + n_orig, f[2] + n_orig]),
);
let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
for face in faces {
for k in 0..3 {
let ea = face[k];
let eb = face[(k + 1) % 3];
let key = (ea.min(eb), ea.max(eb));
*edge_count.entry(key).or_insert(0) += 1;
}
}
for ((ea, eb), count) in &edge_count {
if *count == 1 {
let ia = *ea;
let ib = *eb;
let oa = *ea + n_orig;
let ob = *eb + n_orig;
new_faces.push([ia, ib, ob]);
new_faces.push([ia, ob, oa]);
}
}
let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
OffsetMesh {
vertices: new_verts,
normals,
faces: new_faces,
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_sphere_center_negative_radius() {
let s = SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 2.0,
};
assert!((s.distance([0.0, 0.0, 0.0]) - (-2.0)).abs() < EPS);
}
#[test]
fn test_sphere_surface_zero() {
let s = SdfSphere {
center: [1.0, 2.0, 3.0],
radius: 1.5,
};
let p = [1.0 + 1.5, 2.0, 3.0];
assert!(s.distance(p).abs() < EPS);
}
#[test]
fn test_sphere_outside_positive() {
let s = SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
};
assert!(s.distance([5.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn test_box_inside_negative() {
let b = SdfBox {
center: [0.0, 0.0, 0.0],
half_extents: [2.0, 2.0, 2.0],
};
assert!(b.distance([0.5, 0.5, 0.5]) < 0.0);
assert!(b.distance([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_box_outside_positive() {
let b = SdfBox {
center: [0.0, 0.0, 0.0],
half_extents: [1.0, 1.0, 1.0],
};
assert!(b.distance([3.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn test_capsule_midpoint_surface() {
let c = SdfCapsule {
a: [0.0, 0.0, 0.0],
b: [0.0, 4.0, 0.0],
radius: 1.0,
};
assert!(c.distance([1.0, 2.0, 0.0]).abs() < EPS);
}
#[test]
fn test_capsule_inside_negative() {
let c = SdfCapsule {
a: [0.0, 0.0, 0.0],
b: [0.0, 4.0, 0.0],
radius: 1.0,
};
assert!(c.distance([0.0, 2.0, 0.0]) < 0.0);
}
#[test]
fn test_union_inside_either() {
let union = SdfUnion {
a: Box::new(SdfSphere {
center: [-3.0, 0.0, 0.0],
radius: 1.5,
}),
b: Box::new(SdfSphere {
center: [3.0, 0.0, 0.0],
radius: 1.5,
}),
};
assert!(union.distance([-3.0, 0.0, 0.0]) < 0.0);
assert!(union.distance([3.0, 0.0, 0.0]) < 0.0);
assert!(union.distance([0.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn test_sdf_offset_expanded_sphere() {
let base = SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
};
let expanded = SdfOffset {
inner: Box::new(SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
}),
offset: 0.5,
};
let r = 1.4_f64;
let p = [r, 0.0, 0.0];
assert!(base.distance(p) > 0.0);
assert!(expanded.distance(p) < 0.0);
}
#[test]
fn test_offset_mesh_outward() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
let d = 0.3;
let off = mesh.offset(d);
for (orig, new_v) in mesh.vertices.iter().zip(off.vertices.iter()) {
let dist = length(sub(*new_v, *orig));
assert!(
(dist - d).abs() < 1e-9,
"vertex moved by {dist}, expected {d}"
);
}
}
#[test]
fn test_offset_mesh_normals_unit() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
for n in &mesh.normals {
let len = length(*n);
assert!((len - 1.0).abs() < 1e-9, "normal not unit: {len}");
}
}
#[test]
fn test_voxel_sdf_sphere_center() {
let sphere = SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
};
let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-1.0, -1.0, -1.0], 0.2);
let centre_idx = grid.idx(4, 4, 4);
let v = grid.values[centre_idx];
assert!(v < 0.0, "centre cell value {v} should be negative");
assert!(
(v - (-1.0)).abs() < 0.3,
"centre cell value {v} not close to -1.0"
);
}
#[test]
fn test_voxel_sdf_constant_trilinear() {
let mut grid = VoxelSdf::new(4, 4, 4, [0.0, 0.0, 0.0], 1.0);
for v in grid.values.iter_mut() {
*v = 3.125;
}
let sample = grid.sample_trilinear([1.5, 1.5, 1.5]);
assert!(
(sample - 3.125).abs() < 1e-9,
"constant field returned {sample}"
);
}
#[test]
fn test_voxel_sdf_gradient_central() {
let sphere = SdfSphere {
center: [0.5, 0.5, 0.5],
radius: 0.3,
};
let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [0.0, 0.0, 0.0], 0.1);
let g = grid.gradient_central(7, 5, 5);
let gl = length(g);
assert!(
gl.is_finite() && gl > 0.0,
"gradient should be non-zero: {g:?}"
);
}
#[test]
fn test_plane_above_below() {
let plane = SdfPlane {
normal: [0.0, 1.0, 0.0],
offset: 0.0,
};
assert!(plane.distance([0.0, 1.0, 0.0]) > 0.0);
assert!(plane.distance([0.0, -1.0, 0.0]) < 0.0);
assert!(plane.distance([0.0, 0.0, 0.0]).abs() < EPS);
}
#[test]
fn test_torus_on_ring() {
let t = SdfTorus {
center: [0.0, 0.0, 0.0],
major_radius: 2.0,
minor_radius: 0.5,
};
let p = [2.5, 0.0, 0.0];
assert!(t.distance(p).abs() < EPS);
}
#[test]
fn test_smooth_union_between_shapes() {
let su = SdfSmoothUnion {
a: Box::new(SdfSphere {
center: [-1.0, 0.0, 0.0],
radius: 0.8,
}),
b: Box::new(SdfSphere {
center: [1.0, 0.0, 0.0],
radius: 0.8,
}),
k: 0.5,
};
let d_mid = su.distance([0.0, 0.0, 0.0]);
assert!(d_mid.is_finite(), "smooth union distance should be finite");
}
#[test]
fn test_difference_carves_out() {
let diff = SdfDifference {
a: Box::new(SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 2.0,
}),
b: Box::new(SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
}),
};
assert!(diff.distance([0.0, 0.0, 0.0]) > 0.0);
assert!(diff.distance([1.5, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_intersection_overlap_region() {
let inter = SdfIntersection {
a: Box::new(SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 2.0,
}),
b: Box::new(SdfSphere {
center: [1.0, 0.0, 0.0],
radius: 2.0,
}),
};
assert!(inter.distance([0.5, 0.0, 0.0]) < 0.0);
assert!(inter.distance([-1.5, 0.0, 0.0]) > 0.0);
}
#[test]
fn test_outward_offset_expands_vertices() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let d = 0.5;
let expanded = outward_offset(verts, faces, d);
let orig = OffsetMesh::from_triangle_soup(verts, faces);
for (v_new, v_old) in expanded.vertices.iter().zip(orig.vertices.iter()) {
let moved = length(sub(*v_new, *v_old));
assert!((moved - d).abs() < 1e-9, "vertex moved by {moved}");
}
}
#[test]
fn test_inward_offset_shrinks_vertices() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let shrunk = inward_offset(verts, faces, 0.2);
let orig = OffsetMesh::from_triangle_soup(verts, faces);
for (v_new, v_old) in shrunk.vertices.iter().zip(orig.vertices.iter()) {
let moved = length(sub(*v_new, *v_old));
assert!((moved - 0.2).abs() < 1e-9, "vertex moved by {moved}");
}
}
#[test]
fn test_collision_offset_shell_creates_new_mesh() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
let shell = mesh.collision_offset_shell(0.1);
assert_eq!(shell.faces.len(), 1);
assert_eq!(shell.vertices.len(), 3);
assert!(shell.normals_are_unit(), "shell normals should be unit");
}
#[test]
fn test_collision_offset_shell_moves_vertices() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 0.0, 2.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
let d = 0.5;
let shell = mesh.collision_offset_shell(d);
for (vn, vo) in shell.vertices.iter().zip(mesh.vertices.iter()) {
let dist = length(sub(*vn, *vo));
assert!((dist - d).abs() < 1e-9, "dist={dist}");
}
}
#[test]
fn test_offset_mesh_surface_area_positive() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
let area = mesh.surface_area();
assert!((area - 0.5).abs() < 1e-9, "area={area}");
}
#[test]
fn test_offset_mesh_centroid_of_triangle() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let mesh = OffsetMesh::from_triangle_soup(verts, faces);
let c = mesh.centroid();
assert!((c[0] - 1.0).abs() < 1e-9);
assert!((c[1] - 1.0).abs() < 1e-9);
}
#[test]
fn test_sdf_cylinder_inside_negative() {
let cyl = SdfCylinder {
center: [0.0; 3],
radius: 1.0,
half_height: 2.0,
};
assert!(cyl.distance([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_sdf_cylinder_outside_positive() {
let cyl = SdfCylinder {
center: [0.0; 3],
radius: 1.0,
half_height: 2.0,
};
assert!(cyl.distance([5.0, 0.0, 0.0]) > 0.0); assert!(cyl.distance([0.0, 5.0, 0.0]) > 0.0); }
#[test]
fn test_sdf_translated_moves_sphere() {
let translated = SdfTranslated {
inner: Box::new(SdfSphere {
center: [0.0; 3],
radius: 1.0,
}),
offset: [5.0, 0.0, 0.0],
};
assert!((translated.distance([5.0, 0.0, 0.0]) - (-1.0)).abs() < EPS);
}
#[test]
fn test_sdf_scaled_larger_sphere() {
let scaled = SdfScaled {
inner: Box::new(SdfSphere {
center: [0.0; 3],
radius: 1.0,
}),
scale_factor: 2.0,
};
assert!(scaled.distance([1.5, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_medial_axis_nonempty_for_thick_sdf() {
let sphere = SdfSphere {
center: [0.0; 3],
radius: 3.0,
};
let grid = VoxelSdf::from_sdf(&sphere, 20, 20, 20, [-4.0, -4.0, -4.0], 0.4);
let pts = approximate_medial_axis(&grid, 0.7);
let _ = pts;
}
#[test]
fn test_zero_crossings_nonempty_for_sphere() {
let sphere = SdfSphere {
center: [0.0; 3],
radius: 1.0,
};
let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
let crossings = extract_zero_crossings_slice(&grid, 5);
assert!(
!crossings.is_empty(),
"sphere slice should have zero crossings"
);
}
#[test]
fn test_zero_crossings_empty_outside_range() {
let sphere = SdfSphere {
center: [0.0; 3],
radius: 1.0,
};
let grid = VoxelSdf::from_sdf(&sphere, 5, 5, 5, [-2.0, -2.0, -2.0], 0.4);
let crossings = extract_zero_crossings_slice(&grid, 100); assert!(
crossings.is_empty(),
"out-of-range slice should have no crossings"
);
}
#[test]
fn test_variable_offset_moves_each_vertex_by_its_weight() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let weights = vec![0.1, 0.2, 0.3];
let result = variable_offset(verts, faces, &weights);
let orig = OffsetMesh::from_triangle_soup(verts, faces);
for (i, (vn, vo)) in result.vertices.iter().zip(orig.vertices.iter()).enumerate() {
let dist = length(sub(*vn, *vo));
assert!(
(dist - weights[i]).abs() < 1e-9,
"vertex {i}: moved by {dist}, expected {}",
weights[i]
);
}
}
#[test]
fn test_variable_offset_zero_weights_no_change() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let weights = vec![0.0, 0.0, 0.0];
let result = variable_offset(verts, faces, &weights);
for (vn, &vo) in result.vertices.iter().zip(verts.iter()) {
assert!(
length(sub(*vn, vo)) < 1e-12,
"zero weight should not move vertex"
);
}
}
#[test]
fn test_offset_polyhedron_expands_box() {
let (verts, faces) = unit_cube_mesh();
let d = 0.5;
let result = offset_polyhedron(&verts, &faces, d);
let orig = OffsetMesh::from_triangle_soup(&verts, &faces);
for (vn, vo) in result.vertices.iter().zip(orig.vertices.iter()) {
let dist = length(sub(*vn, *vo));
assert!(dist > 0.0, "vertex should have moved");
}
}
#[test]
fn test_offset_polyhedron_face_count_unchanged() {
let (verts, faces) = unit_cube_mesh();
let result = offset_polyhedron(&verts, &faces, 0.1);
assert_eq!(result.faces.len(), faces.len());
}
#[test]
fn test_detect_convex_edges_cube_has_convex_edges() {
let (verts, faces) = unit_cube_mesh();
let (convex, concave) = detect_edge_features(&verts, &faces);
let total = convex.len() + concave.len();
assert!(total > 0, "cube should have some feature edges, got 0");
}
#[test]
fn test_detect_edges_flat_mesh() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let (convex, concave) = detect_edge_features(verts, faces);
assert!(
convex.is_empty() || !concave.is_empty() || convex.is_empty(),
"single triangle has no interior shared edges"
);
let _ = (convex, concave);
}
#[test]
fn test_offset_curve_3d_expands_outward() {
let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let normal = [0.0, 1.0, 0.0];
let d = 0.5;
let offset_pts = offset_curve_3d(&pts, normal, d);
for (orig, off) in pts.iter().zip(offset_pts.iter()) {
let dy = off[1] - orig[1];
assert!(
(dy - d).abs() < 1e-12,
"offset should be {d} in Y, got {dy}"
);
}
}
#[test]
fn test_offset_curve_3d_negative_shrinks() {
let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let normal = [0.0, 0.0, 1.0];
let offset_pts = offset_curve_3d(&pts, normal, -0.3);
for (orig, off) in pts.iter().zip(offset_pts.iter()) {
let dz = off[2] - orig[2];
assert!(
(dz + 0.3).abs() < 1e-12,
"offset should be -0.3 in Z, got {dz}"
);
}
}
#[test]
fn test_shell_generation_double_face_count() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let shell = generate_shell(verts, faces, 0.1);
assert!(
shell.faces.len() >= faces.len() * 2,
"shell should have at least 2x faces"
);
}
#[test]
fn test_shell_generation_vertex_count() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let shell = generate_shell(verts, faces, 0.2);
assert!(shell.vertices.len() >= verts.len() * 2);
}
#[test]
fn test_sdf_cone_inside_negative() {
let cone = SdfCone {
apex: [0.0; 3],
height: 5.0,
angle_rad: 0.5,
};
let d = cone.distance([0.0, -1.0, 0.0]);
let _ = d; }
#[test]
fn test_sdf_cone_above_apex_positive() {
let cone = SdfCone {
apex: [0.0; 3],
height: 3.0,
angle_rad: 0.3,
};
assert!(cone.distance([0.0, 1.0, 0.0]) > 0.0);
}
#[test]
fn test_smooth_intersection_finite() {
let si = SdfSmoothIntersection {
a: Box::new(SdfSphere {
center: [0.0; 3],
radius: 2.0,
}),
b: Box::new(SdfSphere {
center: [1.0, 0.0, 0.0],
radius: 2.0,
}),
k: 0.3,
};
assert!(si.distance([0.5, 0.0, 0.0]).is_finite());
}
#[test]
fn test_voxel_sdf_idx_row_major() {
let grid = VoxelSdf::new(4, 5, 6, [0.0; 3], 1.0);
let idx = grid.idx(1, 2, 3);
let expected = 1 + 4 * (2 + 5 * 3);
assert_eq!(idx, expected);
}
#[test]
fn test_voxel_sdf_world_to_grid_origin() {
let grid = VoxelSdf::new(10, 10, 10, [1.0, 2.0, 3.0], 0.5);
let g = grid.world_to_grid([1.0, 2.0, 3.0]);
assert!(g[0].abs() < 1e-12);
assert!(g[1].abs() < 1e-12);
assert!(g[2].abs() < 1e-12);
}
#[test]
fn test_offset_mesh_empty_centroid() {
let mesh = OffsetMesh {
vertices: vec![],
normals: vec![],
faces: vec![],
};
let c = mesh.centroid();
assert_eq!(c, [0.0; 3]);
}
#[test]
fn test_sdf_gradient_unit_length_for_sphere() {
let sphere = SdfSphere {
center: [0.0; 3],
radius: 1.0,
};
let g = sphere.gradient([3.0, 0.0, 0.0]);
let gl = length(g);
assert!(
(gl - 1.0).abs() < 0.01,
"gradient magnitude should be ~1, got {gl}"
);
}
#[test]
fn test_sdf_normal_unit_length() {
let sphere = SdfSphere {
center: [0.0; 3],
radius: 1.0,
};
let n = sphere.normal([2.0, 0.0, 0.0]);
let nl = length(n);
assert!(
(nl - 1.0).abs() < 0.01,
"normal should be unit length, got {nl}"
);
}
#[test]
fn test_variable_offset_single_triangle_area_changes() {
let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces: &[[usize; 3]] = &[[0, 1, 2]];
let weights = vec![0.5, 0.5, 0.5];
let expanded = variable_offset(verts, faces, &weights);
for (vn, &vo) in expanded.vertices.iter().zip(verts.iter()) {
assert!(length(sub(*vn, vo)) > 0.0);
}
}
fn unit_cube_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0], ];
let faces = vec![
[0, 1, 2],
[0, 2, 3], [4, 5, 6],
[4, 6, 7], [0, 1, 5],
[0, 5, 4], [2, 3, 7],
[2, 7, 6], [0, 3, 7],
[0, 7, 4], [1, 2, 6],
[1, 6, 5], ];
(verts, faces)
}
}