#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
use std::collections::HashMap;
#[inline]
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn cross3(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 len_sq3(a: [f64; 3]) -> f64 {
dot3(a, a)
}
#[inline]
fn len3(a: [f64; 3]) -> f64 {
len_sq3(a).sqrt()
}
#[inline]
fn normalize3(a: [f64; 3]) -> [f64; 3] {
let l = len3(a);
if l > 1e-10 {
scale3(a, 1.0 / l)
} else {
[0.0, 0.0, 0.0]
}
}
#[inline]
fn mat3_mul_vec(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[inline]
fn transform_point(transform: ([f64; 3], [[f64; 3]; 3]), local: [f64; 3]) -> [f64; 3] {
add3(transform.0, mat3_mul_vec(transform.1, local))
}
pub struct ContactPointId;
impl ContactPointId {
pub fn compute(local_a: [f64; 3], local_b: [f64; 3]) -> u64 {
const QUANT: f64 = 100.0; let qa = [
(local_a[0] * QUANT).round() as i64,
(local_a[1] * QUANT).round() as i64,
(local_a[2] * QUANT).round() as i64,
];
let qb = [
(local_b[0] * QUANT).round() as i64,
(local_b[1] * QUANT).round() as i64,
(local_b[2] * QUANT).round() as i64,
];
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &v in qa.iter().chain(qb.iter()) {
for b in v.to_le_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01B3);
}
}
h
}
}
#[derive(Debug, Clone)]
pub struct ContactPoint {
pub world_pos_a: [f64; 3],
pub world_pos_b: [f64; 3],
pub local_pos_a: [f64; 3],
pub local_pos_b: [f64; 3],
pub normal: [f64; 3],
pub depth: f64,
pub tangent_impulse: [f64; 2],
pub normal_impulse: f64,
pub lifetime: u32,
pub id: u64,
}
impl ContactPoint {
pub fn new(
world_pos_a: [f64; 3],
world_pos_b: [f64; 3],
local_pos_a: [f64; 3],
local_pos_b: [f64; 3],
normal: [f64; 3],
depth: f64,
) -> Self {
let id = ContactPointId::compute(local_pos_a, local_pos_b);
Self {
world_pos_a,
world_pos_b,
local_pos_a,
local_pos_b,
normal,
depth,
tangent_impulse: [0.0, 0.0],
normal_impulse: 0.0,
lifetime: 0,
id,
}
}
}
#[derive(Debug, Clone)]
pub struct PersistentManifold {
pub body_a: u32,
pub body_b: u32,
pub points: Vec<ContactPoint>,
pub normal: [f64; 3],
pub friction: f64,
pub restitution: f64,
pub is_active: bool,
}
const MATCH_DIST_SQ: f64 = 0.01 * 0.01;
const SEPARATION_THRESHOLD: f64 = 0.02;
impl PersistentManifold {
pub fn new(body_a: u32, body_b: u32, friction: f64, restitution: f64) -> Self {
Self {
body_a,
body_b,
points: Vec::with_capacity(4),
normal: [0.0, 1.0, 0.0],
friction,
restitution,
is_active: true,
}
}
pub fn add_or_update(&mut self, mut new_point: ContactPoint) {
let match_idx = self.points.iter().position(|p| {
if p.id == new_point.id {
return true;
}
let d = len_sq3(sub3(p.local_pos_a, new_point.local_pos_a));
d < MATCH_DIST_SQ
});
if let Some(idx) = match_idx {
new_point.normal_impulse = self.points[idx].normal_impulse;
new_point.tangent_impulse = self.points[idx].tangent_impulse;
new_point.lifetime = self.points[idx].lifetime + 1;
self.points[idx] = new_point;
} else {
self.points.push(new_point);
if self.points.len() > 4 {
self.points = ManifoldReduction::reduce_to_4_points(&self.points);
}
}
self.recompute_normal();
}
pub fn remove_stale(
&mut self,
transform_a: ([f64; 3], [[f64; 3]; 3]),
transform_b: ([f64; 3], [[f64; 3]; 3]),
) {
self.points.retain(|p| {
let wa = transform_point(transform_a, p.local_pos_a);
let wb = transform_point(transform_b, p.local_pos_b);
let sep = dot3(sub3(wa, wb), p.normal);
if sep > SEPARATION_THRESHOLD {
return false;
}
if len_sq3(sub3(wa, p.world_pos_a)) > SEPARATION_THRESHOLD * SEPARATION_THRESHOLD {
return false;
}
if len_sq3(sub3(wb, p.world_pos_b)) > SEPARATION_THRESHOLD * SEPARATION_THRESHOLD {
return false;
}
true
});
self.recompute_normal();
}
fn recompute_normal(&mut self) {
if self.points.is_empty() {
return;
}
let mut avg = [0.0f64; 3];
for p in &self.points {
avg = add3(avg, p.normal);
}
let n = self.points.len() as f64;
self.normal = normalize3(scale3(avg, 1.0 / n));
}
}
pub struct ManifoldReduction;
impl ManifoldReduction {
pub fn reduce_to_4_points(points: &[ContactPoint]) -> Vec<ContactPoint> {
if points.len() <= 4 {
return points.to_vec();
}
let idx0 = points
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.depth
.partial_cmp(&b.depth)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
let p0 = points[idx0].world_pos_a;
let idx1 = points
.iter()
.enumerate()
.filter(|(i, _)| *i != idx0)
.max_by(|(_, a), (_, b)| {
len_sq3(sub3(a.world_pos_a, p0))
.partial_cmp(&len_sq3(sub3(b.world_pos_a, p0)))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
let p1 = points[idx1].world_pos_a;
let idx2 = points
.iter()
.enumerate()
.filter(|(i, _)| *i != idx0 && *i != idx1)
.max_by(|(_, a), (_, b)| {
Self::tri_area_sq(p0, p1, a.world_pos_a)
.partial_cmp(&Self::tri_area_sq(p0, p1, b.world_pos_a))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
let p2 = points[idx2].world_pos_a;
let maybe_idx3 = points
.iter()
.enumerate()
.filter(|(i, _)| *i != idx0 && *i != idx1 && *i != idx2)
.max_by(|(_, a), (_, b)| {
Self::quad_area_sq(p0, p1, p2, a.world_pos_a)
.partial_cmp(&Self::quad_area_sq(p0, p1, p2, b.world_pos_a))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i);
let mut result = vec![
points[idx0].clone(),
points[idx1].clone(),
points[idx2].clone(),
];
if let Some(idx3) = maybe_idx3 {
result.push(points[idx3].clone());
}
result
}
fn tri_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
len_sq3(cross3(sub3(b, a), sub3(c, a)))
}
fn quad_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> f64 {
Self::tri_area_sq(a, b, c) + Self::tri_area_sq(a, c, d)
}
pub fn contact_area(points: &[ContactPoint]) -> f64 {
if points.len() < 3 {
return 0.0;
}
let n = points[0].normal;
let t1 = Self::make_tangent(n);
let t2 = cross3(n, t1);
let origin = points[0].world_pos_a;
let projected: Vec<[f64; 2]> = points
.iter()
.map(|p| {
let d = sub3(p.world_pos_a, origin);
[dot3(d, t1), dot3(d, t2)]
})
.collect();
shoelace_area(&projected)
}
fn make_tangent(n: [f64; 3]) -> [f64; 3] {
let candidate = if n[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
normalize3(cross3(n, candidate))
}
}
fn shoelace_area(pts: &[[f64; 2]]) -> f64 {
let n = pts.len();
if n < 3 {
return 0.0;
}
let mut area = 0.0f64;
for i in 0..n {
let j = (i + 1) % n;
area += pts[i][0] * pts[j][1];
area -= pts[j][0] * pts[i][1];
}
area.abs() * 0.5
}
pub struct ContactManifoldCache {
pub manifolds: HashMap<(u32, u32), PersistentManifold>,
pub max_lifetime: u32,
}
impl ContactManifoldCache {
pub fn new(max_lifetime: u32) -> Self {
Self {
manifolds: HashMap::new(),
max_lifetime,
}
}
#[inline]
fn key(body_a: u32, body_b: u32) -> (u32, u32) {
(body_a.min(body_b), body_a.max(body_b))
}
pub fn get_or_create(
&mut self,
body_a: u32,
body_b: u32,
friction: f64,
restitution: f64,
) -> &mut PersistentManifold {
let k = Self::key(body_a, body_b);
self.manifolds
.entry(k)
.or_insert_with(|| PersistentManifold::new(body_a, body_b, friction, restitution))
}
pub fn update_manifold(&mut self, body_a: u32, body_b: u32, new_points: Vec<ContactPoint>) {
let k = Self::key(body_a, body_b);
if let Some(manifold) = self.manifolds.get_mut(&k) {
manifold.is_active = true;
for pt in new_points {
manifold.add_or_update(pt);
}
for p in &mut manifold.points {
p.lifetime = p.lifetime.saturating_add(1);
}
}
}
pub fn remove_inactive(&mut self) {
self.manifolds
.retain(|_, m| m.is_active && !m.points.is_empty());
}
pub fn begin_frame(&mut self) {
for m in self.manifolds.values_mut() {
m.is_active = false;
}
}
}
pub struct ManifoldPointMatcher;
impl ManifoldPointMatcher {
pub fn find_match(
existing: &[ContactPoint],
new_point: &ContactPoint,
dist_sq_threshold: f64,
) -> Option<usize> {
for (i, p) in existing.iter().enumerate() {
if p.id == new_point.id {
return Some(i);
}
let d = len_sq3(sub3(p.local_pos_a, new_point.local_pos_a));
if d < dist_sq_threshold {
return Some(i);
}
}
None
}
pub fn match_all(
existing: &[ContactPoint],
new_points: &[ContactPoint],
dist_sq_threshold: f64,
) -> Vec<(usize, usize)> {
let mut matches = Vec::new();
for (ni, np) in new_points.iter().enumerate() {
if let Some(ei) = Self::find_match(existing, np, dist_sq_threshold) {
matches.push((ni, ei));
}
}
matches
}
}
pub struct ManifoldLifetimeManager {
pub max_inactive_frames: u32,
}
impl ManifoldLifetimeManager {
pub fn new(max_inactive_frames: u32) -> Self {
Self {
max_inactive_frames,
}
}
pub fn age_manifold(&self, manifold: &mut PersistentManifold) {
manifold
.points
.retain(|p| p.lifetime <= self.max_inactive_frames);
}
pub fn process_cache(&self, cache: &mut ContactManifoldCache) {
for manifold in cache.manifolds.values_mut() {
self.age_manifold(manifold);
}
cache.manifolds.retain(|_, m| !m.points.is_empty());
}
pub fn active_count(&self, cache: &ContactManifoldCache) -> usize {
cache.manifolds.values().filter(|m| m.is_active).count()
}
}
pub struct ManifoldCompressor;
impl ManifoldCompressor {
pub fn compress(points: &[ContactPoint], max_count: usize) -> Vec<ContactPoint> {
if points.len() <= max_count {
return points.to_vec();
}
if max_count == 0 {
return vec![];
}
if max_count == 1 {
let idx = points
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.depth
.partial_cmp(&b.depth)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
return vec![points[idx].clone()];
}
ManifoldReduction::reduce_to_4_points(points)
}
pub fn deduplicate(points: &mut Vec<ContactPoint>) {
let mut seen = std::collections::HashSet::new();
points.retain(|p| seen.insert(p.id));
}
pub fn merge(
existing: &[ContactPoint],
incoming: &[ContactPoint],
max_count: usize,
) -> Vec<ContactPoint> {
let mut merged: Vec<ContactPoint> = existing.to_vec();
for inc in incoming {
if let Some(idx) = ManifoldPointMatcher::find_match(&merged, inc, MATCH_DIST_SQ) {
let mut updated = inc.clone();
updated.normal_impulse = merged[idx].normal_impulse;
updated.tangent_impulse = merged[idx].tangent_impulse;
updated.lifetime = merged[idx].lifetime + 1;
merged[idx] = updated;
} else {
merged.push(inc.clone());
}
}
Self::compress(&merged, max_count)
}
}
impl PersistentManifold {
pub fn update_from_new_contacts(&mut self, new_contacts: Vec<ContactPoint>) {
let merged = ManifoldCompressor::merge(&self.points, &new_contacts, 4);
self.points = merged;
self.recompute_normal();
self.is_active = true;
}
pub fn warmstart_data(&self) -> Vec<(f64, [f64; 2])> {
self.points
.iter()
.map(|p| (p.normal_impulse, p.tangent_impulse))
.collect()
}
pub fn store_solver_impulses(&mut self, impulses: &[(f64, [f64; 2])]) {
for (p, &(ni, ti)) in self.points.iter_mut().zip(impulses.iter()) {
p.normal_impulse = ni;
p.tangent_impulse = ti;
}
}
pub fn contact_count(&self) -> usize {
self.points.len()
}
pub fn max_depth(&self) -> f64 {
self.points.iter().map(|p| p.depth).fold(0.0f64, f64::max)
}
pub fn clamp_impulses(&mut self) {
for p in &mut self.points {
p.normal_impulse = p.normal_impulse.max(0.0);
}
}
}
impl ContactManifoldCache {
pub fn total_contact_points(&self) -> usize {
self.manifolds.values().map(|m| m.points.len()).sum()
}
pub fn manifold_count(&self) -> usize {
self.manifolds.len()
}
pub fn clear(&mut self) {
self.manifolds.clear();
}
pub fn get_warmstart(&self, body_a: u32, body_b: u32) -> Option<Vec<(f64, [f64; 2])>> {
let k = Self::key(body_a, body_b);
self.manifolds.get(&k).map(|m| m.warmstart_data())
}
pub fn remove_body(&mut self, body_id: u32) {
self.manifolds
.retain(|_, m| m.body_a != body_id && m.body_b != body_id);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachingStrategy {
IdOnly,
ProximityOnly,
IdThenProximity,
}
pub fn find_match_with_strategy(
existing: &[ContactPoint],
new_point: &ContactPoint,
strategy: CachingStrategy,
dist_sq_threshold: f64,
) -> Option<usize> {
match strategy {
CachingStrategy::IdOnly => existing.iter().position(|p| p.id == new_point.id),
CachingStrategy::ProximityOnly => existing.iter().enumerate().find_map(|(i, p)| {
if len_sq3(sub3(p.local_pos_a, new_point.local_pos_a)) < dist_sq_threshold {
Some(i)
} else {
None
}
}),
CachingStrategy::IdThenProximity => {
ManifoldPointMatcher::find_match(existing, new_point, dist_sq_threshold)
}
}
}
pub fn age_warm_start(point: &mut ContactPoint, age_factor: f64) {
point.normal_impulse *= age_factor;
point.tangent_impulse[0] *= age_factor;
point.tangent_impulse[1] *= age_factor;
}
pub fn age_manifold_warm_start(manifold: &mut PersistentManifold, age_factor: f64) {
for p in &mut manifold.points {
age_warm_start(p, age_factor);
}
}
pub fn age_cache_warm_start(cache: &mut ContactManifoldCache, age_factor: f64) {
for manifold in cache.manifolds.values_mut() {
age_manifold_warm_start(manifold, age_factor);
}
}
pub fn baumgarte_correction(depth: f64, beta: f64, dt: f64) -> f64 {
if dt > 1e-12 {
(beta * depth / dt).max(0.0)
} else {
0.0
}
}
pub fn baumgarte_correction_slop(depth: f64, beta: f64, dt: f64, slop: f64) -> f64 {
baumgarte_correction((depth - slop).max(0.0), beta, dt)
}
pub fn apply_position_corrections(
manifold: &PersistentManifold,
beta: f64,
dt: f64,
slop: f64,
) -> Vec<f64> {
manifold
.points
.iter()
.map(|p| baumgarte_correction_slop(p.depth, beta, dt, slop))
.collect()
}
#[derive(Debug, Clone)]
pub struct ContactIsland {
pub bodies: Vec<u32>,
pub manifold_keys: Vec<(u32, u32)>,
}
impl ContactIsland {
pub fn new() -> Self {
Self {
bodies: Vec::new(),
manifold_keys: Vec::new(),
}
}
pub fn body_count(&self) -> usize {
self.bodies.len()
}
pub fn contact_count(&self) -> usize {
self.manifold_keys.len()
}
}
impl Default for ContactIsland {
fn default() -> Self {
Self::new()
}
}
pub fn build_contact_islands(cache: &ContactManifoldCache) -> Vec<ContactIsland> {
let mut all_bodies: Vec<u32> = Vec::new();
for (a, b) in cache.manifolds.keys() {
if !all_bodies.contains(a) {
all_bodies.push(*a);
}
if !all_bodies.contains(b) {
all_bodies.push(*b);
}
}
let n = all_bodies.len();
if n == 0 {
return Vec::new();
}
let mut parent: Vec<usize> = (0..n).collect();
let find = |parent: &mut Vec<usize>, mut x: usize| -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]]; x = parent[x];
}
x
};
for (a, b) in cache.manifolds.keys() {
let Some(ia) = all_bodies.iter().position(|&id| id == *a) else {
continue;
};
let Some(ib) = all_bodies.iter().position(|&id| id == *b) else {
continue;
};
let ra = find(&mut parent, ia);
let rb = find(&mut parent, ib);
if ra != rb {
parent[ra] = rb;
}
}
let mut island_map: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
let mut islands: Vec<ContactIsland> = Vec::new();
for i in 0..n {
let root = find(&mut parent, i);
let island_idx = *island_map.entry(root).or_insert_with(|| {
islands.push(ContactIsland::new());
islands.len() - 1
});
islands[island_idx].bodies.push(all_bodies[i]);
}
for key in cache.manifolds.keys() {
let Some(ia) = all_bodies.iter().position(|&id| id == key.0) else {
continue;
};
let root = find(&mut parent, ia);
let island_idx = island_map[&root];
islands[island_idx].manifold_keys.push(*key);
}
islands
}
#[derive(Debug, Clone)]
pub struct ManifoldMetrics {
pub contact_count: usize,
pub max_depth: f64,
pub avg_depth: f64,
pub spread: f64,
pub is_warm: bool,
}
pub fn compute_manifold_metrics(manifold: &PersistentManifold) -> ManifoldMetrics {
let n = manifold.points.len();
if n == 0 {
return ManifoldMetrics {
contact_count: 0,
max_depth: 0.0,
avg_depth: 0.0,
spread: 0.0,
is_warm: false,
};
}
let max_depth = manifold
.points
.iter()
.map(|p| p.depth)
.fold(0.0f64, f64::max);
let avg_depth = manifold.points.iter().map(|p| p.depth).sum::<f64>() / n as f64;
let is_warm = manifold
.points
.iter()
.all(|p| p.normal_impulse.abs() > 0.0 || p.lifetime > 0);
let mut spread = 0.0f64;
for i in 0..n {
for j in (i + 1)..n {
let d = len3(sub3(
manifold.points[i].world_pos_a,
manifold.points[j].world_pos_a,
));
if d > spread {
spread = d;
}
}
}
ManifoldMetrics {
contact_count: n,
max_depth,
avg_depth,
spread,
is_warm,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_contact(
pos_a: [f64; 3],
pos_b: [f64; 3],
depth: f64,
normal_impulse: f64,
) -> ContactPoint {
let mut cp = ContactPoint::new(pos_a, pos_b, pos_a, pos_b, [0.0, 1.0, 0.0], depth);
cp.normal_impulse = normal_impulse;
cp
}
#[test]
fn test_add_same_point_twice_no_overflow() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
m.add_or_update(pt.clone());
m.add_or_update(pt.clone());
assert_eq!(m.points.len(), 1, "same point added twice should stay as 1");
}
#[test]
fn test_add_or_update_max_4_points() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let positions: [[f64; 3]; 6] = [
[1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, -1.0],
[0.5, 0.0, 0.5],
[-0.5, 0.0, -0.5],
];
for &p in &positions {
let neg_p = scale3(p, -1.0);
let cp = make_contact(p, neg_p, 0.01, 0.0);
m.add_or_update(cp);
}
assert!(
m.points.len() <= 4,
"manifold must not exceed 4 points, got {}",
m.points.len()
);
}
#[test]
fn test_reduce_to_4_points_with_6_inputs() {
let positions: [[f64; 3]; 6] = [
[1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, -1.0],
[0.5, 0.0, 0.5],
[-0.5, 0.0, -0.5],
];
let points: Vec<ContactPoint> = positions
.iter()
.map(|&p| ContactPoint::new(p, p, p, p, [0.0, 1.0, 0.0], 0.01))
.collect();
let reduced = ManifoldReduction::reduce_to_4_points(&points);
assert!(
reduced.len() <= 4,
"reduce_to_4_points must return <=4 points, got {}",
reduced.len()
);
}
#[test]
fn test_get_or_create_new_pair() {
let mut cache = ContactManifoldCache::new(5);
let m = cache.get_or_create(1, 2, 0.5, 0.3);
assert_eq!(m.friction, 0.5);
assert_eq!(m.restitution, 0.3);
assert!(m.is_active);
}
#[test]
fn test_get_or_create_ordered_key() {
let mut cache = ContactManifoldCache::new(5);
let _ = cache.get_or_create(2, 1, 0.4, 0.2);
assert!(
cache.manifolds.contains_key(&(1, 2)),
"key should be canonicalized to (1,2)"
);
}
#[test]
fn test_begin_frame_marks_all_inactive() {
let mut cache = ContactManifoldCache::new(5);
let _ = cache.get_or_create(0, 1, 0.5, 0.3);
let _ = cache.get_or_create(2, 3, 0.5, 0.3);
cache.begin_frame();
for m in cache.manifolds.values() {
assert!(
!m.is_active,
"manifold should be inactive after begin_frame"
);
}
}
#[test]
fn test_warm_start_impulse_preserved() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
pt.normal_impulse = 42.0;
m.add_or_update(pt);
let pt2 = ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
m.add_or_update(pt2);
assert_eq!(
m.points[0].normal_impulse, 42.0,
"normal_impulse should be warm-started from previous frame"
);
}
#[test]
fn test_contact_area_nonzero_for_triangle() {
let pts = vec![
ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
0.01,
),
ContactPoint::new(
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
0.01,
),
ContactPoint::new(
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 1.0, 0.0],
0.01,
),
];
let area = ManifoldReduction::contact_area(&pts);
assert!(
area > 0.0,
"contact_area of non-collinear triangle must be > 0, got {}",
area
);
}
#[test]
fn test_matcher_finds_by_id() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0)];
let new_pt = ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
assert_eq!(idx, Some(0), "should match by ID");
}
#[test]
fn test_matcher_finds_by_proximity() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0)];
let new_pt = ContactPoint::new(
[0.001, 0.0, 0.0],
[0.001, -0.01, 0.0],
[0.001, 0.0, 0.0], [0.001, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
assert!(idx.is_some(), "should match by proximity");
}
#[test]
fn test_matcher_no_match_far() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0)];
let new_pt = ContactPoint::new(
[5.0, 0.0, 0.0],
[5.0, -0.01, 0.0],
[5.0, 0.0, 0.0],
[5.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let idx = ManifoldPointMatcher::find_match(&existing, &new_pt, MATCH_DIST_SQ);
assert!(idx.is_none(), "far point should not match");
}
#[test]
fn test_lifetime_manager_ages_out_old_points() {
let mgr = ManifoldLifetimeManager::new(2);
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
pt.lifetime = 100; m.points.push(pt);
mgr.age_manifold(&mut m);
assert!(m.points.is_empty(), "old point should be removed");
}
#[test]
fn test_lifetime_manager_keeps_young_points() {
let mgr = ManifoldLifetimeManager::new(10);
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 1.0);
pt.lifetime = 3;
m.points.push(pt);
mgr.age_manifold(&mut m);
assert_eq!(m.points.len(), 1, "young point should be kept");
}
#[test]
fn test_compressor_no_change_small_set() {
let pts: Vec<ContactPoint> = (0..3)
.map(|i| make_contact([i as f64, 0.0, 0.0], [i as f64, -0.01, 0.0], 0.01, 0.0))
.collect();
let out = ManifoldCompressor::compress(&pts, 4);
assert_eq!(out.len(), 3);
}
#[test]
fn test_compressor_single_point() {
let pts: Vec<ContactPoint> = vec![
make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.05, 1.0),
make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0),
];
let out = ManifoldCompressor::compress(&pts, 1);
assert_eq!(out.len(), 1);
assert!((out[0].depth - 0.05).abs() < 1e-10);
}
#[test]
fn test_compressor_merge_preserves_warmstart() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 7.0)];
let incoming = vec![ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
)];
let merged = ManifoldCompressor::merge(&existing, &incoming, 4);
assert_eq!(merged.len(), 1);
assert!(
(merged[0].normal_impulse - 7.0).abs() < 1e-10,
"warm-start should be 7.0"
);
}
#[test]
fn test_update_from_new_contacts() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let contacts = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
m.update_from_new_contacts(contacts);
assert_eq!(m.contact_count(), 1);
assert!(m.is_active);
}
#[test]
fn test_max_depth() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.02, 0.0));
m.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.05, 0.0));
assert!((m.max_depth() - 0.05).abs() < 1e-10);
}
#[test]
fn test_warmstart_data_roundtrip() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0);
pt.normal_impulse = 3.0;
pt.tangent_impulse = [1.0, 2.0];
m.points.push(pt);
let ws = m.warmstart_data();
assert_eq!(ws.len(), 1);
assert!((ws[0].0 - 3.0).abs() < 1e-12);
m.store_solver_impulses(&[(10.0, [5.0, 6.0])]);
assert!((m.points[0].normal_impulse - 10.0).abs() < 1e-12);
}
#[test]
fn test_clamp_impulses() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0);
pt.normal_impulse = -3.0;
m.points.push(pt);
m.clamp_impulses();
assert_eq!(m.points[0].normal_impulse, 0.0);
}
#[test]
fn test_cache_total_contact_points() {
let mut cache = ContactManifoldCache::new(5);
let m = cache.get_or_create(0, 1, 0.5, 0.3);
m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
m.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0));
assert_eq!(cache.total_contact_points(), 2);
}
#[test]
fn test_cache_remove_body() {
let mut cache = ContactManifoldCache::new(5);
let _ = cache.get_or_create(0, 1, 0.5, 0.3);
let _ = cache.get_or_create(2, 3, 0.5, 0.3);
cache.remove_body(0);
assert_eq!(cache.manifold_count(), 1);
}
#[test]
fn test_cache_clear() {
let mut cache = ContactManifoldCache::new(5);
let _ = cache.get_or_create(0, 1, 0.5, 0.3);
cache.clear();
assert_eq!(cache.manifold_count(), 0);
}
#[test]
fn test_cache_get_warmstart_none() {
let cache = ContactManifoldCache::new(5);
assert!(cache.get_warmstart(0, 1).is_none());
}
#[test]
fn test_caching_strategy_id_only_finds_match() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
let new_pt = ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let result =
find_match_with_strategy(&existing, &new_pt, CachingStrategy::IdOnly, MATCH_DIST_SQ);
assert_eq!(result, Some(0), "ID-only strategy should match by hash ID");
}
#[test]
fn test_caching_strategy_proximity_only_finds_match() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
let new_pt = ContactPoint::new(
[0.001, 0.0, 0.0],
[0.001, -0.01, 0.0],
[0.001, 0.0, 0.0],
[0.001, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let result = find_match_with_strategy(
&existing,
&new_pt,
CachingStrategy::ProximityOnly,
MATCH_DIST_SQ,
);
assert!(
result.is_some(),
"Proximity strategy should find nearby match"
);
}
#[test]
fn test_caching_strategy_proximity_only_no_match_far() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0)];
let new_pt = ContactPoint::new(
[10.0, 0.0, 0.0],
[10.0, -0.01, 0.0],
[10.0, 0.0, 0.0],
[10.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let result = find_match_with_strategy(
&existing,
&new_pt,
CachingStrategy::ProximityOnly,
MATCH_DIST_SQ,
);
assert!(
result.is_none(),
"Far point should not match with proximity strategy"
);
}
#[test]
fn test_caching_strategy_id_then_proximity_finds_by_id() {
let existing = vec![make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 3.0)];
let new_pt = ContactPoint::new(
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 0.0, 0.0],
[0.0, -0.01, 0.0],
[0.0, 1.0, 0.0],
0.01,
);
let result = find_match_with_strategy(
&existing,
&new_pt,
CachingStrategy::IdThenProximity,
MATCH_DIST_SQ,
);
assert_eq!(result, Some(0), "IdThenProximity should find by ID");
}
#[test]
fn test_age_warm_start_scales_impulses() {
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 10.0);
pt.tangent_impulse = [4.0, 2.0];
age_warm_start(&mut pt, 0.9);
assert!(
(pt.normal_impulse - 9.0).abs() < 1e-10,
"normal_impulse should be 9.0"
);
assert!(
(pt.tangent_impulse[0] - 3.6).abs() < 1e-10,
"tangent[0] should be 3.6"
);
assert!(
(pt.tangent_impulse[1] - 1.8).abs() < 1e-10,
"tangent[1] should be 1.8"
);
}
#[test]
fn test_age_manifold_warm_start() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0);
pt.tangent_impulse = [2.0, 0.0];
m.points.push(pt);
age_manifold_warm_start(&mut m, 0.5);
assert!(
(m.points[0].normal_impulse - 2.5).abs() < 1e-10,
"normal_impulse should be 2.5"
);
assert!(
(m.points[0].tangent_impulse[0] - 1.0).abs() < 1e-10,
"tangent[0] should be 1.0"
);
}
#[test]
fn test_age_cache_warm_start() {
let mut cache = ContactManifoldCache::new(5);
let m = cache.get_or_create(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 8.0);
pt.tangent_impulse = [4.0, 0.0];
m.points.push(pt);
age_cache_warm_start(&mut cache, 0.5);
let m2 = cache.manifolds.get(&(0, 1)).unwrap();
assert!(
(m2.points[0].normal_impulse - 4.0).abs() < 1e-10,
"normal_impulse should be 4.0"
);
}
#[test]
fn test_baumgarte_correction_basic() {
let corr = baumgarte_correction(0.1, 0.2, 0.016);
let expected = 0.2 * 0.1 / 0.016;
assert!(
(corr - expected).abs() < 1e-10,
"Expected {expected}, got {corr}"
);
}
#[test]
fn test_baumgarte_correction_zero_depth() {
let corr = baumgarte_correction(0.0, 0.2, 0.016);
assert_eq!(corr, 0.0, "Zero depth should produce no correction");
}
#[test]
fn test_baumgarte_correction_slop_no_correction_below_slop() {
let corr = baumgarte_correction_slop(0.003, 0.2, 0.016, 0.005);
assert_eq!(corr, 0.0, "Depth below slop should produce no correction");
}
#[test]
fn test_baumgarte_correction_slop_correction_above_slop() {
let corr = baumgarte_correction_slop(0.01, 0.2, 0.016, 0.005);
let expected = baumgarte_correction(0.005, 0.2, 0.016);
assert!(
(corr - expected).abs() < 1e-10,
"Expected {expected}, got {corr}"
);
}
#[test]
fn test_apply_position_corrections_returns_per_point() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
m.points
.push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
m.points
.push(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.02, 0.0));
let corrections = apply_position_corrections(&m, 0.2, 0.016, 0.005);
assert_eq!(
corrections.len(),
2,
"Should return one correction per point"
);
assert!(
corrections[0] >= 0.0 && corrections[1] >= 0.0,
"Corrections should be non-negative"
);
}
#[test]
fn test_contact_island_default() {
let island = ContactIsland::default();
assert_eq!(island.body_count(), 0);
assert_eq!(island.contact_count(), 0);
}
#[test]
fn test_build_contact_islands_single_pair() {
let mut cache = ContactManifoldCache::new(5);
let m = cache.get_or_create(0, 1, 0.5, 0.3);
m.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
let islands = build_contact_islands(&cache);
assert_eq!(islands.len(), 1, "One pair should form one island");
assert_eq!(islands[0].body_count(), 2, "Island should have 2 bodies");
}
#[test]
fn test_build_contact_islands_two_separate_pairs() {
let mut cache = ContactManifoldCache::new(5);
let m1 = cache.get_or_create(0, 1, 0.5, 0.3);
m1.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
let m2 = cache.get_or_create(2, 3, 0.5, 0.3);
m2.add_or_update(make_contact([5.0, 0.0, 0.0], [5.0, -0.01, 0.0], 0.01, 0.0));
let islands = build_contact_islands(&cache);
assert_eq!(
islands.len(),
2,
"Two separate pairs should form two islands"
);
}
#[test]
fn test_build_contact_islands_connected_chain() {
let mut cache = ContactManifoldCache::new(5);
let m1 = cache.get_or_create(0, 1, 0.5, 0.3);
m1.add_or_update(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 0.0));
let m2 = cache.get_or_create(1, 2, 0.5, 0.3);
m2.add_or_update(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.01, 0.0));
let islands = build_contact_islands(&cache);
assert_eq!(islands.len(), 1, "Connected chain should form one island");
assert_eq!(
islands[0].body_count(),
3,
"Chain of 3 bodies should have 3 bodies in island"
);
}
#[test]
fn test_manifold_metrics_empty() {
let m = PersistentManifold::new(0, 1, 0.5, 0.3);
let metrics = compute_manifold_metrics(&m);
assert_eq!(metrics.contact_count, 0);
assert_eq!(metrics.max_depth, 0.0);
assert!(!metrics.is_warm);
}
#[test]
fn test_manifold_metrics_single_point() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
m.points
.push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.05, 0.0));
let metrics = compute_manifold_metrics(&m);
assert_eq!(metrics.contact_count, 1);
assert!(
(metrics.max_depth - 0.05).abs() < 1e-10,
"max_depth should be 0.05"
);
assert!(
(metrics.avg_depth - 0.05).abs() < 1e-10,
"avg_depth should be 0.05"
);
assert_eq!(metrics.spread, 0.0, "Single point has zero spread");
}
#[test]
fn test_manifold_metrics_two_points() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
m.points
.push(make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.02, 0.0));
m.points
.push(make_contact([1.0, 0.0, 0.0], [1.0, -0.01, 0.0], 0.04, 0.0));
let metrics = compute_manifold_metrics(&m);
assert_eq!(metrics.contact_count, 2);
assert!((metrics.max_depth - 0.04).abs() < 1e-10);
assert!((metrics.avg_depth - 0.03).abs() < 1e-10);
assert!(
(metrics.spread - 1.0).abs() < 1e-10,
"Spread should be 1.0, got {}",
metrics.spread
);
}
#[test]
fn test_manifold_metrics_is_warm_with_impulse() {
let mut m = PersistentManifold::new(0, 1, 0.5, 0.3);
let mut pt = make_contact([0.0, 0.0, 0.0], [0.0, -0.01, 0.0], 0.01, 5.0);
pt.normal_impulse = 5.0;
m.points.push(pt);
let metrics = compute_manifold_metrics(&m);
assert!(
metrics.is_warm,
"Manifold with non-zero normal_impulse should be warm"
);
}
}