#[inline]
fn vadd(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn vsub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn vscale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn vdot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn vcross(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],
]
}
#[inline]
fn vlen_sq(a: [f64; 3]) -> f64 {
vdot(a, a)
}
#[inline]
fn vlen(a: [f64; 3]) -> f64 {
vlen_sq(a).sqrt()
}
#[inline]
fn vnormalize(a: [f64; 3]) -> [f64; 3] {
let l = vlen(a);
if l < 1e-300 {
[0.0, 0.0, 0.0]
} else {
vscale(a, 1.0 / l)
}
}
#[inline]
fn vlerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
vadd(vscale(a, 1.0 - t), vscale(b, t))
}
#[inline]
fn vdist(a: [f64; 3], b: [f64; 3]) -> f64 {
vlen(vsub(a, b))
}
#[inline]
fn vclamp(x: f64, lo: f64, hi: f64) -> f64 {
x.max(lo).min(hi)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClosestFeature {
Vertex,
Edge,
Face,
}
#[derive(Debug, Clone, Copy)]
pub struct ClosestPointResult {
pub position: [f64; 3],
pub distance: f64,
pub feature: ClosestFeature,
pub u_param: f64,
pub v_param: f64,
}
impl ClosestPointResult {
fn new(
position: [f64; 3],
query_pt: [f64; 3],
feature: ClosestFeature,
u_param: f64,
v_param: f64,
) -> Self {
let distance = vdist(position, query_pt);
Self {
position,
distance,
feature,
u_param,
v_param,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SegSegResult {
pub point_a: [f64; 3],
pub point_b: [f64; 3],
pub s: f64,
pub t: f64,
pub distance: f64,
}
pub fn point_segment_closest(a: [f64; 3], b: [f64; 3], p: [f64; 3]) -> ClosestPointResult {
let ab = vsub(b, a);
let ap = vsub(p, a);
let len_sq = vlen_sq(ab);
let (t, feature) = if len_sq < 1e-300 {
(0.0, ClosestFeature::Vertex)
} else {
let raw_t = vdot(ap, ab) / len_sq;
if raw_t <= 0.0 {
(0.0, ClosestFeature::Vertex)
} else if raw_t >= 1.0 {
(1.0, ClosestFeature::Vertex)
} else {
(raw_t, ClosestFeature::Edge)
}
};
let pos = vadd(a, vscale(ab, t));
ClosestPointResult::new(pos, p, feature, t, 0.0)
}
pub fn point_triangle_closest(
v0: [f64; 3],
v1: [f64; 3],
v2: [f64; 3],
p: [f64; 3],
) -> ClosestPointResult {
let ab = vsub(v1, v0);
let ac = vsub(v2, v0);
let ap = vsub(p, v0);
let d1 = vdot(ab, ap);
let d2 = vdot(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
return ClosestPointResult::new(v0, p, ClosestFeature::Vertex, 0.0, 0.0);
}
let bp = vsub(p, v1);
let d3 = vdot(ab, bp);
let d4 = vdot(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
return ClosestPointResult::new(v1, p, ClosestFeature::Vertex, 1.0, 0.0);
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
let pos = vadd(v0, vscale(ab, v));
return ClosestPointResult::new(pos, p, ClosestFeature::Edge, v, 0.0);
}
let cp = vsub(p, v2);
let d5 = vdot(ab, cp);
let d6 = vdot(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
return ClosestPointResult::new(v2, p, ClosestFeature::Vertex, 0.0, 1.0);
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
let pos = vadd(v0, vscale(ac, w));
return ClosestPointResult::new(pos, p, ClosestFeature::Edge, 0.0, w);
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
let pos = vadd(v1, vscale(vsub(v2, v1), w));
return ClosestPointResult::new(pos, p, ClosestFeature::Edge, 1.0 - w, w);
}
let denom = 1.0 / (va + vb + vc);
let v = vb * denom;
let w = vc * denom;
let pos = vadd(v0, vadd(vscale(ab, v), vscale(ac, w)));
ClosestPointResult::new(pos, p, ClosestFeature::Face, v, w)
}
pub fn segment_segment_closest(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> SegSegResult {
let d1 = vsub(b, a);
let d2 = vsub(d, c);
let r = vsub(a, c);
let e = vdot(d1, d1);
let f = vdot(d2, d2);
let (mut s, t);
const EPS: f64 = 1e-10;
if e < EPS && f < EPS {
s = 0.0;
t = 0.0;
} else if e < EPS {
s = 0.0;
t = vclamp(vdot(r, d2) / f, 0.0, 1.0);
} else {
let c1 = vdot(d1, r);
if f < EPS {
t = 0.0;
s = vclamp(-c1 / e, 0.0, 1.0);
} else {
let b_coeff = vdot(d1, d2);
let denom = e * f - b_coeff * b_coeff;
if denom.abs() > EPS {
let c2 = vdot(d2, r);
s = vclamp((b_coeff * c2 - c1 * f) / denom, 0.0, 1.0);
} else {
s = 0.0;
}
let c2 = vdot(d2, r);
let tnom = b_coeff * s + c2;
if tnom < 0.0 {
t = 0.0;
s = vclamp(-c1 / e, 0.0, 1.0);
} else if tnom > f {
t = 1.0;
s = vclamp((b_coeff - c1) / e, 0.0, 1.0);
} else {
t = tnom / f;
}
}
}
let point_a = vadd(a, vscale(d1, s));
let point_b = vadd(c, vscale(d2, t));
let distance = vdist(point_a, point_b);
SegSegResult {
point_a,
point_b,
s,
t,
distance,
}
}
pub fn point_box_distance(min: [f64; 3], max: [f64; 3], p: [f64; 3]) -> f64 {
let mut dist_sq_outside = 0.0_f64;
for i in 0..3 {
if p[i] < min[i] {
let d = min[i] - p[i];
dist_sq_outside += d * d;
} else if p[i] > max[i] {
let d = p[i] - max[i];
dist_sq_outside += d * d;
}
}
if dist_sq_outside > 0.0 {
return dist_sq_outside.sqrt();
}
let mut min_pen = f64::MAX;
for i in 0..3 {
let d_lo = p[i] - min[i];
let d_hi = max[i] - p[i];
let pen = d_lo.min(d_hi);
if pen < min_pen {
min_pen = pen;
}
}
-min_pen
}
pub fn point_sphere_distance(center: [f64; 3], r: f64, p: [f64; 3]) -> f64 {
vdist(p, center) - r
}
pub fn point_capsule_distance(a: [f64; 3], b: [f64; 3], r: f64, p: [f64; 3]) -> f64 {
let res = point_segment_closest(a, b, p);
res.distance - r
}
#[derive(Debug, Clone)]
pub enum SignedDistanceField {
Sphere {
center: [f64; 3],
r: f64,
},
Box {
half: [f64; 3],
},
Capsule {
a: [f64; 3],
b: [f64; 3],
r: f64,
},
Cylinder {
axis: [f64; 3],
r: f64,
h: f64,
},
}
impl SignedDistanceField {
pub fn eval(&self, p: [f64; 3]) -> f64 {
match self {
SignedDistanceField::Sphere { center, r } => point_sphere_distance(*center, *r, p),
SignedDistanceField::Box { half } => {
let q = [
p[0].abs() - half[0],
p[1].abs() - half[1],
p[2].abs() - half[2],
];
let q_pos = [q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)];
let outside = vlen(q_pos);
let inside = q[0].max(q[1]).max(q[2]).min(0.0);
outside + inside
}
SignedDistanceField::Capsule { a, b, r } => point_capsule_distance(*a, *b, *r, p),
SignedDistanceField::Cylinder { axis, r, h } => {
let ax = vnormalize(*axis);
let proj = vdot(p, ax);
let half_h = h / 2.0;
let radial_pt = vsub(p, vscale(ax, proj));
let rd = vlen(radial_pt) - r;
let yd = proj.abs() - half_h;
let outside_r = rd.max(0.0);
let outside_y = yd.max(0.0);
let outside = (outside_r * outside_r + outside_y * outside_y).sqrt();
let inside = rd.max(yd).min(0.0);
outside + inside
}
}
}
}
#[inline]
pub fn sdf_union(a: f64, b: f64) -> f64 {
a.min(b)
}
#[inline]
pub fn sdf_intersect(a: f64, b: f64) -> f64 {
a.max(b)
}
#[inline]
pub fn sdf_subtract(a: f64, b: f64) -> f64 {
a.max(-b)
}
pub const PROXIMITY_CACHE_CAPACITY: usize = 64;
#[derive(Debug, Clone, Copy)]
pub struct ProximityCacheEntry {
pub id_a: u32,
pub id_b: u32,
pub point_a: [f64; 3],
pub point_b: [f64; 3],
pub distance: f64,
pub feature_a: ClosestFeature,
pub feature_b: ClosestFeature,
pub stamp: u64,
}
#[derive(Debug, Clone)]
pub struct ProximityCache {
entries: Vec<ProximityCacheEntry>,
}
impl Default for ProximityCache {
fn default() -> Self {
Self::new()
}
}
impl ProximityCache {
pub fn new() -> Self {
Self {
entries: Vec::with_capacity(PROXIMITY_CACHE_CAPACITY),
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, id_a: u32, id_b: u32) -> Option<&ProximityCacheEntry> {
let (lo, hi) = if id_a <= id_b {
(id_a, id_b)
} else {
(id_b, id_a)
};
self.entries.iter().find(|e| e.id_a == lo && e.id_b == hi)
}
pub fn insert(
&mut self,
id_a: u32,
id_b: u32,
point_a: [f64; 3],
point_b: [f64; 3],
distance: f64,
feature_a: ClosestFeature,
feature_b: ClosestFeature,
stamp: u64,
) {
let (lo, hi, pa, pb, fa, fb) = if id_a <= id_b {
(id_a, id_b, point_a, point_b, feature_a, feature_b)
} else {
(id_b, id_a, point_b, point_a, feature_b, feature_a)
};
if let Some(e) = self
.entries
.iter_mut()
.find(|e| e.id_a == lo && e.id_b == hi)
{
e.point_a = pa;
e.point_b = pb;
e.distance = distance;
e.feature_a = fa;
e.feature_b = fb;
e.stamp = stamp;
return;
}
let entry = ProximityCacheEntry {
id_a: lo,
id_b: hi,
point_a: pa,
point_b: pb,
distance,
feature_a: fa,
feature_b: fb,
stamp,
};
if self.entries.len() < PROXIMITY_CACHE_CAPACITY {
self.entries.push(entry);
} else {
let oldest_idx = self
.entries
.iter()
.enumerate()
.min_by_key(|(_, e)| e.stamp)
.map(|(i, _)| i)
.unwrap_or(0);
self.entries[oldest_idx] = entry;
}
}
pub fn remove(&mut self, id_a: u32, id_b: u32) {
let (lo, hi) = if id_a <= id_b {
(id_a, id_b)
} else {
(id_b, id_a)
};
self.entries.retain(|e| !(e.id_a == lo && e.id_b == hi));
}
pub fn evict_stale(&mut self, min_stamp: u64) {
self.entries.retain(|e| e.stamp >= min_stamp);
}
}
#[derive(Debug, Clone, Copy)]
pub struct TriTriResult {
pub point_a: [f64; 3],
pub point_b: [f64; 3],
pub distance: f64,
}
pub fn triangle_triangle_distance(
a0: [f64; 3],
a1: [f64; 3],
a2: [f64; 3],
b0: [f64; 3],
b1: [f64; 3],
b2: [f64; 3],
) -> TriTriResult {
let mut best_dist = f64::MAX;
let mut best_pa = a0;
let mut best_pb = b0;
macro_rules! try_pt_tri {
($pt:expr, $q0:expr, $q1:expr, $q2:expr, $is_a:expr) => {{
let r = point_triangle_closest($q0, $q1, $q2, $pt);
if r.distance < best_dist {
best_dist = r.distance;
if $is_a {
best_pa = $pt;
best_pb = r.position;
} else {
best_pa = r.position;
best_pb = $pt;
}
}
}};
}
try_pt_tri!(a0, b0, b1, b2, true);
try_pt_tri!(a1, b0, b1, b2, true);
try_pt_tri!(a2, b0, b1, b2, true);
try_pt_tri!(b0, a0, a1, a2, false);
try_pt_tri!(b1, a0, a1, a2, false);
try_pt_tri!(b2, a0, a1, a2, false);
let edges_a = [(a0, a1), (a1, a2), (a2, a0)];
let edges_b = [(b0, b1), (b1, b2), (b2, b0)];
for &(ea, eb) in &edges_a {
for &(ec, ed) in &edges_b {
let r = segment_segment_closest(ea, eb, ec, ed);
if r.distance < best_dist {
best_dist = r.distance;
best_pa = r.point_a;
best_pb = r.point_b;
}
}
}
TriTriResult {
point_a: best_pa,
point_b: best_pb,
distance: best_dist,
}
}
pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
vlerp([b, 0.0, 0.0], [a, 0.0, 0.0], h)[0] - k * h * (1.0 - h)
}
pub fn point_box_closest(min: [f64; 3], max: [f64; 3], p: [f64; 3]) -> [f64; 3] {
[
p[0].clamp(min[0], max[0]),
p[1].clamp(min[1], max[1]),
p[2].clamp(min[2], max[2]),
]
}
pub fn point_in_triangle_barycentric(
v0: [f64; 3],
v1: [f64; 3],
v2: [f64; 3],
p: [f64; 3],
) -> Option<(f64, f64)> {
let ab = vsub(v1, v0);
let ac = vsub(v2, v0);
let ap = vsub(p, v0);
let n = vcross(ab, ac);
let area2 = vlen(n);
if area2 < 1e-300 {
return None;
}
let inv_area2 = 1.0 / area2;
let u = vdot(vcross(ap, ac), n) * inv_area2;
let v = vdot(vcross(ab, ap), n) * inv_area2;
if u >= 0.0 && v >= 0.0 && u + v <= 1.0 {
Some((u, v))
} else {
None
}
}
pub fn point_plane_distance(normal: [f64; 3], offset_d: f64, p: [f64; 3]) -> f64 {
vdot(normal, p) - offset_d
}
pub fn point_segment_dist_sq(a: [f64; 3], b: [f64; 3], p: [f64; 3]) -> f64 {
let res = point_segment_closest(a, b, p);
res.distance * res.distance
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_point_segment_at_start() {
let r = point_segment_closest([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]);
assert!((r.u_param - 0.0).abs() < EPS, "t should be 0 at start");
assert!((r.distance - 1.0).abs() < EPS);
assert_eq!(r.feature, ClosestFeature::Vertex);
}
#[test]
fn test_point_segment_at_end() {
let r = point_segment_closest([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
assert!((r.u_param - 1.0).abs() < EPS, "t should be 1 at end");
assert!((r.distance - 1.0).abs() < EPS);
assert_eq!(r.feature, ClosestFeature::Vertex);
}
#[test]
fn test_point_segment_midpoint() {
let r = point_segment_closest([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0]);
assert!((r.u_param - 0.5).abs() < EPS);
assert!((r.distance - 1.0).abs() < EPS);
assert_eq!(r.feature, ClosestFeature::Edge);
}
#[test]
fn test_point_segment_degenerate() {
let r = point_segment_closest([3.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert!((r.distance - 3.0).abs() < EPS);
}
#[test]
fn test_point_triangle_vertex_v0() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[-1.0, -1.0, 0.0],
);
assert_eq!(r.feature, ClosestFeature::Vertex);
assert!((r.position[0] - 0.0).abs() < EPS && (r.position[1] - 0.0).abs() < EPS);
}
#[test]
fn test_point_triangle_vertex_v1() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[4.0, 0.0, 0.0],
);
assert_eq!(r.feature, ClosestFeature::Vertex);
assert!((r.position[0] - 2.0).abs() < EPS);
}
#[test]
fn test_point_triangle_vertex_v2() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[0.0, 4.0, 0.0],
);
assert_eq!(r.feature, ClosestFeature::Vertex);
assert!((r.position[1] - 2.0).abs() < EPS);
}
#[test]
fn test_point_triangle_edge_v0v1() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[1.0, -1.0, 0.0],
);
assert_eq!(r.feature, ClosestFeature::Edge);
assert!((r.position[1] - 0.0).abs() < EPS);
}
#[test]
fn test_point_triangle_face_interior() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[0.0, 3.0, 0.0],
[1.0, 1.0, 0.0],
);
assert_eq!(r.feature, ClosestFeature::Face);
assert!((r.distance).abs() < EPS);
}
#[test]
fn test_point_triangle_above_face() {
let r = point_triangle_closest(
[0.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[0.0, 3.0, 0.0],
[1.0, 1.0, 5.0],
);
assert!((r.distance - 5.0).abs() < EPS);
}
#[test]
fn test_segseg_crossing() {
let r = segment_segment_closest(
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[1.0, -1.0, 0.0],
[1.0, 1.0, 0.0],
);
assert!(r.distance < EPS, "crossing segments: dist={}", r.distance);
}
#[test]
fn test_segseg_parallel() {
let r = segment_segment_closest(
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[2.0, 1.0, 0.0],
);
assert!(
(r.distance - 1.0).abs() < EPS,
"parallel dist should be 1.0"
);
}
#[test]
fn test_segseg_skew() {
let r = segment_segment_closest(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 1.0],
[0.5, 1.0, -1.0],
);
assert!((r.distance - 1.0).abs() < EPS);
}
#[test]
fn test_segseg_endpoint_to_endpoint() {
let r = segment_segment_closest(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
);
assert!((r.distance - 1.0).abs() < EPS);
assert!((r.s - 1.0).abs() < EPS);
assert!((r.t - 0.0).abs() < EPS);
}
#[test]
fn test_point_box_inside() {
let d = point_box_distance([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], [0.0, 0.0, 0.0]);
assert!(d < 0.0, "origin should be inside unit box");
assert!((d - (-1.0)).abs() < EPS);
}
#[test]
fn test_point_box_outside_face() {
let d = point_box_distance([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], [2.0, 0.0, 0.0]);
assert!((d - 1.0).abs() < EPS);
}
#[test]
fn test_point_box_outside_corner() {
let d = point_box_distance([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
let expected = (3.0_f64).sqrt();
assert!((d - expected).abs() < EPS);
}
#[test]
fn test_point_box_on_surface() {
let d = point_box_distance([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], [1.0, 0.0, 0.0]);
assert!(
d.abs() < EPS,
"point on surface should have zero signed dist"
);
}
#[test]
fn test_point_sphere_inside() {
let d = point_sphere_distance([0.0, 0.0, 0.0], 2.0, [1.0, 0.0, 0.0]);
assert!((d - (-1.0)).abs() < EPS);
}
#[test]
fn test_point_sphere_outside() {
let d = point_sphere_distance([0.0, 0.0, 0.0], 1.0, [3.0, 0.0, 0.0]);
assert!((d - 2.0).abs() < EPS);
}
#[test]
fn test_point_sphere_on_surface() {
let d = point_sphere_distance([1.0, 2.0, 3.0], 1.5, [2.5, 2.0, 3.0]);
assert!(d.abs() < EPS);
}
#[test]
fn test_point_capsule_inside_cylinder_portion() {
let d = point_capsule_distance([0.0, 0.0, 0.0], [0.0, 4.0, 0.0], 1.0, [0.0, 2.0, 0.0]);
assert!((d - (-1.0)).abs() < EPS);
}
#[test]
fn test_point_capsule_outside_end_cap() {
let d = point_capsule_distance([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5, [0.0, 3.0, 0.0]);
assert!((d - 1.5).abs() < EPS);
}
#[test]
fn test_sdf_sphere_inside() {
let sdf = SignedDistanceField::Sphere {
center: [0.0; 3],
r: 2.0,
};
assert!(sdf.eval([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_sdf_sphere_outside() {
let sdf = SignedDistanceField::Sphere {
center: [0.0; 3],
r: 1.0,
};
let d = sdf.eval([2.0, 0.0, 0.0]);
assert!((d - 1.0).abs() < EPS);
}
#[test]
fn test_sdf_box_center() {
let sdf = SignedDistanceField::Box {
half: [1.0, 1.0, 1.0],
};
let d = sdf.eval([0.0, 0.0, 0.0]);
assert!(d < 0.0);
assert!((d - (-1.0)).abs() < EPS);
}
#[test]
fn test_sdf_box_corner() {
let sdf = SignedDistanceField::Box {
half: [1.0, 1.0, 1.0],
};
let d = sdf.eval([2.0, 2.0, 2.0]);
let expected = (3.0_f64).sqrt();
assert!((d - expected).abs() < EPS);
}
#[test]
fn test_sdf_box_face() {
let sdf = SignedDistanceField::Box {
half: [1.0, 1.0, 1.0],
};
let d = sdf.eval([3.0, 0.0, 0.0]);
assert!((d - 2.0).abs() < EPS);
}
#[test]
fn test_sdf_capsule_inside() {
let sdf = SignedDistanceField::Capsule {
a: [0.0, 0.0, 0.0],
b: [0.0, 2.0, 0.0],
r: 1.0,
};
assert!(sdf.eval([0.0, 1.0, 0.0]) < 0.0);
}
#[test]
fn test_sdf_capsule_outside() {
let sdf = SignedDistanceField::Capsule {
a: [0.0, 0.0, 0.0],
b: [0.0, 2.0, 0.0],
r: 0.5,
};
let d = sdf.eval([0.0, 5.0, 0.0]);
assert!((d - 2.5).abs() < EPS);
}
#[test]
fn test_sdf_cylinder_inside() {
let sdf = SignedDistanceField::Cylinder {
axis: [0.0, 1.0, 0.0],
r: 1.0,
h: 4.0,
};
assert!(sdf.eval([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn test_sdf_cylinder_outside_radially() {
let sdf = SignedDistanceField::Cylinder {
axis: [0.0, 1.0, 0.0],
r: 1.0,
h: 4.0,
};
let d = sdf.eval([3.0, 0.0, 0.0]);
assert!((d - 2.0).abs() < EPS);
}
#[test]
fn test_csg_union_picks_smaller() {
assert!((sdf_union(1.0, -0.5) - (-0.5)).abs() < EPS);
assert!((sdf_union(-0.3, 0.7) - (-0.3)).abs() < EPS);
}
#[test]
fn test_csg_intersect_picks_larger() {
assert!((sdf_intersect(-1.0, 0.5) - 0.5).abs() < EPS);
assert!((sdf_intersect(-0.5, -0.3) - (-0.3)).abs() < EPS);
}
#[test]
fn test_csg_subtract() {
assert!((sdf_subtract(-0.5, -0.3) - 0.3).abs() < EPS);
assert!((sdf_subtract(-1.0, 2.0) - (-1.0)).abs() < EPS);
}
#[test]
fn test_csg_two_spheres_union() {
let s1 = SignedDistanceField::Sphere {
center: [-2.0, 0.0, 0.0],
r: 1.5,
};
let s2 = SignedDistanceField::Sphere {
center: [2.0, 0.0, 0.0],
r: 1.5,
};
let p = [0.0, 0.0, 0.0];
let combined = sdf_union(s1.eval(p), s2.eval(p));
assert!((combined - 0.5).abs() < EPS);
}
#[test]
fn test_proximity_cache_insert_and_get() {
let mut cache = ProximityCache::new();
cache.insert(
1,
2,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
0,
);
let e = cache.get(1, 2).expect("entry should exist");
assert!((e.distance - 1.0).abs() < EPS);
}
#[test]
fn test_proximity_cache_order_independent() {
let mut cache = ProximityCache::new();
cache.insert(
5,
3,
[0.0; 3],
[1.0, 0.0, 0.0],
2.0,
ClosestFeature::Edge,
ClosestFeature::Vertex,
0,
);
assert!(cache.get(3, 5).is_some());
assert!(cache.get(5, 3).is_some());
}
#[test]
fn test_proximity_cache_remove() {
let mut cache = ProximityCache::new();
cache.insert(
1,
2,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
0,
);
cache.remove(1, 2);
assert!(cache.get(1, 2).is_none());
}
#[test]
fn test_proximity_cache_evict_stale() {
let mut cache = ProximityCache::new();
cache.insert(
1,
2,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
0,
);
cache.insert(
3,
4,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
10,
);
cache.evict_stale(5);
assert!(cache.get(1, 2).is_none());
assert!(cache.get(3, 4).is_some());
}
#[test]
fn test_proximity_cache_capacity_evicts_oldest() {
let mut cache = ProximityCache::new();
for i in 0..PROXIMITY_CACHE_CAPACITY as u32 {
cache.insert(
i * 2,
i * 2 + 1,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
i as u64,
);
}
assert_eq!(cache.len(), PROXIMITY_CACHE_CAPACITY);
cache.insert(
200,
201,
[0.0; 3],
[1.0, 0.0, 0.0],
1.0,
ClosestFeature::Face,
ClosestFeature::Face,
1000,
);
assert_eq!(cache.len(), PROXIMITY_CACHE_CAPACITY);
assert!(cache.get(0, 1).is_none());
}
#[test]
fn test_tri_tri_parallel_offset() {
let r = triangle_triangle_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 3.0],
[1.0, 0.0, 3.0],
[0.0, 1.0, 3.0],
);
assert!((r.distance - 3.0).abs() < EPS);
}
#[test]
fn test_tri_tri_touching() {
let r = triangle_triangle_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 0.0],
[-1.0, 0.0, 0.0],
[0.0, -1.0, 0.0],
);
assert!(r.distance < EPS, "touching at origin, dist={}", r.distance);
}
#[test]
fn test_point_box_closest_outside() {
let cp = point_box_closest([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], [3.0, 0.0, 0.0]);
assert!((cp[0] - 1.0).abs() < EPS);
assert!((cp[1] - 0.0).abs() < EPS);
}
#[test]
fn test_sdf_smooth_union_equals_min_when_far_apart() {
let a = -10.0_f64;
let b = 10.0_f64;
let su = sdf_smooth_union(a, b, 0.1);
let u = sdf_union(a, b);
assert!((su - u).abs() < 0.01, "smooth_union far apart should ≈ min");
}
}