#[allow(unused_imports)]
use super::functions::*;
use oxiphysics_core::math::Vec3;
#[derive(Debug, Clone)]
pub struct ContactResult {
pub normal: Vec3,
pub depth: f64,
pub point_a: Vec3,
pub point_b: Vec3,
}
pub fn sphere_sphere(
center_a: Vec3,
radius_a: f64,
center_b: Vec3,
radius_b: f64,
) -> Option<ContactResult> {
let diff = center_b - center_a;
let dist = diff.norm();
let depth = radius_a + radius_b - dist;
if depth < 0.0 {
return None;
}
let normal = if dist > 1e-10 {
diff / dist
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let point_a = center_a + normal * radius_a;
let point_b = center_b - normal * radius_b;
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
pub fn sphere_plane(
sphere_center: Vec3,
radius: f64,
plane_normal: Vec3,
plane_offset: f64,
) -> Option<ContactResult> {
let signed_dist = sphere_center.dot(&plane_normal) - plane_offset;
let depth = radius - signed_dist;
if depth < 0.0 {
return None;
}
let normal = -plane_normal;
let point_a = sphere_center - plane_normal * radius;
let point_b = sphere_center - plane_normal * signed_dist;
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
pub fn sphere_aabb(
sphere_center: Vec3,
radius: f64,
aabb_min: Vec3,
aabb_max: Vec3,
) -> Option<ContactResult> {
let closest = Vec3::new(
sphere_center.x.clamp(aabb_min.x, aabb_max.x),
sphere_center.y.clamp(aabb_min.y, aabb_max.y),
sphere_center.z.clamp(aabb_min.z, aabb_max.z),
);
let diff = sphere_center - closest;
let dist_sq = diff.norm_squared();
if dist_sq >= radius * radius {
return None;
}
let dist = dist_sq.sqrt();
let depth = radius - dist;
let normal = if dist > 1e-10 {
diff / dist
} else {
let dx = (sphere_center.x - aabb_min.x).min(aabb_max.x - sphere_center.x);
let dy = (sphere_center.y - aabb_min.y).min(aabb_max.y - sphere_center.y);
let dz = (sphere_center.z - aabb_min.z).min(aabb_max.z - sphere_center.z);
if dx <= dy && dx <= dz {
let sign = if sphere_center.x - aabb_min.x < aabb_max.x - sphere_center.x {
-1.0
} else {
1.0
};
Vec3::new(sign, 0.0, 0.0)
} else if dy <= dz {
let sign = if sphere_center.y - aabb_min.y < aabb_max.y - sphere_center.y {
-1.0
} else {
1.0
};
Vec3::new(0.0, sign, 0.0)
} else {
let sign = if sphere_center.z - aabb_min.z < aabb_max.z - sphere_center.z {
-1.0
} else {
1.0
};
Vec3::new(0.0, 0.0, sign)
}
};
let point_a = sphere_center - normal * radius;
let point_b = closest;
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
pub fn capsule_capsule(
a0: Vec3,
a1: Vec3,
radius_a: f64,
b0: Vec3,
b1: Vec3,
radius_b: f64,
) -> Option<ContactResult> {
let (_, _, ca, cb) = closest_points_segment_segment(a0, a1, b0, b1);
sphere_sphere(ca, radius_a, cb, radius_b)
}
pub fn closest_point_on_segment(p0: Vec3, p1: Vec3, q: Vec3) -> (Vec3, f64) {
let d = p1 - p0;
let len_sq = d.dot(&d);
if len_sq < 1e-20 {
return (p0, 0.0);
}
let t = ((q - p0).dot(&d) / len_sq).clamp(0.0, 1.0);
(p0 + d * t, t)
}
pub fn closest_points_segment_segment(
a0: Vec3,
a1: Vec3,
b0: Vec3,
b1: Vec3,
) -> (f64, f64, Vec3, Vec3) {
let d1 = a1 - a0;
let d2 = b1 - b0;
let r = a0 - b0;
let a = d1.dot(&d1);
let e = d2.dot(&d2);
let f = d2.dot(&r);
let (s, t) = if a <= 1e-10 && e <= 1e-10 {
(0.0_f64, 0.0_f64)
} else if a <= 1e-10 {
(0.0, (f / e).clamp(0.0, 1.0))
} else {
let c = d1.dot(&r);
if e <= 1e-10 {
((-c / a).clamp(0.0, 1.0), 0.0)
} else {
let b = d1.dot(&d2);
let denom = a * e - b * b;
let s = if denom.abs() > 1e-10 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let t_raw = (b * s + f) / e;
if t_raw < 0.0 {
let s2 = (-c / a).clamp(0.0, 1.0);
(s2, 0.0)
} else if t_raw > 1.0 {
let s2 = ((b - c) / a).clamp(0.0, 1.0);
(s2, 1.0)
} else {
(s, t_raw)
}
}
};
(s, t, a0 + d1 * s, b0 + d2 * t)
}
pub fn sphere_triangle(
sphere_center: Vec3,
radius: f64,
v0: Vec3,
v1: Vec3,
v2: Vec3,
) -> Option<ContactResult> {
let closest = closest_point_on_triangle(v0, v1, v2, sphere_center);
let diff = sphere_center - closest;
let dist = diff.norm();
let depth = radius - dist;
if depth < 0.0 {
return None;
}
let normal = if dist > 1e-10 {
diff / dist
} else {
let n = (v1 - v0).cross(&(v2 - v0));
let n_len = n.norm();
if n_len > 1e-10 {
n / n_len
} else {
Vec3::new(0.0, 1.0, 0.0)
}
};
let point_a = sphere_center - normal * radius;
let point_b = closest;
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
pub fn closest_point_on_triangle(v0: Vec3, v1: Vec3, v2: Vec3, p: Vec3) -> Vec3 {
let ab = v1 - v0;
let ac = v2 - v0;
let ap = p - v0;
let d1 = ab.dot(&ap);
let d2 = ac.dot(&ap);
if d1 <= 0.0 && d2 <= 0.0 {
return v0;
}
let bp = p - v1;
let d3 = ab.dot(&bp);
let d4 = ac.dot(&bp);
if d3 >= 0.0 && d4 <= d3 {
return v1;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
return v0 + ab * v;
}
let cp = p - v2;
let d5 = ab.dot(&cp);
let d6 = ac.dot(&cp);
if d6 >= 0.0 && d5 <= d6 {
return v2;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
return v0 + ac * 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));
return v1 + (v2 - v1) * w;
}
let denom = 1.0 / (va + vb + vc);
let v = vb * denom;
let w = vc * denom;
v0 + ab * v + ac * w
}
pub fn aabb_aabb(min_a: Vec3, max_a: Vec3, min_b: Vec3, max_b: Vec3) -> Option<ContactResult> {
let ox = max_a.x.min(max_b.x) - min_a.x.max(min_b.x);
let oy = max_a.y.min(max_b.y) - min_a.y.max(min_b.y);
let oz = max_a.z.min(max_b.z) - min_a.z.max(min_b.z);
if ox <= 0.0 || oy <= 0.0 || oz <= 0.0 {
return None;
}
let center_a = (min_a + max_a) * 0.5;
let center_b = (min_b + max_b) * 0.5;
let dir = center_b - center_a;
let (depth, normal) = if ox <= oy && ox <= oz {
let sign = if dir.x >= 0.0 { 1.0 } else { -1.0 };
(ox, Vec3::new(sign, 0.0, 0.0))
} else if oy <= oz {
let sign = if dir.y >= 0.0 { 1.0 } else { -1.0 };
(oy, Vec3::new(0.0, sign, 0.0))
} else {
let sign = if dir.z >= 0.0 { 1.0 } else { -1.0 };
(oz, Vec3::new(0.0, 0.0, sign))
};
let point_a = center_a + normal * (max_a - center_a).dot(&normal);
let point_b = center_b - normal * (center_b - min_b).dot(&normal);
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
pub fn plane_aabb(
plane_normal: Vec3,
plane_offset: f64,
aabb_min: Vec3,
aabb_max: Vec3,
) -> Option<ContactResult> {
let support = Vec3::new(
if plane_normal.x >= 0.0 {
aabb_min.x
} else {
aabb_max.x
},
if plane_normal.y >= 0.0 {
aabb_min.y
} else {
aabb_max.y
},
if plane_normal.z >= 0.0 {
aabb_min.z
} else {
aabb_max.z
},
);
let signed_dist = support.dot(&plane_normal) - plane_offset;
let depth = -signed_dist; if depth < 0.0 {
return None;
}
let normal = -plane_normal;
let point_a = support;
let point_b = support - plane_normal * signed_dist; Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
#[allow(dead_code)]
pub fn point_triangle_distance_sq(p: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> f64 {
let closest = closest_point_on_triangle(v0, v1, v2, p);
(p - closest).norm_squared()
}
#[allow(dead_code)]
pub fn point_triangle_distance(p: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> f64 {
point_triangle_distance_sq(p, v0, v1, v2).sqrt()
}
#[allow(dead_code)]
pub fn edge_edge_distance_sq(a0: Vec3, a1: Vec3, b0: Vec3, b1: Vec3) -> f64 {
let (_, _, ca, cb) = closest_points_segment_segment(a0, a1, b0, b1);
(ca - cb).norm_squared()
}
#[allow(dead_code)]
pub fn edge_edge_distance(a0: Vec3, a1: Vec3, b0: Vec3, b1: Vec3) -> f64 {
edge_edge_distance_sq(a0, a1, b0, b1).sqrt()
}
#[allow(dead_code)]
pub fn segment_triangle_intersection(
seg_start: Vec3,
seg_end: Vec3,
v0: Vec3,
v1: Vec3,
v2: Vec3,
) -> Option<(f64, Vec3)> {
let dir = seg_end - seg_start;
let e1 = v1 - v0;
let e2 = v2 - v0;
let h = dir.cross(&e2);
let a = e1.dot(&h);
if a.abs() < 1e-12 {
return None; }
let f = 1.0 / a;
let s = seg_start - v0;
let u = f * s.dot(&h);
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = s.cross(&e1);
let v = f * dir.dot(&q);
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = f * e2.dot(&q);
if (0.0..=1.0).contains(&t) {
let point = seg_start + dir * t;
Some((t, point))
} else {
None
}
}
#[allow(dead_code)]
pub fn triangle_triangle_intersects(
a0: Vec3,
a1: Vec3,
a2: Vec3,
b0: Vec3,
b1: Vec3,
b2: Vec3,
) -> bool {
let a_edges = [(a0, a1), (a1, a2), (a2, a0)];
for &(start, end) in &a_edges {
if segment_triangle_intersection(start, end, b0, b1, b2).is_some() {
return true;
}
}
let b_edges = [(b0, b1), (b1, b2), (b2, b0)];
for &(start, end) in &b_edges {
if segment_triangle_intersection(start, end, a0, a1, a2).is_some() {
return true;
}
}
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClosestFeature {
VertexVertex,
VertexEdge,
EdgeVertex,
EdgeEdge,
VertexFace,
FaceVertex,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ClosestFeatureResult {
pub feature: ClosestFeature,
pub point_a: Vec3,
pub point_b: Vec3,
pub distance: f64,
}
#[allow(dead_code)]
pub fn closest_feature_pair(
a0: Vec3,
a1: Vec3,
a2: Vec3,
b0: Vec3,
b1: Vec3,
b2: Vec3,
) -> ClosestFeatureResult {
let a_verts = [a0, a1, a2];
let b_verts = [b0, b1, b2];
let a_edges = [(a0, a1), (a1, a2), (a2, a0)];
let b_edges = [(b0, b1), (b1, b2), (b2, b0)];
let mut best_dist = f64::MAX;
let mut best_pa = Vec3::zeros();
let mut best_pb = Vec3::zeros();
let mut best_feature = ClosestFeature::VertexVertex;
for &va in &a_verts {
for &vb in &b_verts {
let d = (va - vb).norm();
if d < best_dist {
best_dist = d;
best_pa = va;
best_pb = vb;
best_feature = ClosestFeature::VertexVertex;
}
}
}
for &va in &a_verts {
for &(e0, e1) in &b_edges {
let (closest, _) = closest_point_on_segment(e0, e1, va);
let d = (va - closest).norm();
if d < best_dist {
best_dist = d;
best_pa = va;
best_pb = closest;
best_feature = ClosestFeature::VertexEdge;
}
}
}
for &vb in &b_verts {
for &(e0, e1) in &a_edges {
let (closest, _) = closest_point_on_segment(e0, e1, vb);
let d = (vb - closest).norm();
if d < best_dist {
best_dist = d;
best_pa = closest;
best_pb = vb;
best_feature = ClosestFeature::EdgeVertex;
}
}
}
for &(a_e0, a_e1) in &a_edges {
for &(b_e0, b_e1) in &b_edges {
let (_, _, ca, cb) = closest_points_segment_segment(a_e0, a_e1, b_e0, b_e1);
let d = (ca - cb).norm();
if d < best_dist {
best_dist = d;
best_pa = ca;
best_pb = cb;
best_feature = ClosestFeature::EdgeEdge;
}
}
}
for &va in &a_verts {
let closest = closest_point_on_triangle(b0, b1, b2, va);
let d = (va - closest).norm();
if d < best_dist {
best_dist = d;
best_pa = va;
best_pb = closest;
best_feature = ClosestFeature::VertexFace;
}
}
for &vb in &b_verts {
let closest = closest_point_on_triangle(a0, a1, a2, vb);
let d = (vb - closest).norm();
if d < best_dist {
best_dist = d;
best_pa = closest;
best_pb = vb;
best_feature = ClosestFeature::FaceVertex;
}
}
ClosestFeatureResult {
feature: best_feature,
point_a: best_pa,
point_b: best_pb,
distance: best_dist,
}
}
#[allow(dead_code)]
pub fn point_plane_signed_distance(p: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> f64 {
let n = (v1 - v0).cross(&(v2 - v0));
let n_len = n.norm();
if n_len < 1e-12 {
return 0.0;
}
(p - v0).dot(&n) / n_len
}
#[allow(dead_code)]
pub fn barycentric_coordinates(p: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> (f64, f64, f64) {
let e0 = v1 - v0;
let e1 = v2 - v0;
let e2 = p - v0;
let d00 = e0.dot(&e0);
let d01 = e0.dot(&e1);
let d11 = e1.dot(&e1);
let d20 = e2.dot(&e0);
let d21 = e2.dot(&e1);
let denom = d00 * d11 - d01 * d01;
if denom.abs() < 1e-14 {
return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
}
let v = (d11 * d20 - d01 * d21) / denom;
let w = (d00 * d21 - d01 * d20) / denom;
let u = 1.0 - v - w;
(u, v, w)
}
#[allow(dead_code)]
pub fn closest_point_on_cylinder_axis(c0: Vec3, c1: Vec3, p: Vec3) -> (Vec3, f64) {
closest_point_on_segment(c0, c1, p)
}
#[allow(dead_code)]
pub fn sphere_cylinder(
sphere_center: Vec3,
sphere_radius: f64,
c0: Vec3,
c1: Vec3,
cyl_radius: f64,
) -> Option<ContactResult> {
let (axis_pt, _t) = closest_point_on_segment(c0, c1, sphere_center);
let radial = sphere_center - axis_pt;
let radial_dist = radial.norm();
let sum_r = sphere_radius + cyl_radius;
let depth = sum_r - radial_dist;
if depth < 0.0 {
return None;
}
let normal = if radial_dist > 1e-10 {
radial / radial_dist
} else {
let axis = c1 - c0;
let axis_len = axis.norm();
if axis_len > 1e-10 {
let perp = if axis.x.abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0) - axis / axis_len * (axis.x / axis_len)
} else {
Vec3::new(0.0, 1.0, 0.0) - axis / axis_len * (axis.y / axis_len)
};
let pn = perp.norm();
if pn > 1e-10 {
perp / pn
} else {
Vec3::new(0.0, 1.0, 0.0)
}
} else {
Vec3::new(0.0, 1.0, 0.0)
}
};
let point_a = sphere_center - normal * sphere_radius;
let point_b = axis_pt + normal * cyl_radius;
Some(ContactResult {
normal,
depth,
point_a,
point_b,
})
}
#[allow(dead_code)]
pub fn capsule_cylinder(
a0: Vec3,
a1: Vec3,
r_cap: f64,
c0: Vec3,
c1: Vec3,
r_cyl: f64,
) -> Option<ContactResult> {
let (_s, _t, ca, _cb) = closest_points_segment_segment(a0, a1, c0, c1);
sphere_cylinder(ca, r_cap, c0, c1, r_cyl)
}
#[allow(dead_code)]
pub fn box_cylinder_approx(
aabb_min: Vec3,
aabb_max: Vec3,
c0: Vec3,
c1: Vec3,
cyl_radius: f64,
) -> Option<ContactResult> {
let box_center = (aabb_min + aabb_max) * 0.5;
let (axis_pt, _) = closest_point_on_segment(c0, c1, box_center);
let half = (aabb_max - aabb_min) * 0.5;
let max_half = half.x.max(half.y).max(half.z);
let sum_r = cyl_radius + max_half;
let diff = box_center - axis_pt;
if diff.norm() > sum_r {
return None;
}
sphere_aabb(axis_pt, cyl_radius, aabb_min, aabb_max)
}
#[allow(dead_code)]
pub fn sdf_sphere(p: Vec3, center: Vec3, radius: f64) -> f64 {
(p - center).norm() - radius
}
#[allow(dead_code)]
pub fn sdf_aabb(p: Vec3, aabb_min: Vec3, aabb_max: Vec3) -> f64 {
let center = (aabb_min + aabb_max) * 0.5;
let half = (aabb_max - aabb_min) * 0.5;
let q = Vec3::new(
(p.x - center.x).abs() - half.x,
(p.y - center.y).abs() - half.y,
(p.z - center.z).abs() - half.z,
);
let outside = Vec3::new(q.x.max(0.0), q.y.max(0.0), q.z.max(0.0)).norm();
let inside = q.x.max(q.y).max(q.z).min(0.0);
outside + inside
}
#[allow(dead_code)]
pub fn sdf_capsule(p: Vec3, c0: Vec3, c1: Vec3, radius: f64) -> f64 {
let (closest, _) = closest_point_on_segment(c0, c1, p);
(p - closest).norm() - radius
}
#[allow(dead_code)]
pub fn sdf_plane(p: Vec3, normal: Vec3, d: f64) -> f64 {
p.dot(&normal) - d
}
#[allow(dead_code)]
pub fn sdf_cylinder_infinite(p: Vec3, c0: Vec3, c1: Vec3, radius: f64) -> f64 {
let axis = c1 - c0;
let axis_len = axis.norm();
if axis_len < 1e-12 {
return (p - c0).norm() - radius;
}
let axis_dir = axis / axis_len;
let t = (p - c0).dot(&axis_dir);
let axis_pt = c0 + axis_dir * t;
(p - axis_pt).norm() - radius
}
#[allow(dead_code)]
pub fn point_in_convex(p: Vec3, planes: &[(Vec3, f64)]) -> bool {
for &(n, d) in planes {
if p.dot(&n) < d - 1e-10 {
return false;
}
}
true
}
#[allow(dead_code)]
pub fn aabb_face_planes(aabb_min: Vec3, aabb_max: Vec3) -> Vec<(Vec3, f64)> {
vec![
(Vec3::new(1.0, 0.0, 0.0), aabb_min.x),
(Vec3::new(-1.0, 0.0, 0.0), -aabb_max.x),
(Vec3::new(0.0, 1.0, 0.0), aabb_min.y),
(Vec3::new(0.0, -1.0, 0.0), -aabb_max.y),
(Vec3::new(0.0, 0.0, 1.0), aabb_min.z),
(Vec3::new(0.0, 0.0, -1.0), -aabb_max.z),
]
}
#[allow(dead_code)]
pub fn closest_point_on_triangle_with_bary(
v0: Vec3,
v1: Vec3,
v2: Vec3,
p: Vec3,
) -> (Vec3, f64, f64, f64) {
let closest = closest_point_on_triangle(v0, v1, v2, p);
let (u, v, w) = barycentric_coordinates(closest, v0, v1, v2);
(closest, u, v, w)
}
#[allow(dead_code)]
pub fn triangle_area(v0: Vec3, v1: Vec3, v2: Vec3) -> f64 {
let e1 = v1 - v0;
let e2 = v2 - v0;
e1.cross(&e2).norm() * 0.5
}
#[allow(dead_code)]
pub fn triangle_normal(v0: Vec3, v1: Vec3, v2: Vec3) -> Vec3 {
let n = (v1 - v0).cross(&(v2 - v0));
let len = n.norm();
if len > 1e-12 {
n / len
} else {
Vec3::new(0.0, 1.0, 0.0)
}
}
#[allow(dead_code)]
pub fn project_onto_triangle_plane(v0: Vec3, v1: Vec3, v2: Vec3, p: Vec3) -> Vec3 {
let n = triangle_normal(v0, v1, v2);
let dist = (p - v0).dot(&n);
p - n * dist
}
#[allow(dead_code)]
pub fn point_aabb_signed_distance(p: Vec3, aabb_min: Vec3, aabb_max: Vec3) -> f64 {
sdf_aabb(p, aabb_min, aabb_max)
}
#[allow(dead_code)]
pub fn contact_tangent_basis(normal: Vec3) -> (Vec3, Vec3) {
let t_u = if normal.x.abs() < 0.9 {
let c = Vec3::new(1.0, 0.0, 0.0);
let v = c - normal * c.dot(&normal);
let l = v.norm();
if l > 1e-10 {
v / l
} else {
Vec3::new(0.0, 1.0, 0.0)
}
} else {
let c = Vec3::new(0.0, 1.0, 0.0);
let v = c - normal * c.dot(&normal);
let l = v.norm();
if l > 1e-10 {
v / l
} else {
Vec3::new(1.0, 0.0, 0.0)
}
};
let t_v = normal.cross(&t_u);
(t_u, t_v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sphere_sphere_touching() {
let c = sphere_sphere(Vec3::new(0.0, 0.0, 0.0), 1.0, Vec3::new(2.0, 0.0, 0.0), 1.0)
.expect("touching spheres must produce a contact");
assert!(c.depth.abs() < 1e-10, "depth should be ≈0, got {}", c.depth);
}
#[test]
fn test_sphere_sphere_overlapping() {
let c = sphere_sphere(Vec3::new(0.0, 0.0, 0.0), 1.0, Vec3::new(1.5, 0.0, 0.0), 1.0)
.expect("overlapping spheres must produce a contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.depth - 0.5).abs() < 1e-10,
"expected depth 0.5, got {}",
c.depth
);
assert!(
(c.normal.norm() - 1.0).abs() < 1e-10,
"normal must be unit length"
);
}
#[test]
fn test_sphere_sphere_separated() {
assert!(
sphere_sphere(Vec3::new(0.0, 0.0, 0.0), 1.0, Vec3::new(5.0, 0.0, 0.0), 1.0,).is_none(),
"separated spheres must return None"
);
}
#[test]
fn test_sphere_plane_above() {
assert!(
sphere_plane(Vec3::new(0.0, 2.0, 0.0), 1.0, Vec3::new(0.0, 1.0, 0.0), 0.0,).is_none(),
"sphere above plane should return None"
);
}
#[test]
fn test_sphere_plane_penetrating() {
let c = sphere_plane(Vec3::new(0.0, 0.5, 0.0), 1.0, Vec3::new(0.0, 1.0, 0.0), 0.0)
.expect("sphere penetrating plane must return contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.depth - 0.5).abs() < 1e-10,
"expected depth 0.5, got {}",
c.depth
);
}
#[test]
fn test_capsule_capsule_parallel() {
let c = capsule_capsule(
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
0.5,
Vec3::new(0.8, -1.0, 0.0),
Vec3::new(0.8, 1.0, 0.0),
0.5,
)
.expect("parallel overlapping capsules must produce contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.normal.norm() - 1.0).abs() < 1e-10,
"normal must be unit length"
);
}
#[test]
fn test_capsule_capsule_miss() {
assert!(
capsule_capsule(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.3,
Vec3::new(10.0, 0.0, 0.0),
Vec3::new(11.0, 0.0, 0.0),
0.3,
)
.is_none(),
"far-apart capsules must return None"
);
}
#[test]
fn test_sphere_triangle_above_face() {
let v0 = Vec3::new(-1.0, 0.0, -1.0);
let v1 = Vec3::new(1.0, 0.0, -1.0);
let v2 = Vec3::new(0.0, 0.0, 1.0);
let c = sphere_triangle(Vec3::new(0.0, 0.5, 0.0), 1.0, v0, v1, v2)
.expect("sphere above triangle face must produce contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.normal.norm() - 1.0).abs() < 1e-10,
"normal must be unit length"
);
}
#[test]
fn test_sphere_triangle_near_edge() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(2.0, 0.0, 0.0);
let v2 = Vec3::new(1.0, 0.0, 2.0);
let c = sphere_triangle(Vec3::new(1.0, 0.4, -0.3), 0.6, v0, v1, v2)
.expect("sphere near edge must produce contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
}
#[test]
fn test_aabb_aabb_separated() {
assert!(
aabb_aabb(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(3.0, 1.0, 1.0),
)
.is_none(),
"separated AABBs must return None"
);
}
#[test]
fn test_aabb_aabb_overlapping() {
let c = aabb_aabb(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(0.5, 0.0, 0.0),
Vec3::new(1.5, 1.0, 1.0),
)
.expect("overlapping AABBs must produce a contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.normal.x.abs() - 1.0).abs() < 1e-10,
"normal should be along X"
);
}
#[test]
fn test_closest_point_segment() {
let (pt, t) = closest_point_on_segment(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 0.0),
);
assert!((pt.x - 1.0).abs() < 1e-10, "expected x=1.0, got {}", pt.x);
assert!(pt.y.abs() < 1e-10, "expected y=0.0, got {}", pt.y);
assert!((t - 0.5).abs() < 1e-10, "expected t=0.5, got {}", t);
}
#[test]
fn test_point_triangle_distance_above() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let p = Vec3::new(0.25, 0.25, 1.0); let d = point_triangle_distance(p, v0, v1, v2);
assert!((d - 1.0).abs() < 1e-10, "Expected distance 1.0, got {d}");
}
#[test]
fn test_point_triangle_distance_on_triangle() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let p = Vec3::new(0.25, 0.25, 0.0); let d = point_triangle_distance(p, v0, v1, v2);
assert!(
d < 1e-10,
"Point on triangle should have distance ~0, got {d}"
);
}
#[test]
fn test_point_triangle_distance_near_vertex() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let p = Vec3::new(-1.0, -1.0, 0.0); let d = point_triangle_distance(p, v0, v1, v2);
let expected = (1.0_f64 + 1.0).sqrt(); assert!((d - expected).abs() < 1e-10, "Expected {expected}, got {d}");
}
#[test]
fn test_edge_edge_distance_parallel() {
let d = edge_edge_distance(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(2.0, 1.0, 0.0),
);
assert!((d - 2.0).abs() < 1e-10, "Expected distance 2.0, got {d}");
}
#[test]
fn test_edge_edge_distance_crossing() {
let d = edge_edge_distance(
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, -1.0, 1.0),
Vec3::new(0.0, 1.0, 1.0),
);
assert!((d - 1.0).abs() < 1e-10, "Expected distance 1.0, got {d}");
}
#[test]
fn test_segment_triangle_hit() {
let v0 = Vec3::new(-1.0, 0.0, -1.0);
let v1 = Vec3::new(1.0, 0.0, -1.0);
let v2 = Vec3::new(0.0, 0.0, 1.0);
let result = segment_triangle_intersection(
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
v0,
v1,
v2,
);
assert!(result.is_some(), "Segment should hit the triangle");
let (t, pt) = result.unwrap();
assert!((t - 0.5).abs() < 1e-10, "t should be 0.5, got {t}");
assert!(pt.y.abs() < 1e-10, "intersection should be at y=0");
}
#[test]
fn test_segment_triangle_miss() {
let v0 = Vec3::new(-1.0, 0.0, -1.0);
let v1 = Vec3::new(1.0, 0.0, -1.0);
let v2 = Vec3::new(0.0, 0.0, 1.0);
let result = segment_triangle_intersection(
Vec3::new(-1.0, 1.0, 0.0),
Vec3::new(1.0, 1.0, 0.0),
v0,
v1,
v2,
);
assert!(result.is_none(), "Parallel segment should miss");
}
#[test]
fn test_triangle_triangle_intersects() {
let a0 = Vec3::new(-1.0, 0.0, -1.0);
let a1 = Vec3::new(1.0, 0.0, -1.0);
let a2 = Vec3::new(0.0, 0.0, 1.0);
let b0 = Vec3::new(0.0, -1.0, 0.0);
let b1 = Vec3::new(0.0, 1.0, 0.0);
let b2 = Vec3::new(0.0, 0.0, 2.0);
assert!(
triangle_triangle_intersects(a0, a1, a2, b0, b1, b2),
"Crossing triangles should intersect"
);
}
#[test]
fn test_triangle_triangle_no_intersect() {
let a0 = Vec3::new(0.0, 0.0, 0.0);
let a1 = Vec3::new(1.0, 0.0, 0.0);
let a2 = Vec3::new(0.0, 1.0, 0.0);
let b0 = Vec3::new(0.0, 0.0, 10.0);
let b1 = Vec3::new(1.0, 0.0, 10.0);
let b2 = Vec3::new(0.0, 1.0, 10.0);
assert!(
!triangle_triangle_intersects(a0, a1, a2, b0, b1, b2),
"Separated triangles should not intersect"
);
}
#[test]
fn test_closest_feature_pair_vertex_vertex() {
let a0 = Vec3::new(0.0, 0.0, 0.0);
let a1 = Vec3::new(1.0, 0.0, 0.0);
let a2 = Vec3::new(0.0, 1.0, 0.0);
let b0 = Vec3::new(5.0, 5.0, 0.0);
let b1 = Vec3::new(6.0, 5.0, 0.0);
let b2 = Vec3::new(5.0, 6.0, 0.0);
let result = closest_feature_pair(a0, a1, a2, b0, b1, b2);
assert!(
result.distance > 0.0,
"Separated triangles should have positive distance"
);
assert!(result.distance.is_finite(), "Distance should be finite");
}
#[test]
fn test_closest_feature_pair_nearby() {
let a0 = Vec3::new(0.0, 0.0, 0.0);
let a1 = Vec3::new(1.0, 0.0, 0.0);
let a2 = Vec3::new(0.5, 1.0, 0.0);
let b0 = Vec3::new(1.0, 0.0, 0.0);
let b1 = Vec3::new(2.0, 0.0, 0.0);
let b2 = Vec3::new(1.5, 1.0, 0.0);
let result = closest_feature_pair(a0, a1, a2, b0, b1, b2);
assert!(
result.distance < 1e-10,
"Adjacent triangles sharing a vertex should have ~0 distance"
);
}
#[test]
fn test_barycentric_at_vertices() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let (u, v, w) = barycentric_coordinates(v0, v0, v1, v2);
assert!((u - 1.0).abs() < 1e-10, "u should be 1 at v0, got {u}");
assert!(v.abs() < 1e-10, "v should be 0 at v0, got {v}");
assert!(w.abs() < 1e-10, "w should be 0 at v0, got {w}");
}
#[test]
fn test_barycentric_at_centroid() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let centroid = (v0 + v1 + v2) * (1.0 / 3.0);
let (u, v, w) = barycentric_coordinates(centroid, v0, v1, v2);
assert!((u - 1.0 / 3.0).abs() < 1e-10, "u at centroid: {u}");
assert!((v - 1.0 / 3.0).abs() < 1e-10, "v at centroid: {v}");
assert!((w - 1.0 / 3.0).abs() < 1e-10, "w at centroid: {w}");
}
#[test]
fn test_point_plane_signed_distance() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let d = point_plane_signed_distance(Vec3::new(0.5, 0.5, 2.0), v0, v1, v2);
assert!(
d > 0.0,
"Point above plane should have positive distance, got {d}"
);
let d2 = point_plane_signed_distance(Vec3::new(0.5, 0.5, -2.0), v0, v1, v2);
assert!(
d2 < 0.0,
"Point below plane should have negative distance, got {d2}"
);
let d3 = point_plane_signed_distance(Vec3::new(0.5, 0.5, 0.0), v0, v1, v2);
assert!(
d3.abs() < 1e-10,
"Point on plane should have zero distance, got {d3}"
);
}
#[test]
fn test_plane_aabb_penetrating() {
let c = plane_aabb(
Vec3::new(0.0, 1.0, 0.0),
0.5, Vec3::new(-1.0, 0.0, -1.0), Vec3::new(1.0, 1.0, 1.0), )
.expect("AABB should penetrate the plane");
assert!(c.depth > 0.0, "depth should be positive, got {}", c.depth);
}
#[test]
fn test_plane_aabb_above() {
let result = plane_aabb(
Vec3::new(0.0, 1.0, 0.0),
-5.0, Vec3::new(-1.0, 0.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
);
assert!(result.is_none(), "AABB above plane should not collide");
}
#[test]
fn test_sphere_aabb_inside() {
let c = sphere_aabb(
Vec3::new(0.0, 0.0, 0.0),
0.1,
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
)
.expect("Sphere inside AABB should produce contact");
assert!(c.depth > 0.0, "depth should be positive");
}
#[test]
fn test_sphere_cylinder_overlapping() {
let c = sphere_cylinder(
Vec3::new(1.2, 0.5, 0.0),
0.5,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
)
.expect("Sphere overlapping cylinder must produce contact");
assert!(c.depth > 0.0, "depth must be positive, got {}", c.depth);
assert!(
(c.normal.norm() - 1.0).abs() < 1e-10,
"normal must be unit length"
);
}
#[test]
fn test_sphere_cylinder_separated() {
let result = sphere_cylinder(
Vec3::new(10.0, 0.0, 0.0),
0.5,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
);
assert!(result.is_none(), "far sphere should return None");
}
#[test]
fn test_sphere_cylinder_touching() {
let c = sphere_cylinder(
Vec3::new(1.5, 0.0, 0.0),
0.5,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
)
.expect("touching sphere-cylinder must produce contact");
assert!(c.depth.abs() < 1e-10, "depth should be ~0, got {}", c.depth);
}
#[test]
fn test_capsule_cylinder_overlapping() {
let result = capsule_cylinder(
Vec3::new(1.2, 0.0, -0.5),
Vec3::new(1.2, 0.0, 0.5),
0.5,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
);
assert!(result.is_some(), "capsule close to cylinder should collide");
}
#[test]
fn test_capsule_cylinder_separated() {
let result = capsule_cylinder(
Vec3::new(5.0, 0.0, -0.5),
Vec3::new(5.0, 0.0, 0.5),
0.3,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
0.5,
);
assert!(
result.is_none(),
"far capsule should not collide with cylinder"
);
}
#[test]
fn test_box_cylinder_approx_overlapping() {
let result = box_cylinder_approx(
Vec3::new(-0.5, -0.5, -0.5),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(0.0, -2.0, 0.0),
Vec3::new(0.0, 2.0, 0.0),
0.8,
);
assert!(
result.is_some(),
"box overlapping cylinder should produce contact"
);
}
#[test]
fn test_box_cylinder_approx_separated() {
let result = box_cylinder_approx(
Vec3::new(-0.5, -0.5, -0.5),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(10.0, -2.0, 0.0),
Vec3::new(10.0, 2.0, 0.0),
0.3,
);
assert!(result.is_none(), "far box-cylinder should return None");
}
#[test]
fn test_sdf_sphere_outside() {
let d = sdf_sphere(Vec3::new(3.0, 0.0, 0.0), Vec3::zeros(), 1.0);
assert!((d - 2.0).abs() < 1e-10, "Expected 2.0, got {d}");
}
#[test]
fn test_sdf_sphere_inside() {
let d = sdf_sphere(Vec3::new(0.5, 0.0, 0.0), Vec3::zeros(), 1.0);
assert!(
d < 0.0,
"Point inside sphere should have negative SDF, got {d}"
);
assert!((d - (-0.5)).abs() < 1e-10, "Expected -0.5, got {d}");
}
#[test]
fn test_sdf_sphere_on_surface() {
let d = sdf_sphere(Vec3::new(1.0, 0.0, 0.0), Vec3::zeros(), 1.0);
assert!(
d.abs() < 1e-10,
"Point on sphere surface should have SDF ≈ 0, got {d}"
);
}
#[test]
fn test_sdf_aabb_outside() {
let d = sdf_aabb(
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
);
assert!((d - 1.0).abs() < 1e-10, "Expected 1.0, got {d}");
}
#[test]
fn test_sdf_aabb_inside() {
let d = sdf_aabb(
Vec3::zeros(),
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
);
assert!(
d < 0.0,
"Point inside AABB should have negative SDF, got {d}"
);
}
#[test]
fn test_sdf_capsule_outside() {
let d = sdf_capsule(
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
0.5,
);
assert!((d - 1.5).abs() < 1e-10, "Expected 1.5, got {d}");
}
#[test]
fn test_sdf_capsule_inside() {
let d = sdf_capsule(
Vec3::new(0.2, 0.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
0.5,
);
assert!(
d < 0.0,
"Point inside capsule should have negative SDF, got {d}"
);
}
#[test]
fn test_sdf_plane_positive() {
let d = sdf_plane(Vec3::new(0.0, 2.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.0);
assert!((d - 2.0).abs() < 1e-10, "Expected 2.0, got {d}");
}
#[test]
fn test_sdf_plane_negative() {
let d = sdf_plane(Vec3::new(0.0, -1.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.0);
assert!((d - (-1.0)).abs() < 1e-10, "Expected -1.0, got {d}");
}
#[test]
fn test_sdf_cylinder_infinite_outside() {
let d = sdf_cylinder_infinite(
Vec3::new(2.0, 5.0, 0.0),
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
);
assert!((d - 1.0).abs() < 1e-10, "Expected 1.0, got {d}");
}
#[test]
fn test_point_in_convex_aabb_inside() {
let planes = aabb_face_planes(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
assert!(
point_in_convex(Vec3::zeros(), &planes),
"origin should be inside unit box"
);
}
#[test]
fn test_point_in_convex_aabb_outside() {
let planes = aabb_face_planes(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
assert!(
!point_in_convex(Vec3::new(2.0, 0.0, 0.0), &planes),
"point outside box should fail"
);
}
#[test]
fn test_point_in_convex_on_boundary() {
let planes = aabb_face_planes(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
assert!(
point_in_convex(Vec3::new(1.0, 0.0, 0.0), &planes),
"point on box face should be inside"
);
}
#[test]
fn test_closest_point_bary_centroid() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(3.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 3.0, 0.0);
let centroid = (v0 + v1 + v2) * (1.0 / 3.0);
let (pt, u, v, w) = closest_point_on_triangle_with_bary(v0, v1, v2, centroid);
assert!(
(pt - centroid).norm() < 1e-10,
"closest point should be centroid"
);
assert!(
(u + v + w - 1.0).abs() < 1e-10,
"bary coords should sum to 1"
);
assert!((u - 1.0 / 3.0).abs() < 1e-9, "u should be 1/3");
assert!((v - 1.0 / 3.0).abs() < 1e-9, "v should be 1/3");
assert!((w - 1.0 / 3.0).abs() < 1e-9, "w should be 1/3");
}
#[test]
fn test_closest_point_bary_vertex() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let (pt, u, v, w) =
closest_point_on_triangle_with_bary(v0, v1, v2, Vec3::new(-1.0, -1.0, 0.0));
assert!((pt - v0).norm() < 1e-10, "closest to corner should be v0");
assert!((u - 1.0).abs() < 1e-10, "u=1 at v0");
let _ = (v, w);
}
#[test]
fn test_triangle_area_unit() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(2.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 2.0, 0.0);
let area = triangle_area(v0, v1, v2);
assert!((area - 2.0).abs() < 1e-10, "Expected area 2.0, got {area}");
}
#[test]
fn test_triangle_normal_up() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 0.0, 1.0);
let n = triangle_normal(v0, v1, v2);
assert!((n.norm() - 1.0).abs() < 1e-10, "Normal must be unit length");
}
#[test]
fn test_project_onto_triangle_plane() {
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let p = Vec3::new(0.5, 0.5, 3.0);
let proj = project_onto_triangle_plane(v0, v1, v2, p);
assert!(
proj.z.abs() < 1e-10,
"Projected point should be on plane z=0, got {}",
proj.z
);
}
#[test]
fn test_contact_tangent_basis_orthonormal() {
let normals = [
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(1.0, 1.0, 0.0).normalize(),
];
for n in normals {
let (u, v) = contact_tangent_basis(n);
assert!(
(u.norm() - 1.0).abs() < 1e-10,
"tangent_u must be unit length"
);
assert!(
(v.norm() - 1.0).abs() < 1e-10,
"tangent_v must be unit length"
);
assert!(
n.dot(&u).abs() < 1e-9,
"tangent_u must be perpendicular to normal"
);
assert!(
n.dot(&v).abs() < 1e-9,
"tangent_v must be perpendicular to normal"
);
assert!(
u.dot(&v).abs() < 1e-9,
"tangent_u and tangent_v must be perpendicular"
);
}
}
#[test]
fn test_point_aabb_signed_distance_inside() {
let d = point_aabb_signed_distance(
Vec3::zeros(),
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
);
assert!(d < 0.0, "Inside AABB → negative SDF, got {d}");
}
#[test]
fn test_point_aabb_signed_distance_outside() {
let d = point_aabb_signed_distance(
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
);
assert!((d - 1.0).abs() < 1e-10, "Expected 1.0, got {d}");
}
#[test]
fn test_closest_on_cylinder_axis_midpoint() {
let (pt, t) = closest_point_on_cylinder_axis(
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(3.0, 0.0, 0.0),
);
assert!(
pt.y.abs() < 1e-10,
"closest on Y-axis should have y≈0, got {}",
pt.y
);
assert!((t - 0.5).abs() < 1e-10, "t should be 0.5, got {t}");
}
#[test]
fn test_closest_on_cylinder_axis_clamped() {
let (pt, t) = closest_point_on_cylinder_axis(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(5.0, 0.0, 0.0),
);
assert!(
(pt.x - 1.0).abs() < 1e-10,
"should be clamped to end, got {}",
pt.x
);
assert!((t - 1.0).abs() < 1e-10, "t should be 1.0, got {t}");
}
}