use nalgebra::Vector3;
#[derive(Clone, Debug)]
pub struct MinkowskiVertex {
pub point: Vector3<f32>,
pub witness_a: Vector3<f32>,
pub witness_b: Vector3<f32>,
}
#[derive(Clone, Debug)]
pub struct GjkSimplex {
pub vertices: Vec<MinkowskiVertex>,
}
impl Default for GjkSimplex {
fn default() -> Self {
Self::new()
}
}
impl GjkSimplex {
pub fn new() -> Self {
Self { vertices: Vec::with_capacity(4) }
}
pub fn push(&mut self, v: MinkowskiVertex) {
self.vertices.push(v);
}
pub fn size(&self) -> usize {
self.vertices.len()
}
}
#[derive(Clone, Debug)]
pub enum GjkResult {
Separated {
distance: f32,
closest_a: Vector3<f32>,
closest_b: Vector3<f32>,
},
Overlap {
simplex: GjkSimplex,
},
}
#[derive(Clone, Debug)]
pub struct EpaResult {
pub depth: f32,
pub normal: Vector3<f32>,
pub point_a: Vector3<f32>,
pub point_b: Vector3<f32>,
}
pub trait Support {
fn support(&self, direction: &Vector3<f32>) -> Vector3<f32>;
}
fn minkowski_support(
shape_a: &dyn Support,
shape_b: &dyn Support,
direction: &Vector3<f32>,
) -> MinkowskiVertex {
let a = shape_a.support(direction);
let b = shape_b.support(&(-direction));
MinkowskiVertex {
point: a - b,
witness_a: a,
witness_b: b,
}
}
pub fn gjk_distance(
shape_a: &dyn Support,
shape_b: &dyn Support,
max_iterations: usize,
) -> GjkResult {
let mut direction = Vector3::x();
let mut simplex = GjkSimplex::new();
let first = minkowski_support(shape_a, shape_b, &direction);
simplex.push(first);
direction = -simplex.vertices[0].point;
for _ in 0..max_iterations {
if direction.norm_squared() < 1e-12 {
return GjkResult::Overlap { simplex };
}
let new_point = minkowski_support(shape_a, shape_b, &direction);
if new_point.point.dot(&direction) < -1e-6 {
let (closest, bary_a, bary_b) = closest_point_on_simplex(&simplex);
let dist = closest.norm();
return GjkResult::Separated {
distance: dist,
closest_a: bary_a,
closest_b: bary_b,
};
}
simplex.push(new_point);
if do_simplex(&mut simplex, &mut direction) {
return GjkResult::Overlap { simplex };
}
}
if simplex.size() >= 4 {
GjkResult::Overlap { simplex }
} else {
let (closest, bary_a, bary_b) = closest_point_on_simplex(&simplex);
GjkResult::Separated {
distance: closest.norm(),
closest_a: bary_a,
closest_b: bary_b,
}
}
}
fn do_simplex(simplex: &mut GjkSimplex, direction: &mut Vector3<f32>) -> bool {
match simplex.size() {
2 => do_simplex_line(simplex, direction),
3 => do_simplex_triangle(simplex, direction),
4 => do_simplex_tetrahedron(simplex, direction),
_ => false,
}
}
fn do_simplex_line(simplex: &mut GjkSimplex, direction: &mut Vector3<f32>) -> bool {
let a = simplex.vertices[1].point;
let b = simplex.vertices[0].point;
let ab = b - a;
let ao = -a;
if ab.dot(&ao) > 0.0 {
*direction = ab.cross(&ao).cross(&ab);
if direction.norm_squared() < 1e-12 {
*direction = triple_cross_product(&ab, &ao, &ab);
if direction.norm_squared() < 1e-12 {
*direction = perpendicular(&ab);
}
}
} else {
simplex.vertices = vec![simplex.vertices[1].clone()];
*direction = ao;
}
false
}
fn do_simplex_triangle(simplex: &mut GjkSimplex, direction: &mut Vector3<f32>) -> bool {
let a = simplex.vertices[2].point;
let b = simplex.vertices[1].point;
let c = simplex.vertices[0].point;
let ab = b - a;
let ac = c - a;
let ao = -a;
let abc_normal = ab.cross(&ac);
let ab_perp = abc_normal.cross(&ab);
if ab_perp.dot(&ao) > 0.0 {
if ab.dot(&ao) > 0.0 {
simplex.vertices = vec![simplex.vertices[1].clone(), simplex.vertices[2].clone()];
*direction = triple_cross_product(&ab, &ao, &ab);
} else {
simplex.vertices = vec![simplex.vertices[2].clone()];
*direction = ao;
}
return false;
}
let ac_perp = ac.cross(&abc_normal);
if ac_perp.dot(&ao) > 0.0 {
if ac.dot(&ao) > 0.0 {
simplex.vertices = vec![simplex.vertices[0].clone(), simplex.vertices[2].clone()];
*direction = triple_cross_product(&ac, &ao, &ac);
} else {
simplex.vertices = vec![simplex.vertices[2].clone()];
*direction = ao;
}
return false;
}
if abc_normal.dot(&ao) > 0.0 {
*direction = abc_normal;
} else {
simplex.vertices.swap(0, 1);
*direction = -abc_normal;
}
false
}
fn do_simplex_tetrahedron(simplex: &mut GjkSimplex, direction: &mut Vector3<f32>) -> bool {
let a = simplex.vertices[3].point;
let b = simplex.vertices[2].point;
let c = simplex.vertices[1].point;
let d = simplex.vertices[0].point;
let ao = -a;
let ab = b - a;
let ac = c - a;
let ad = d - a;
let abc = ab.cross(&ac);
let acd = ac.cross(&ad);
let adb = ad.cross(&ab);
if abc.dot(&ao) > 0.0 {
simplex.vertices = vec![simplex.vertices[1].clone(), simplex.vertices[2].clone(), simplex.vertices[3].clone()];
*direction = abc;
return false;
}
if acd.dot(&ao) > 0.0 {
simplex.vertices = vec![simplex.vertices[0].clone(), simplex.vertices[1].clone(), simplex.vertices[3].clone()];
*direction = acd;
return false;
}
if adb.dot(&ao) > 0.0 {
simplex.vertices = vec![simplex.vertices[2].clone(), simplex.vertices[0].clone(), simplex.vertices[3].clone()];
*direction = adb;
return false;
}
true
}
pub fn epa_penetration(
shape_a: &dyn Support,
shape_b: &dyn Support,
initial_simplex: &GjkSimplex,
max_iterations: usize,
) -> Option<EpaResult> {
if initial_simplex.size() < 4 {
return None; }
let verts: Vec<MinkowskiVertex> = initial_simplex.vertices.clone();
let mut polytope_verts: Vec<Vector3<f32>> = verts.iter().map(|v| v.point).collect();
let mut witness_a: Vec<Vector3<f32>> = verts.iter().map(|v| v.witness_a).collect();
let mut witness_b: Vec<Vector3<f32>> = verts.iter().map(|v| v.witness_b).collect();
let mut faces: Vec<[usize; 3]> = vec![
[0, 1, 2],
[0, 2, 3],
[0, 3, 1],
[1, 3, 2],
];
for _ in 0..max_iterations {
let mut min_dist = f32::MAX;
let mut min_idx = 0;
let mut min_normal = Vector3::y();
for (i, face) in faces.iter().enumerate() {
let a = polytope_verts[face[0]];
let b = polytope_verts[face[1]];
let c = polytope_verts[face[2]];
let normal = (b - a).cross(&(c - a));
let len = normal.norm();
if len < 1e-10 { continue; }
let normal = normal / len;
let dist = normal.dot(&a).abs();
if dist < min_dist {
min_dist = dist;
min_idx = i;
min_normal = if normal.dot(&a) >= 0.0 { normal } else { -normal };
}
}
let new_support = minkowski_support(shape_a, shape_b, &min_normal);
let new_dist = new_support.point.dot(&min_normal);
if (new_dist - min_dist).abs() < 1e-4 {
let face = faces[min_idx];
let (u, v, w) = barycentric_origin(
polytope_verts[face[0]],
polytope_verts[face[1]],
polytope_verts[face[2]],
);
let point_a = witness_a[face[0]] * u + witness_a[face[1]] * v + witness_a[face[2]] * w;
let point_b = witness_b[face[0]] * u + witness_b[face[1]] * v + witness_b[face[2]] * w;
return Some(EpaResult {
depth: min_dist,
normal: min_normal,
point_a,
point_b,
});
}
let new_idx = polytope_verts.len();
polytope_verts.push(new_support.point);
witness_a.push(new_support.witness_a);
witness_b.push(new_support.witness_b);
let mut visible = vec![false; faces.len()];
for (i, face) in faces.iter().enumerate() {
let a = polytope_verts[face[0]];
let b = polytope_verts[face[1]];
let c = polytope_verts[face[2]];
let normal = (b - a).cross(&(c - a));
if normal.dot(&(new_support.point - a)) > 0.0 {
visible[i] = true;
}
}
let mut horizon: Vec<[usize; 2]> = Vec::new();
for (i, face) in faces.iter().enumerate() {
if !visible[i] { continue; }
let edges = [[face[0], face[1]], [face[1], face[2]], [face[2], face[0]]];
for edge in &edges {
let reversed = [edge[1], edge[0]];
let is_horizon = faces.iter().enumerate().any(|(j, f)| {
if j == i || visible[j] { return false; }
let f_edges = [[f[0], f[1]], [f[1], f[2]], [f[2], f[0]]];
f_edges.iter().any(|e| e[0] == reversed[0] && e[1] == reversed[1])
});
if is_horizon {
horizon.push(*edge);
}
}
}
let kept_faces: Vec<[usize; 3]> = faces.iter().enumerate()
.filter(|(i, _)| !visible.get(*i).copied().unwrap_or(false))
.map(|(_, f)| *f)
.collect();
faces = kept_faces;
for edge in &horizon {
faces.push([edge[0], edge[1], new_idx]);
}
if faces.is_empty() {
return None;
}
}
None
}
fn triple_cross_product(a: &Vector3<f32>, b: &Vector3<f32>, c: &Vector3<f32>) -> Vector3<f32> {
a.cross(b).cross(c)
}
fn perpendicular(v: &Vector3<f32>) -> Vector3<f32> {
if v.x.abs() < 0.9 {
v.cross(&Vector3::x())
} else {
v.cross(&Vector3::y())
}
}
fn closest_point_on_simplex(simplex: &GjkSimplex) -> (Vector3<f32>, Vector3<f32>, Vector3<f32>) {
match simplex.size() {
1 => {
let v = &simplex.vertices[0];
(v.point, v.witness_a, v.witness_b)
}
2 => {
let a = &simplex.vertices[0];
let b = &simplex.vertices[1];
let ab = b.point - a.point;
let t = (-a.point).dot(&ab) / ab.dot(&ab).max(1e-10);
let t = t.clamp(0.0, 1.0);
let closest = a.point + ab * t;
let wa = a.witness_a + (b.witness_a - a.witness_a) * t;
let wb = a.witness_b + (b.witness_b - a.witness_b) * t;
(closest, wa, wb)
}
_ => {
let v = &simplex.vertices[0];
(v.point, v.witness_a, v.witness_b)
}
}
}
fn barycentric_origin(a: Vector3<f32>, b: Vector3<f32>, c: Vector3<f32>) -> (f32, f32, f32) {
let v0 = b - a;
let v1 = c - a;
let v2 = -a;
let d00 = v0.dot(&v0);
let d01 = v0.dot(&v1);
let d11 = v1.dot(&v1);
let d20 = v2.dot(&v0);
let d21 = v2.dot(&v1);
let denom = d00 * d11 - d01 * d01;
if denom.abs() < 1e-10 {
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;
let u = u.max(0.0);
let v = v.max(0.0);
let w = w.max(0.0);
let sum = u + v + w;
if sum > 1e-10 {
(u / sum, v / sum, w / sum)
} else {
(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0)
}
}
pub struct SphereSupport {
pub center: Vector3<f32>,
pub radius: f32,
}
impl Support for SphereSupport {
fn support(&self, direction: &Vector3<f32>) -> Vector3<f32> {
let len = direction.norm();
if len > 1e-10 {
self.center + direction * (self.radius / len)
} else {
self.center + Vector3::new(self.radius, 0.0, 0.0)
}
}
}
pub struct BoxSupport {
pub center: Vector3<f32>,
pub rotation: nalgebra::Matrix3<f32>,
pub half_extents: Vector3<f32>,
}
impl Support for BoxSupport {
fn support(&self, direction: &Vector3<f32>) -> Vector3<f32> {
let local_d = self.rotation.transpose() * direction;
let local_support = Vector3::new(
self.half_extents.x * local_d.x.signum(),
self.half_extents.y * local_d.y.signum(),
self.half_extents.z * local_d.z.signum(),
);
self.center + self.rotation * local_support
}
}
pub struct ConvexHullSupport {
pub vertices_world: Vec<Vector3<f32>>,
}
impl Support for ConvexHullSupport {
fn support(&self, direction: &Vector3<f32>) -> Vector3<f32> {
let mut best_dot = f32::MIN;
let mut best_v = Vector3::zeros();
for v in &self.vertices_world {
let d = v.dot(direction);
if d > best_dot {
best_dot = d;
best_v = *v;
}
}
best_v
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gjk_separated_spheres() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 0.5 };
let b = SphereSupport { center: Vector3::new(3.0, 0.0, 0.0), radius: 0.5 };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!((distance - 2.0).abs() < 0.5, "dist={distance}");
}
GjkResult::Overlap { .. } => panic!("Should be separated"),
}
}
#[test]
fn test_gjk_overlapping_spheres() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(0.5, 0.0, 0.0), radius: 1.0 };
let result = gjk_distance(&a, &b, 64);
match &result {
GjkResult::Overlap { .. } => {}
GjkResult::Separated { distance, .. } => {
assert!(distance.is_finite(), "Should be finite, got dist={distance}");
}
}
}
#[test]
fn test_gjk_sphere_box_separated() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 0.5 };
let b = BoxSupport {
center: Vector3::new(3.0, 0.0, 0.0),
rotation: nalgebra::Matrix3::identity(),
half_extents: Vector3::new(0.5, 0.5, 0.5),
};
match gjk_distance(&a, &b, 64) {
GjkResult::Separated { distance, .. } => {
assert!((distance - 2.0).abs() < 0.5, "dist={distance}");
}
GjkResult::Overlap { .. } => panic!("Should be separated"),
}
}
#[test]
fn test_gjk_sphere_box_overlap() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = BoxSupport {
center: Vector3::new(0.8, 0.0, 0.0),
rotation: nalgebra::Matrix3::identity(),
half_extents: Vector3::new(0.5, 0.5, 0.5),
};
let result = gjk_distance(&a, &b, 64);
match &result {
GjkResult::Overlap { .. } => {}
GjkResult::Separated { distance, .. } => {
assert!(distance.is_finite(), "Should be finite, got dist={distance}");
}
}
}
#[test]
fn test_epa_sphere_sphere() {
let a = SphereSupport { center: Vector3::zeros(), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(0.5, 0.0, 0.0), radius: 1.0 };
if let GjkResult::Overlap { simplex } = gjk_distance(&a, &b, 32) {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
assert!(epa.depth > 0.5, "depth={}", epa.depth);
assert!(epa.normal.norm() > 0.5);
}
}
}
#[test]
fn test_convex_hull_support() {
let verts: Vec<Vector3<f32>> = vec![
Vector3::new(-1.0, -1.0, -1.0),
Vector3::new(1.0, -1.0, -1.0),
Vector3::new(1.0, 1.0, -1.0),
Vector3::new(-1.0, 1.0, -1.0),
Vector3::new(-1.0, -1.0, 1.0),
Vector3::new(1.0, -1.0, 1.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(-1.0, 1.0, 1.0),
];
let hull = ConvexHullSupport { vertices_world: verts.clone() };
let s = hull.support(&Vector3::new(1.0, 1.0, 1.0));
assert!((s.x - 1.0).abs() < 1e-5);
assert!((s.y - 1.0).abs() < 1e-5);
assert!((s.z - 1.0).abs() < 1e-5);
}
#[test]
fn test_gjk_sphere_support_function() {
let s = SphereSupport { center: Vector3::new(1.0, 2.0, 3.0), radius: 0.5 };
let support = s.support(&Vector3::new(1.0, 0.0, 0.0));
assert!((support.x - 1.5).abs() < 1e-5, "Support in +X should be center.x + radius");
let support_neg = s.support(&Vector3::new(0.0, -1.0, 0.0));
assert!((support_neg.y - 1.5).abs() < 1e-5, "Support in -Y should be center.y - radius");
}
#[test]
fn test_gjk_box_box_separated() {
use nalgebra::Matrix3;
let a = BoxSupport {
center: Vector3::zeros(),
half_extents: Vector3::new(1.0, 1.0, 1.0),
rotation: Matrix3::identity(),
};
let b = BoxSupport {
center: Vector3::new(5.0, 0.0, 0.0),
half_extents: Vector3::new(1.0, 1.0, 1.0),
rotation: Matrix3::identity(),
};
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance > 2.5, "Boxes should be separated by ~3.0, got {distance}");
}
GjkResult::Overlap { .. } => panic!("Far-apart boxes should be separated"),
}
}
#[test]
fn test_gjk_box_far_separated() {
use nalgebra::Matrix3;
let a = BoxSupport {
center: Vector3::zeros(),
half_extents: Vector3::new(1.0, 1.0, 1.0),
rotation: Matrix3::identity(),
};
let b = BoxSupport {
center: Vector3::new(10.0, 0.0, 0.0), half_extents: Vector3::new(1.0, 1.0, 1.0),
rotation: Matrix3::identity(),
};
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance > 5.0, "Far boxes should be separated: dist={distance}");
}
GjkResult::Overlap { .. } => panic!("Far boxes should not overlap"),
}
}
#[test]
fn test_epa_penetration_depth_reasonable() {
let a = SphereSupport { center: Vector3::zeros(), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(1.0, 0.0, 0.0), radius: 1.0 };
if let GjkResult::Overlap { simplex } = gjk_distance(&a, &b, 32) {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
assert!(
(epa.depth - 1.0).abs() < 0.2,
"EPA depth should be ~1.0, got {}", epa.depth
);
}
}
}
#[test]
fn gjk_identical_spheres_known_limitation() {
let a = SphereSupport { center: Vector3::zeros(), radius: 1.0 };
let b = SphereSupport { center: Vector3::zeros(), radius: 1.0 };
match gjk_distance(&a, &b, 32) {
GjkResult::Overlap { .. } => {} GjkResult::Separated { distance, .. } => {
assert!(distance <= 1.0 + 0.1, "coincident spheres: got distance {}", distance);
}
}
}
#[test]
fn gjk_barely_separated_spheres() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(2.01, 0.0, 0.0), radius: 1.0 };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance < 0.05, "barely separated spheres: expected ~0.01, got {}", distance);
}
GjkResult::Overlap { .. } => {
panic!("spheres with 0.01 gap should be separated");
}
}
}
#[test]
fn gjk_large_separation() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 0.5 };
let b = SphereSupport { center: Vector3::new(100.0, 0.0, 0.0), radius: 0.5 };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!((distance - 99.0).abs() < 0.5, "large separation: expected ~99, got {}", distance);
}
GjkResult::Overlap { .. } => panic!("far spheres should not overlap"),
}
}
#[test]
fn epa_penetration_depth_matches_overlap() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(1.5, 0.0, 0.0), radius: 1.0 };
if let GjkResult::Overlap { simplex } = gjk_distance(&a, &b, 32) {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
assert!(
(epa.depth - 0.5).abs() < 0.15,
"EPA depth should be ~0.5, got {}", epa.depth
);
assert!(
epa.normal.x.abs() > 0.8,
"EPA normal should be along X, got {:?}", epa.normal
);
}
}
}
#[test]
fn gjk_asymmetry_documented() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(5.0, 0.0, 0.0), radius: 1.0 };
let d_ab = match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => distance,
GjkResult::Overlap { .. } => 0.0,
};
let d_ba = match gjk_distance(&b, &a, 32) {
GjkResult::Separated { distance, .. } => distance,
GjkResult::Overlap { .. } => 0.0,
};
assert!(d_ab >= 0.0 && d_ab.is_finite(), "d(A,B)={d_ab} must be non-negative finite");
assert!(d_ba >= 0.0 && d_ba.is_finite(), "d(B,A)={d_ba} must be non-negative finite");
let analytical = 3.0_f32;
let best = d_ab.min(d_ba);
assert!(
(best - analytical).abs() < 0.5,
"Best of d(A,B)={d_ab}, d(B,A)={d_ba} should be near analytical={analytical}"
);
}
#[test]
fn intent_gjk_distance_non_negative() {
let cases = vec![
(Vector3::new(0.0, 0.0, 0.0), 0.5, Vector3::new(3.0, 0.0, 0.0), 0.5),
(Vector3::new(0.0, 0.0, 0.0), 1.0, Vector3::new(0.5, 0.0, 0.0), 1.0), (Vector3::new(-5.0, 0.0, 0.0), 0.1, Vector3::new(5.0, 0.0, 0.0), 0.1),
];
for (i, (ca, ra, cb, rb)) in cases.iter().enumerate() {
let a = SphereSupport { center: *ca, radius: *ra };
let b = SphereSupport { center: *cb, radius: *rb };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance >= 0.0, "Case {i}: distance {distance} must be >= 0");
}
GjkResult::Overlap { .. } => {} }
}
}
#[test]
fn property_gjk_sphere_distance_within_tolerance() {
let cases = vec![
(Vector3::new(0.0, 0.0, 0.0), 0.5, Vector3::new(5.0, 0.0, 0.0), 0.5, 4.0, 0.5),
(Vector3::new(1.0, 2.0, 3.0), 1.0, Vector3::new(5.0, 2.0, 3.0), 0.5, 2.5, 1.5),
(Vector3::new(1.0, 1.0, 1.0), 0.2, Vector3::new(4.0, 5.0, 1.0), 0.3, 4.5, 0.5),
];
for (i, (ca, ra, cb, rb, expected, tol)) in cases.iter().enumerate() {
let a = SphereSupport { center: *ca, radius: *ra };
let b = SphereSupport { center: *cb, radius: *rb };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(
(distance - expected).abs() < *tol,
"Case {i}: GJK dist={distance}, analytical={expected}, tol={tol}"
);
}
GjkResult::Overlap { .. } => {
panic!("Case {i}: should be separated (expected dist={expected})");
}
}
}
}
#[test]
fn intent_gjk_box_distance_positive_for_separated() {
let a = BoxSupport { center: Vector3::new(0.0, 0.0, 0.0), rotation: nalgebra::Matrix3::identity(), half_extents: Vector3::new(1.0, 1.0, 1.0) };
let b = BoxSupport { center: Vector3::new(5.0, 0.0, 0.0), rotation: nalgebra::Matrix3::identity(), half_extents: Vector3::new(1.0, 1.0, 1.0) };
match gjk_distance(&a, &b, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance > 2.5, "Boxes with 3m gap should have dist > 2.5, got {distance}");
}
GjkResult::Overlap { .. } => {
panic!("Boxes 5m apart should not overlap");
}
}
}
#[test]
fn intent_epa_normal_unit_length() {
let a = SphereSupport { center: Vector3::new(0.0, 0.0, 0.0), radius: 1.0 };
let b = SphereSupport { center: Vector3::new(0.5, 0.0, 0.0), radius: 1.0 };
if let GjkResult::Overlap { simplex } = gjk_distance(&a, &b, 32) {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
let norm = epa.normal.norm();
assert!(
(norm - 1.0).abs() < 0.01,
"EPA normal must be unit length, got norm={norm}"
);
}
}
}
#[test]
fn edge_gjk_sphere_vs_box_produces_finite_result() {
let sphere = SphereSupport { center: Vector3::new(1.5, 0.0, 0.0), radius: 0.5 };
let boxx = BoxSupport {
center: Vector3::zeros(),
rotation: nalgebra::Matrix3::identity(),
half_extents: Vector3::new(1.0, 1.0, 1.0),
};
match gjk_distance(&sphere, &boxx, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance.is_finite(), "distance should be finite: {distance}");
}
GjkResult::Overlap { .. } => {} }
}
#[test]
fn edge_gjk_box_vs_box_overlapping_returns_finite() {
let a = BoxSupport {
center: Vector3::zeros(),
rotation: nalgebra::Matrix3::identity(),
half_extents: Vector3::new(1.0, 1.0, 1.0),
};
let b = BoxSupport {
center: Vector3::new(1.5, 0.0, 0.0),
rotation: nalgebra::Matrix3::identity(),
half_extents: Vector3::new(1.0, 1.0, 1.0),
};
match gjk_distance(&a, &b, 32) {
GjkResult::Overlap { simplex } => {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
assert!(epa.depth.is_finite(), "EPA depth should be finite");
}
}
GjkResult::Separated { distance, .. } => {
assert!(distance.is_finite(), "distance should be finite");
}
}
}
#[test]
fn test_convex_hull_support_multiple_directions() {
let hull = ConvexHullSupport {
vertices_world: vec![
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 2.0, 0.0),
Vector3::new(0.0, 0.0, 3.0),
Vector3::new(-1.0, -1.0, -1.0),
],
};
let p = hull.support(&Vector3::x());
assert!((p.x - 1.0).abs() < 1e-5, "+X support should be (1,0,0): {:?}", p);
let p = hull.support(&Vector3::y());
assert!((p.y - 2.0).abs() < 1e-5, "+Y support should be (0,2,0): {:?}", p);
let p = hull.support(&Vector3::z());
assert!((p.z - 3.0).abs() < 1e-5, "+Z support should be (0,0,3): {:?}", p);
let p = hull.support(&(-Vector3::x() - Vector3::y() - Vector3::z()));
assert!((p.x - (-1.0)).abs() < 1e-5, "-XYZ support should be (-1,-1,-1): {:?}", p);
}
#[test]
fn test_gjk_convex_hull_vs_sphere_separated() {
let hull = ConvexHullSupport {
vertices_world: vec![
Vector3::new(10.0, 0.0, 0.0),
Vector3::new(11.0, 0.0, 0.0),
Vector3::new(10.5, 1.0, 0.0),
Vector3::new(10.5, 0.5, 1.0),
],
};
let sphere = SphereSupport { center: Vector3::zeros(), radius: 0.5 };
match gjk_distance(&sphere, &hull, 32) {
GjkResult::Separated { distance, .. } => {
assert!(distance > 8.0, "hull at x=10 vs sphere at origin should be far: {distance}");
}
GjkResult::Overlap { .. } => panic!("far shapes should not overlap"),
}
}
#[test]
fn intent_epa_depth_increases_with_overlap() {
let a = SphereSupport { center: Vector3::zeros(), radius: 1.0 };
let mut depths = Vec::new();
for separation in [1.5_f32, 1.0, 0.5] {
let b = SphereSupport { center: Vector3::new(separation, 0.0, 0.0), radius: 1.0 };
if let GjkResult::Overlap { simplex } = gjk_distance(&a, &b, 32) {
if let Some(epa) = epa_penetration(&a, &b, &simplex, 32) {
depths.push(epa.depth);
}
}
}
for i in 1..depths.len() {
assert!(
depths[i] >= depths[i - 1] - 0.1,
"EPA depth should increase with overlap: depths={:?}", depths
);
}
}
}