#![allow(dead_code)]
#[inline]
pub fn v3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub fn v3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub fn v3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
pub fn v3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub fn v3_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],
]
}
#[inline]
pub fn v3_len_sq(a: [f64; 3]) -> f64 {
v3_dot(a, a)
}
#[inline]
pub fn v3_len(a: [f64; 3]) -> f64 {
v3_len_sq(a).sqrt()
}
#[inline]
pub fn v3_normalize(a: [f64; 3]) -> [f64; 3] {
let l = v3_len(a);
if l < f64::EPSILON {
[1.0, 0.0, 0.0]
} else {
[a[0] / l, a[1] / l, a[2] / l]
}
}
#[inline]
pub fn v3_neg(a: [f64; 3]) -> [f64; 3] {
[-a[0], -a[1], -a[2]]
}
#[inline]
pub fn v3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
v3_add(a, v3_scale(v3_sub(b, a), t))
}
#[inline]
pub fn v3_dist(a: [f64; 3], b: [f64; 3]) -> f64 {
v3_len(v3_sub(a, b))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContactPoint {
pub position: [f64; 3],
pub local_a: [f64; 3],
pub local_b: [f64; 3],
pub depth: f64,
pub normal_impulse: f64,
pub tangent_impulse0: f64,
pub tangent_impulse1: f64,
pub age: u32,
}
impl ContactPoint {
pub fn new(position: [f64; 3], local_a: [f64; 3], local_b: [f64; 3], depth: f64) -> Self {
Self {
position,
local_a,
local_b,
depth,
normal_impulse: 0.0,
tangent_impulse0: 0.0,
tangent_impulse1: 0.0,
age: 0,
}
}
pub fn is_penetrating(&self, tolerance: f64) -> bool {
self.depth > tolerance
}
}
#[derive(Debug, Clone, Copy)]
pub struct ContactBasis {
pub normal: [f64; 3],
pub tangent0: [f64; 3],
pub tangent1: [f64; 3],
}
impl ContactBasis {
pub fn from_normal(n: [f64; 3]) -> Self {
let normal = v3_normalize(n);
let tangent0 = compute_tangent(normal);
let tangent1 = v3_cross(normal, tangent0);
Self {
normal,
tangent0,
tangent1,
}
}
pub fn is_orthonormal(&self, tol: f64) -> bool {
let d01 = v3_dot(self.normal, self.tangent0).abs();
let d02 = v3_dot(self.normal, self.tangent1).abs();
let d12 = v3_dot(self.tangent0, self.tangent1).abs();
let l0 = (v3_len(self.normal) - 1.0).abs();
let l1 = (v3_len(self.tangent0) - 1.0).abs();
let l2 = (v3_len(self.tangent1) - 1.0).abs();
d01 < tol && d02 < tol && d12 < tol && l0 < tol && l1 < tol && l2 < tol
}
}
pub fn compute_tangent(n: [f64; 3]) -> [f64; 3] {
let abs_x = n[0].abs();
let abs_y = n[1].abs();
let abs_z = n[2].abs();
let perp = if abs_x <= abs_y && abs_x <= abs_z {
[1.0_f64, 0.0, 0.0]
} else if abs_y <= abs_z {
[0.0_f64, 1.0, 0.0]
} else {
[0.0_f64, 0.0, 1.0]
};
let t = v3_cross(n, perp);
v3_normalize(t)
}
pub const MAX_MANIFOLD_POINTS: usize = 4;
#[derive(Debug, Clone)]
pub struct ContactManifold {
pub body_a: usize,
pub body_b: usize,
pub basis: ContactBasis,
pub points: [Option<ContactPoint>; MAX_MANIFOLD_POINTS],
pub point_count: usize,
pub last_update: u32,
}
impl ContactManifold {
pub fn new(body_a: usize, body_b: usize, normal: [f64; 3]) -> Self {
Self {
body_a,
body_b,
basis: ContactBasis::from_normal(normal),
points: [None; MAX_MANIFOLD_POINTS],
point_count: 0,
last_update: 0,
}
}
pub fn active_points(&self) -> impl Iterator<Item = &ContactPoint> {
self.points[..self.point_count]
.iter()
.filter_map(|p| p.as_ref())
}
pub fn len(&self) -> usize {
self.point_count
}
pub fn is_empty(&self) -> bool {
self.point_count == 0
}
pub fn add_point(&mut self, cp: ContactPoint) {
if self.point_count < MAX_MANIFOLD_POINTS {
self.points[self.point_count] = Some(cp);
self.point_count += 1;
} else {
let mut pts: Vec<ContactPoint> = self.points.iter().filter_map(|p| *p).collect();
pts.push(cp);
let reduced = reduce_to_4(&pts);
for (i, rp) in reduced.iter().enumerate() {
self.points[i] = Some(*rp);
}
self.point_count = reduced.len();
}
}
pub fn clear(&mut self) {
self.points = [None; MAX_MANIFOLD_POINTS];
self.point_count = 0;
}
pub fn update_normal(&mut self, normal: [f64; 3]) {
self.basis = ContactBasis::from_normal(normal);
}
pub fn average_position(&self) -> [f64; 3] {
let count = self.point_count;
if count == 0 {
return [0.0; 3];
}
let sum = self
.active_points()
.fold([0.0_f64; 3], |acc, p| v3_add(acc, p.position));
v3_scale(sum, 1.0 / count as f64)
}
pub fn deepest_point(&self) -> Option<&ContactPoint> {
self.active_points().max_by(|a, b| {
a.depth
.partial_cmp(&b.depth)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn age_points(&mut self) {
for p in self.points[..self.point_count].iter_mut().flatten() {
p.age += 1;
}
}
pub fn prune_old_points(&mut self, max_age: u32) {
let mut new_count = 0;
let mut new_points: [Option<ContactPoint>; MAX_MANIFOLD_POINTS] =
[None; MAX_MANIFOLD_POINTS];
for slot in &self.points[..self.point_count] {
if let Some(p) = slot
&& p.age <= max_age
{
new_points[new_count] = Some(*p);
new_count += 1;
}
}
self.points = new_points;
self.point_count = new_count;
}
pub fn signed_depth_at(&self, pos: [f64; 3]) -> f64 {
v3_dot(v3_sub(pos, self.average_position()), self.basis.normal)
}
}
pub fn reduce_to_4(points: &[ContactPoint]) -> Vec<ContactPoint> {
if points.len() <= MAX_MANIFOLD_POINTS {
return points.to_vec();
}
let mut result: Vec<ContactPoint> = Vec::with_capacity(MAX_MANIFOLD_POINTS);
let Some(&p0) = points.iter().max_by(|a, b| {
a.depth
.partial_cmp(&b.depth)
.unwrap_or(std::cmp::Ordering::Equal)
}) else {
return result;
};
result.push(p0);
let Some(&p1) = points.iter().max_by(|a, b| {
v3_dist(a.position, p0.position)
.partial_cmp(&v3_dist(b.position, p0.position))
.unwrap_or(std::cmp::Ordering::Equal)
}) else {
return result;
};
result.push(p1);
if let Some(p2) = points.iter().max_by(|a, b| {
triangle_area_sq(p0.position, p1.position, a.position)
.partial_cmp(&triangle_area_sq(p0.position, p1.position, b.position))
.unwrap_or(std::cmp::Ordering::Equal)
}) {
result.push(*p2);
if let Some(p3) = points.iter().max_by(|a, b| {
let area_a = triangle_area_sq(p0.position, p1.position, a.position)
+ triangle_area_sq(p1.position, p2.position, a.position);
let area_b = triangle_area_sq(p0.position, p1.position, b.position)
+ triangle_area_sq(p1.position, p2.position, b.position);
area_a
.partial_cmp(&area_b)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
result.push(*p3);
}
}
result
}
pub fn triangle_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
let ab = v3_sub(b, a);
let ac = v3_sub(c, a);
let cross = v3_cross(ab, ac);
v3_len_sq(cross) * 0.25
}
pub fn triangle_area(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
triangle_area_sq(a, b, c).sqrt()
}
pub fn manifold_sphere_sphere(
body_a: usize,
body_b: usize,
centre_a: [f64; 3],
ra: f64,
centre_b: [f64; 3],
rb: f64,
) -> Option<ContactManifold> {
let diff = v3_sub(centre_b, centre_a);
let dist_sq = v3_len_sq(diff);
let sum_r = ra + rb;
if dist_sq >= sum_r * sum_r {
return None;
}
let dist = dist_sq.sqrt();
let normal = if dist < f64::EPSILON {
[0.0, 1.0, 0.0]
} else {
v3_normalize(diff)
};
let depth = sum_r - dist;
let contact_pos = v3_add(centre_a, v3_scale(normal, ra - depth * 0.5));
let local_a = v3_scale(normal, ra);
let local_b = v3_scale(v3_neg(normal), rb);
let mut manifold = ContactManifold::new(body_a, body_b, normal);
manifold.add_point(ContactPoint::new(contact_pos, local_a, local_b, depth));
Some(manifold)
}
pub fn manifold_sphere_aabb(
body_sphere: usize,
body_box: usize,
sphere_centre: [f64; 3],
radius: f64,
box_centre: [f64; 3],
half_extents: [f64; 3],
) -> Option<ContactManifold> {
let closest = [
sphere_centre[0].clamp(
box_centre[0] - half_extents[0],
box_centre[0] + half_extents[0],
),
sphere_centre[1].clamp(
box_centre[1] - half_extents[1],
box_centre[1] + half_extents[1],
),
sphere_centre[2].clamp(
box_centre[2] - half_extents[2],
box_centre[2] + half_extents[2],
),
];
let diff = v3_sub(sphere_centre, closest);
let dist_sq = v3_len_sq(diff);
if dist_sq >= radius * radius {
return None;
}
let dist = dist_sq.sqrt();
let normal = if dist < f64::EPSILON {
penetration_axis_aabb(sphere_centre, box_centre, half_extents)
} else {
v3_normalize(diff)
};
let depth = radius - dist;
let contact_pos = closest;
let local_a = v3_scale(v3_neg(normal), radius);
let local_b = v3_sub(closest, box_centre);
let mut manifold = ContactManifold::new(body_sphere, body_box, normal);
manifold.add_point(ContactPoint::new(contact_pos, local_a, local_b, depth));
Some(manifold)
}
fn penetration_axis_aabb(
point: [f64; 3],
box_centre: [f64; 3],
half_extents: [f64; 3],
) -> [f64; 3] {
let local = v3_sub(point, box_centre);
let overlaps = [
half_extents[0] - local[0].abs(),
half_extents[1] - local[1].abs(),
half_extents[2] - local[2].abs(),
];
let min_axis = overlaps
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let mut axis = [0.0_f64; 3];
axis[min_axis] = if local[min_axis] >= 0.0 { 1.0 } else { -1.0 };
axis
}
pub fn closest_point_on_segment(p: [f64; 3], q: [f64; 3], t: [f64; 3]) -> [f64; 3] {
let pq = v3_sub(q, p);
let len_sq = v3_len_sq(pq);
if len_sq < f64::EPSILON {
return p;
}
let s = v3_dot(v3_sub(t, p), pq) / len_sq;
let s_clamped = s.clamp(0.0, 1.0);
v3_add(p, v3_scale(pq, s_clamped))
}
pub fn manifold_capsule_sphere(
body_cap: usize,
body_sphere: usize,
cap_a: [f64; 3],
cap_b: [f64; 3],
cap_r: f64,
sphere_centre: [f64; 3],
sphere_r: f64,
) -> Option<ContactManifold> {
let closest = closest_point_on_segment(cap_a, cap_b, sphere_centre);
let diff = v3_sub(sphere_centre, closest);
let dist_sq = v3_len_sq(diff);
let sum_r = cap_r + sphere_r;
if dist_sq >= sum_r * sum_r {
return None;
}
let dist = dist_sq.sqrt();
let normal = if dist < f64::EPSILON {
[0.0, 1.0, 0.0]
} else {
v3_normalize(diff)
};
let depth = sum_r - dist;
let contact_pos = v3_add(closest, v3_scale(normal, cap_r - depth * 0.5));
let local_a = v3_scale(normal, cap_r);
let local_b = v3_scale(v3_neg(normal), sphere_r);
let mut manifold = ContactManifold::new(body_cap, body_sphere, normal);
manifold.add_point(ContactPoint::new(contact_pos, local_a, local_b, depth));
Some(manifold)
}
pub fn closest_points_on_segments(
p1: [f64; 3],
p2: [f64; 3],
p3: [f64; 3],
p4: [f64; 3],
) -> (f64, f64, [f64; 3], [f64; 3]) {
let d1 = v3_sub(p2, p1);
let d2 = v3_sub(p4, p3);
let r = v3_sub(p1, p3);
let a = v3_dot(d1, d1);
let e = v3_dot(d2, d2);
let f = v3_dot(d2, r);
let (s, t) = if a < f64::EPSILON && e < f64::EPSILON {
(0.0_f64, 0.0_f64)
} else if a < f64::EPSILON {
(0.0, (f / e).clamp(0.0, 1.0))
} else {
let c = v3_dot(d1, r);
if e < f64::EPSILON {
((-c / a).clamp(0.0, 1.0), 0.0)
} else {
let b = v3_dot(d1, d2);
let denom = a * e - b * b;
let s = if denom.abs() > f64::EPSILON {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let t = (b * s + f) / e;
let (s, t) = if t < 0.0 {
((-c / a).clamp(0.0, 1.0), 0.0)
} else if t > 1.0 {
(((b - c) / a).clamp(0.0, 1.0), 1.0)
} else {
(s, t)
};
(s, t)
}
};
let c1 = v3_add(p1, v3_scale(d1, s));
let c2 = v3_add(p3, v3_scale(d2, t));
(s, t, c1, c2)
}
#[allow(clippy::too_many_arguments)]
pub fn manifold_capsule_capsule(
body_a: usize,
body_b: usize,
a1: [f64; 3],
a2: [f64; 3],
ra: f64,
b1: [f64; 3],
b2: [f64; 3],
rb: f64,
) -> Option<ContactManifold> {
let (_s, _t, ca, cb) = closest_points_on_segments(a1, a2, b1, b2);
let diff = v3_sub(cb, ca);
let dist_sq = v3_len_sq(diff);
let sum_r = ra + rb;
if dist_sq >= sum_r * sum_r {
return None;
}
let dist = dist_sq.sqrt();
let normal = if dist < f64::EPSILON {
[0.0, 1.0, 0.0]
} else {
v3_normalize(diff)
};
let depth = sum_r - dist;
let contact_pos = v3_add(ca, v3_scale(normal, ra - depth * 0.5));
let local_a = v3_scale(normal, ra);
let local_b = v3_scale(v3_neg(normal), rb);
let mut manifold = ContactManifold::new(body_a, body_b, normal);
manifold.add_point(ContactPoint::new(contact_pos, local_a, local_b, depth));
Some(manifold)
}
pub fn merge_manifolds(a: &ContactManifold, b: &ContactManifold) -> ContactManifold {
debug_assert_eq!(a.body_a, b.body_a);
debug_assert_eq!(a.body_b, b.body_b);
let mut all_points: Vec<ContactPoint> = Vec::new();
for p in a.active_points() {
all_points.push(*p);
}
for p in b.active_points() {
all_points.push(*p);
}
let n = v3_normalize(v3_add(a.basis.normal, b.basis.normal));
let mut merged = ContactManifold::new(a.body_a, a.body_b, n);
for cp in reduce_to_4(&all_points) {
merged.add_point(cp);
}
merged
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ManifoldKey {
pub a: usize,
pub b: usize,
}
impl ManifoldKey {
pub fn new(i: usize, j: usize) -> Self {
if i <= j {
Self { a: i, b: j }
} else {
Self { a: j, b: i }
}
}
}
#[derive(Debug, Default)]
pub struct ManifoldCache {
entries: std::collections::HashMap<ManifoldKey, ContactManifold>,
}
impl ManifoldCache {
pub fn new() -> Self {
Self {
entries: std::collections::HashMap::new(),
}
}
pub fn insert(&mut self, manifold: ContactManifold) {
let key = ManifoldKey::new(manifold.body_a, manifold.body_b);
self.entries.insert(key, manifold);
}
pub fn get(&self, a: usize, b: usize) -> Option<&ContactManifold> {
self.entries.get(&ManifoldKey::new(a, b))
}
pub fn get_mut(&mut self, a: usize, b: usize) -> Option<&mut ContactManifold> {
self.entries.get_mut(&ManifoldKey::new(a, b))
}
pub fn remove(&mut self, a: usize, b: usize) {
self.entries.remove(&ManifoldKey::new(a, b));
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn purge_empty(&mut self) {
self.entries.retain(|_, v| !v.is_empty());
}
pub fn age_all(&mut self) {
for m in self.entries.values_mut() {
m.age_points();
}
}
pub fn prune_all(&mut self, max_age: u32) {
for m in self.entries.values_mut() {
m.prune_old_points(max_age);
}
self.purge_empty();
}
pub fn iter(&self) -> impl Iterator<Item = &ContactManifold> {
self.entries.values()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ContactJacobian {
pub lin_a: [f64; 3],
pub ang_a: [f64; 3],
pub lin_b: [f64; 3],
pub ang_b: [f64; 3],
}
impl ContactJacobian {
pub fn normal(r_a: [f64; 3], r_b: [f64; 3], normal: [f64; 3]) -> Self {
Self {
lin_a: normal,
ang_a: v3_cross(r_a, normal),
lin_b: v3_neg(normal),
ang_b: v3_neg(v3_cross(r_b, normal)),
}
}
pub fn tangent(r_a: [f64; 3], r_b: [f64; 3], tangent: [f64; 3]) -> Self {
Self {
lin_a: tangent,
ang_a: v3_cross(r_a, tangent),
lin_b: v3_neg(tangent),
ang_b: v3_neg(v3_cross(r_b, tangent)),
}
}
pub fn evaluate(
&self,
v_a: [f64; 3],
omega_a: [f64; 3],
v_b: [f64; 3],
omega_b: [f64; 3],
) -> f64 {
v3_dot(self.lin_a, v_a)
+ v3_dot(self.ang_a, omega_a)
+ v3_dot(self.lin_b, v_b)
+ v3_dot(self.ang_b, omega_b)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ManifoldQuality {
pub avg_depth: f64,
pub max_depth: f64,
pub spread: f64,
pub area: f64,
pub point_count: usize,
}
pub fn manifold_quality(m: &ContactManifold) -> ManifoldQuality {
let pts: Vec<&ContactPoint> = m.active_points().collect();
let n = pts.len();
if n == 0 {
return ManifoldQuality {
avg_depth: 0.0,
max_depth: 0.0,
spread: 0.0,
area: 0.0,
point_count: 0,
};
}
let avg_depth = pts.iter().map(|p| p.depth).sum::<f64>() / n as f64;
let max_depth = pts
.iter()
.map(|p| p.depth)
.fold(f64::NEG_INFINITY, f64::max);
let mut spread = 0.0_f64;
for i in 0..n {
for j in i + 1..n {
let d = v3_dist(pts[i].position, pts[j].position);
if d > spread {
spread = d;
}
}
}
let area = if n >= 3 {
triangle_area(pts[0].position, pts[1].position, pts[2].position)
} else {
0.0
};
ManifoldQuality {
avg_depth,
max_depth,
spread,
area,
point_count: n,
}
}
pub fn best_fit_normal(points: &[[f64; 3]], hint_normal: [f64; 3]) -> [f64; 3] {
if points.len() < 3 {
return v3_normalize(hint_normal);
}
let n = points.len() as f64;
let centroid = points.iter().fold([0.0_f64; 3], |acc, p| v3_add(acc, *p));
let centroid = v3_scale(centroid, 1.0 / n);
let mut cov = [[0.0_f64; 3]; 3];
for p in points {
let d = v3_sub(*p, centroid);
for i in 0..3 {
for j in 0..3 {
cov[i][j] += d[i] * d[j];
}
}
}
let d0 = v3_sub(points[1], points[0]);
let d1 = v3_sub(points[2], points[0]);
let cross = v3_cross(d0, d1);
let candidate = v3_normalize(cross);
if v3_dot(candidate, hint_normal) < 0.0 {
v3_neg(candidate)
} else {
candidate
}
}
pub fn recompute_manifold_normal(manifold: &mut ContactManifold, hint: [f64; 3]) {
let positions: Vec<[f64; 3]> = manifold.active_points().map(|p| p.position).collect();
if positions.len() >= 3 {
let n = best_fit_normal(&positions, hint);
manifold.update_normal(n);
}
}
pub fn split_manifold(
m: &ContactManifold,
plane_point: [f64; 3],
plane_normal: [f64; 3],
) -> (ContactManifold, ContactManifold) {
let mut pos = ContactManifold::new(m.body_a, m.body_b, m.basis.normal);
let mut neg = ContactManifold::new(m.body_a, m.body_b, m.basis.normal);
for cp in m.active_points() {
let side = v3_dot(v3_sub(cp.position, plane_point), plane_normal);
if side >= 0.0 {
pos.add_point(*cp);
} else {
neg.add_point(*cp);
}
}
(pos, neg)
}
pub fn warm_start_manifold(
new_manifold: &mut ContactManifold,
cached: &ContactManifold,
radius: f64,
) {
for new_pt in new_manifold.points[..new_manifold.point_count]
.iter_mut()
.flatten()
{
if let Some(cached_pt) = cached.active_points().min_by(|a, b| {
v3_dist(a.local_a, new_pt.local_a)
.partial_cmp(&v3_dist(b.local_a, new_pt.local_a))
.unwrap_or(std::cmp::Ordering::Equal)
}) && v3_dist(cached_pt.local_a, new_pt.local_a) < radius
{
new_pt.normal_impulse = cached_pt.normal_impulse;
new_pt.tangent_impulse0 = cached_pt.tangent_impulse0;
new_pt.tangent_impulse1 = cached_pt.tangent_impulse1;
}
}
}
pub fn manifold_aabb_aabb(
body_a: usize,
body_b: usize,
centre_a: [f64; 3],
half_a: [f64; 3],
centre_b: [f64; 3],
half_b: [f64; 3],
) -> Option<ContactManifold> {
let diff = v3_sub(centre_b, centre_a);
let overlap = [
(half_a[0] + half_b[0]) - diff[0].abs(),
(half_a[1] + half_b[1]) - diff[1].abs(),
(half_a[2] + half_b[2]) - diff[2].abs(),
];
if overlap[0] <= 0.0 || overlap[1] <= 0.0 || overlap[2] <= 0.0 {
return None;
}
let axis = if overlap[0] <= overlap[1] && overlap[0] <= overlap[2] {
0
} else if overlap[1] <= overlap[2] {
1
} else {
2
};
let depth = overlap[axis];
let mut normal = [0.0_f64; 3];
normal[axis] = if diff[axis] >= 0.0 { 1.0 } else { -1.0 };
let mut contact_pos = centre_b;
contact_pos[axis] -= normal[axis] * half_b[axis];
let local_a = v3_sub(contact_pos, centre_a);
let local_b = v3_sub(contact_pos, centre_b);
let mut manifold = ContactManifold::new(body_a, body_b, normal);
manifold.add_point(ContactPoint::new(contact_pos, local_a, local_b, depth));
Some(manifold)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_v3_add() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0];
let r = v3_add(a, b);
assert_eq!(r, [5.0, 7.0, 9.0]);
}
#[test]
fn test_v3_dot() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
assert!((v3_dot(a, b)).abs() < 1e-12);
assert!((v3_dot(a, a) - 1.0).abs() < 1e-12);
}
#[test]
fn test_v3_cross_perpendicular() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let c = v3_cross(a, b);
assert!((c[0]).abs() < 1e-12);
assert!((c[1]).abs() < 1e-12);
assert!((c[2] - 1.0).abs() < 1e-12);
}
#[test]
fn test_v3_normalize_unit() {
let a = [3.0, 4.0, 0.0];
let n = v3_normalize(a);
let len = v3_len(n);
assert!((len - 1.0).abs() < 1e-12);
}
#[test]
fn test_v3_normalize_zero_returns_fallback() {
let n = v3_normalize([0.0, 0.0, 0.0]);
assert_eq!(n, [1.0, 0.0, 0.0]);
}
#[test]
fn test_contact_basis_orthonormal_x() {
let basis = ContactBasis::from_normal([1.0, 0.0, 0.0]);
assert!(basis.is_orthonormal(1e-10), "basis not orthonormal");
}
#[test]
fn test_contact_basis_orthonormal_y() {
let basis = ContactBasis::from_normal([0.0, 1.0, 0.0]);
assert!(basis.is_orthonormal(1e-10));
}
#[test]
fn test_contact_basis_orthonormal_diagonal() {
let n = v3_normalize([1.0, 1.0, 1.0]);
let basis = ContactBasis::from_normal(n);
assert!(basis.is_orthonormal(1e-10));
}
#[test]
fn test_contact_point_penetrating() {
let cp = ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.05);
assert!(cp.is_penetrating(0.01));
assert!(!cp.is_penetrating(0.1));
}
#[test]
fn test_contact_point_impulse_default_zero() {
let cp = ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1);
assert_eq!(cp.normal_impulse, 0.0);
assert_eq!(cp.tangent_impulse0, 0.0);
assert_eq!(cp.tangent_impulse1, 0.0);
}
#[test]
fn test_manifold_add_up_to_four_points() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
for k in 0..4 {
let pos = [k as f64, 0.0, 0.0];
m.add_point(ContactPoint::new(pos, pos, pos, 0.1));
}
assert_eq!(m.len(), 4);
}
#[test]
fn test_manifold_clear() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1));
m.clear();
assert!(m.is_empty());
}
#[test]
fn test_manifold_deepest_point() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1));
m.add_point(ContactPoint::new([1.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.5));
m.add_point(ContactPoint::new([2.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.2));
let dp = m.deepest_point().unwrap();
assert!((dp.depth - 0.5).abs() < 1e-12);
}
#[test]
fn test_manifold_average_position() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
m.add_point(ContactPoint::new([2.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
let avg = m.average_position();
assert!((avg[0] - 1.0).abs() < 1e-12);
}
#[test]
fn test_manifold_age_and_prune() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1));
m.age_points();
m.age_points();
m.prune_old_points(1); assert!(m.is_empty());
}
#[test]
fn test_manifold_prune_keeps_young() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1));
m.prune_old_points(5);
assert_eq!(m.len(), 1);
}
#[test]
fn test_reduce_to_4_fewer_unchanged() {
let pts: Vec<ContactPoint> = (0..3)
.map(|i| ContactPoint::new([i as f64, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1))
.collect();
let reduced = reduce_to_4(&pts);
assert_eq!(reduced.len(), 3);
}
#[test]
fn test_reduce_to_4_reduces_large_set() {
let pts: Vec<ContactPoint> = (0..8)
.map(|i| {
let angle = i as f64 * std::f64::consts::PI / 4.0;
ContactPoint::new(
[angle.cos(), angle.sin(), 0.0],
[0.0; 3],
[0.0; 3],
0.1 + i as f64 * 0.01,
)
})
.collect();
let reduced = reduce_to_4(&pts);
assert!(reduced.len() <= MAX_MANIFOLD_POINTS);
assert!(!reduced.is_empty());
}
#[test]
fn test_triangle_area_unit() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let area = triangle_area(a, b, c);
assert!((area - 0.5).abs() < 1e-10, "area={area}");
}
#[test]
fn test_triangle_area_degenerate() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [2.0, 0.0, 0.0]; let area = triangle_area(a, b, c);
assert!(area.abs() < 1e-12, "area={area}");
}
#[test]
fn test_sphere_sphere_separated_returns_none() {
let result = manifold_sphere_sphere(0, 1, [0.0; 3], 1.0, [5.0, 0.0, 0.0], 1.0);
assert!(result.is_none());
}
#[test]
fn test_sphere_sphere_overlapping_returns_some() {
let result = manifold_sphere_sphere(0, 1, [0.0; 3], 1.0, [1.5, 0.0, 0.0], 1.0);
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.len(), 1);
let dp = m.deepest_point().unwrap();
assert!(dp.depth > 0.0);
}
#[test]
fn test_sphere_sphere_normal_unit_length() {
let result = manifold_sphere_sphere(0, 1, [0.0; 3], 1.0, [1.5, 0.0, 0.0], 1.0).unwrap();
let nl = v3_len(result.basis.normal);
assert!((nl - 1.0).abs() < 1e-10, "normal length={nl}");
}
#[test]
fn test_sphere_sphere_depth_correct() {
let ra = 1.0_f64;
let rb = 1.0_f64;
let dist = 1.5_f64;
let expected_depth = ra + rb - dist;
let result = manifold_sphere_sphere(0, 1, [0.0; 3], ra, [dist, 0.0, 0.0], rb).unwrap();
let dp = result.deepest_point().unwrap();
assert!((dp.depth - expected_depth).abs() < 1e-10);
}
#[test]
fn test_sphere_aabb_outside_returns_none() {
let result = manifold_sphere_aabb(0, 1, [5.0, 0.0, 0.0], 0.5, [0.0; 3], [1.0; 3]);
assert!(result.is_none());
}
#[test]
fn test_sphere_aabb_touching_returns_some() {
let result = manifold_sphere_aabb(0, 1, [1.9, 0.0, 0.0], 1.0, [0.0; 3], [1.0; 3]);
assert!(result.is_some());
}
#[test]
fn test_capsule_sphere_separated() {
let result = manifold_capsule_sphere(
0,
1,
[-1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
0.2,
[5.0, 0.0, 0.0],
0.2,
);
assert!(result.is_none());
}
#[test]
fn test_capsule_sphere_overlapping() {
let result = manifold_capsule_sphere(
0,
1,
[-1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
0.5,
[0.0, 0.0, 0.0],
0.5,
);
assert!(result.is_some());
let m = result.unwrap();
assert!(m.deepest_point().unwrap().depth > 0.0);
}
#[test]
fn test_closest_point_midpoint() {
let p = [0.0, 0.0, 0.0];
let q = [2.0, 0.0, 0.0];
let t = [1.0, 1.0, 0.0];
let c = closest_point_on_segment(p, q, t);
assert!((c[0] - 1.0).abs() < 1e-12);
assert!(c[1].abs() < 1e-12);
}
#[test]
fn test_closest_point_clamp_start() {
let p = [0.0, 0.0, 0.0];
let q = [1.0, 0.0, 0.0];
let t = [-1.0, 0.0, 0.0];
let c = closest_point_on_segment(p, q, t);
assert!((c[0]).abs() < 1e-12); }
#[test]
fn test_capsule_capsule_separated() {
let result = manifold_capsule_capsule(
0,
1,
[-5.0, 0.0, 0.0],
[-3.0, 0.0, 0.0],
0.2,
[3.0, 0.0, 0.0],
[5.0, 0.0, 0.0],
0.2,
);
assert!(result.is_none());
}
#[test]
fn test_capsule_capsule_overlapping() {
let result = manifold_capsule_capsule(
0,
1,
[0.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
0.5,
[0.3, 0.0, 0.0],
[0.3, 2.0, 0.0],
0.5,
);
assert!(result.is_some());
}
#[test]
fn test_aabb_aabb_separated() {
let result = manifold_aabb_aabb(0, 1, [0.0; 3], [1.0; 3], [5.0, 0.0, 0.0], [1.0; 3]);
assert!(result.is_none());
}
#[test]
fn test_aabb_aabb_overlapping() {
let result = manifold_aabb_aabb(
0,
1,
[0.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
[1.5, 0.0, 0.0],
[1.0, 1.0, 1.0],
);
assert!(result.is_some());
let m = result.unwrap();
assert!(m.deepest_point().unwrap().depth > 0.0);
}
#[test]
fn test_aabb_aabb_normal_axis_aligned() {
let result = manifold_aabb_aabb(
0,
1,
[0.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
[1.5, 0.0, 0.0],
[1.0, 1.0, 1.0],
)
.unwrap();
let n = result.basis.normal;
assert!(n[0].abs() > 0.9, "normal should be along x, got {:?}", n);
}
#[test]
fn test_jacobian_normal_constraint_velocity() {
let r_a = [0.0, -1.0, 0.0];
let r_b = [0.0, 1.0, 0.0];
let normal = [0.0, 1.0, 0.0];
let jac = ContactJacobian::normal(r_a, r_b, normal);
let cv = jac.evaluate([0.0, 1.0, 0.0], [0.0; 3], [0.0, -1.0, 0.0], [0.0; 3]);
assert!((cv - 2.0).abs() < 1e-10, "cv={cv}");
}
#[test]
fn test_jacobian_tangent_non_zero() {
let r_a = [0.0, 0.0, 0.0];
let r_b = [0.0, 0.0, 0.0];
let tangent = [1.0, 0.0, 0.0];
let jac = ContactJacobian::tangent(r_a, r_b, tangent);
let cv = jac.evaluate([1.0, 0.0, 0.0], [0.0; 3], [0.0; 3], [0.0; 3]);
assert!((cv - 1.0).abs() < 1e-10, "cv={cv}");
}
#[test]
fn test_manifold_quality_empty() {
let m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
let q = manifold_quality(&m);
assert_eq!(q.point_count, 0);
assert_eq!(q.avg_depth, 0.0);
}
#[test]
fn test_manifold_quality_one_point() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.3));
let q = manifold_quality(&m);
assert!((q.avg_depth - 0.3).abs() < 1e-12);
assert!((q.max_depth - 0.3).abs() < 1e-12);
assert_eq!(q.spread, 0.0);
}
#[test]
fn test_manifold_quality_spread() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
m.add_point(ContactPoint::new([3.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
let q = manifold_quality(&m);
assert!((q.spread - 3.0).abs() < 1e-10);
}
#[test]
fn test_manifold_cache_insert_and_get() {
let mut cache = ManifoldCache::new();
let m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
cache.insert(m);
assert!(cache.get(0, 1).is_some());
assert!(cache.get(1, 0).is_some()); }
#[test]
fn test_manifold_cache_remove() {
let mut cache = ManifoldCache::new();
cache.insert(ContactManifold::new(0, 1, [0.0, 1.0, 0.0]));
cache.remove(0, 1);
assert!(cache.get(0, 1).is_none());
}
#[test]
fn test_manifold_cache_purge_empty() {
let mut cache = ManifoldCache::new();
cache.insert(ContactManifold::new(0, 1, [0.0, 1.0, 0.0])); cache.insert(ContactManifold::new(2, 3, [0.0, 1.0, 0.0]));
if let Some(m) = cache.get_mut(2, 3) {
m.add_point(ContactPoint::new([0.0; 3], [0.0; 3], [0.0; 3], 0.1));
}
cache.purge_empty();
assert!(cache.get(0, 1).is_none());
assert!(cache.get(2, 3).is_some());
}
#[test]
fn test_merge_manifolds_combines_points() {
let mut m1 = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m1.add_point(ContactPoint::new([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
let mut m2 = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m2.add_point(ContactPoint::new([2.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.2));
let merged = merge_manifolds(&m1, &m2);
assert!(!merged.is_empty() && merged.len() <= MAX_MANIFOLD_POINTS);
}
#[test]
fn test_split_manifold_separates_correctly() {
let mut m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
m.add_point(ContactPoint::new([1.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
m.add_point(ContactPoint::new([-1.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 0.1));
let (pos, neg) = split_manifold(&m, [0.0; 3], [1.0, 0.0, 0.0]);
assert_eq!(pos.len(), 1);
assert_eq!(neg.len(), 1);
}
#[test]
fn test_warm_start_copies_impulse() {
let mut cached = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
let mut cp = ContactPoint::new([0.0; 3], [0.1, 0.0, 0.0], [0.0; 3], 0.1);
cp.normal_impulse = 5.0;
cached.add_point(cp);
let mut new_m = ContactManifold::new(0, 1, [0.0, 1.0, 0.0]);
new_m.add_point(ContactPoint::new([0.0; 3], [0.1, 0.0, 0.0], [0.0; 3], 0.09));
warm_start_manifold(&mut new_m, &cached, 0.05);
let pt = new_m.active_points().next().unwrap();
assert!((pt.normal_impulse - 5.0).abs() < 1e-12);
}
#[test]
fn test_best_fit_normal_xy_plane() {
let pts = vec![[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let hint = [0.0, 0.0, 1.0];
let n = best_fit_normal(&pts, hint);
assert!(v3_dot(n, [0.0, 0.0, 1.0]).abs() > 0.99, "got {n:?}");
}
#[test]
fn test_manifold_key_canonical() {
let k1 = ManifoldKey::new(3, 5);
let k2 = ManifoldKey::new(5, 3);
assert_eq!(k1, k2);
}
#[test]
fn test_v3_lerp_midpoint() {
let a = [0.0, 0.0, 0.0];
let b = [2.0, 4.0, 6.0];
let m = v3_lerp(a, b, 0.5);
assert!((m[0] - 1.0).abs() < 1e-12);
assert!((m[1] - 2.0).abs() < 1e-12);
assert!((m[2] - 3.0).abs() < 1e-12);
}
}