use arrayvec::ArrayVec;
use nalgebra::SVector;
use crate::body::BodyHandle;
pub const MAX_CONTACTS: usize = 8;
#[derive(Clone, Debug)]
pub struct ContactPoint<const D: usize> {
pub position: SVector<f64, D>,
pub depth: f64,
pub lambda: f64,
}
#[derive(Clone, Debug)]
pub struct CollisionEvent<const D: usize> {
pub body_a: BodyHandle,
pub body_b: BodyHandle,
pub impulse: f64,
pub normal: SVector<f64, D>,
pub depth: f64,
}
#[derive(Clone, Debug)]
pub struct SensorEvent {
pub sensor: BodyHandle,
pub other: BodyHandle,
}
#[derive(Clone, Debug)]
pub struct ContactManifold<const D: usize> {
pub body_a: BodyHandle,
pub body_b: BodyHandle,
pub normal: SVector<f64, D>,
pub points: ArrayVec<ContactPoint<D>, MAX_CONTACTS>,
}
impl<const D: usize> ContactManifold<D> {
pub fn single(
body_a: BodyHandle,
body_b: BodyHandle,
normal: SVector<f64, D>,
point: SVector<f64, D>,
depth: f64,
) -> Self {
let mut points = ArrayVec::new();
points.push(ContactPoint { position: point, depth, lambda: 0.0 });
Self {
body_a,
body_b,
normal,
points,
}
}
pub fn primary_point(&self) -> &ContactPoint<D> {
self.points
.iter()
.max_by(|a, b| a.depth.total_cmp(&b.depth))
.unwrap_or(&self.points[0])
}
pub fn depth(&self) -> f64 {
self.primary_point().depth
}
pub fn point(&self) -> SVector<f64, D> {
self.primary_point().position
}
pub fn impulse_magnitude(
&self,
relative_velocity: &SVector<f64, D>,
inv_mass_a: f64,
inv_mass_b: f64,
restitution: f64,
) -> f64 {
let v_rel_n = relative_velocity.dot(&self.normal);
if v_rel_n > 0.0 {
return 0.0;
}
let denom = inv_mass_a + inv_mass_b;
if denom < 1e-15 {
return 0.0;
}
-(1.0 + restitution) * v_rel_n / denom
}
}
#[derive(Clone, Debug)]
pub struct CachedImpulse<const D: usize> {
pub normal_impulse: f64,
pub tangent_impulse: f64,
pub point: SVector<f64, D>,
}
pub struct ContactCache<const D: usize> {
entries: std::collections::BTreeMap<(BodyHandle, BodyHandle), Vec<CachedImpulse<D>>>,
pub match_threshold: f64,
}
impl<const D: usize> ContactCache<D> {
pub fn new() -> Self {
Self {
entries: std::collections::BTreeMap::new(),
match_threshold: 2.0,
}
}
pub fn store(
&mut self,
body_a: BodyHandle,
body_b: BodyHandle,
point: SVector<f64, D>,
normal_impulse: f64,
tangent_impulse: f64,
) {
let key = if body_a < body_b {
(body_a, body_b)
} else {
(body_b, body_a)
};
let entry = self.entries.entry(key).or_insert_with(Vec::new);
entry.push(CachedImpulse {
normal_impulse,
tangent_impulse,
point,
});
}
pub fn lookup(
&self,
body_a: BodyHandle,
body_b: BodyHandle,
point: &SVector<f64, D>,
) -> Option<&CachedImpulse<D>> {
let key = if body_a < body_b {
(body_a, body_b)
} else {
(body_b, body_a)
};
let entries = self.entries.get(&key)?;
entries
.iter()
.filter(|c| (c.point - point).norm() < self.match_threshold)
.min_by(|a, b| {
let da = (a.point - point).norm();
let db = (b.point - point).norm();
da.total_cmp(&db)
})
}
pub fn begin_frame(&mut self) {
self.entries.clear();
}
pub fn pair_count(&self) -> usize {
self.entries.len()
}
}
impl<const D: usize> Default for ContactCache<D> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_point_manifold() {
let m = ContactManifold::<3>::single(
BodyHandle(0), BodyHandle(1),
SVector::from([0.0, 1.0, 0.0]),
SVector::from([1.0, 0.0, 0.0]),
0.5,
);
assert_eq!(m.points.len(), 1);
assert!((m.depth() - 0.5).abs() < 1e-12);
}
#[test]
fn multi_point_primary_is_deepest() {
let mut m = ContactManifold::<3>::single(
BodyHandle(0), BodyHandle(1),
SVector::from([0.0, 1.0, 0.0]),
SVector::from([1.0, 0.0, 0.0]),
0.3,
);
m.points.push(ContactPoint {
position: SVector::from([2.0, 0.0, 0.0]),
depth: 0.7,
lambda: 0.0,
});
assert_eq!(m.points.len(), 2);
assert!((m.depth() - 0.7).abs() < 1e-12, "primary should be deepest");
}
#[test]
fn contact_cache_store_and_lookup() {
let mut cache = ContactCache::<3>::new();
let pt = SVector::from([1.0, 0.0, 0.0]);
cache.store(BodyHandle(0), BodyHandle(1), pt, 5.0, 1.0);
let hit = cache.lookup(BodyHandle(0), BodyHandle(1), &pt);
assert!(hit.is_some());
assert!((hit.unwrap().normal_impulse - 5.0).abs() < 1e-12);
}
#[test]
fn contact_cache_proximity_match() {
let mut cache = ContactCache::<3>::new();
cache.store(BodyHandle(0), BodyHandle(1), SVector::from([1.0, 0.0, 0.0]), 5.0, 1.0);
let nearby = SVector::from([1.5, 0.0, 0.0]);
let hit = cache.lookup(BodyHandle(0), BodyHandle(1), &nearby);
assert!(hit.is_some(), "nearby point should match cached contact");
let far = SVector::from([100.0, 0.0, 0.0]);
let miss = cache.lookup(BodyHandle(0), BodyHandle(1), &far);
assert!(miss.is_none(), "far point should not match");
}
#[test]
fn contact_cache_symmetric_keys() {
let mut cache = ContactCache::<3>::new();
let pt = SVector::from([0.0, 0.0, 0.0]);
cache.store(BodyHandle(1), BodyHandle(0), pt, 3.0, 0.5);
let hit = cache.lookup(BodyHandle(0), BodyHandle(1), &pt);
assert!(hit.is_some(), "cache should be symmetric on body order");
}
#[test]
fn contact_cache_begin_frame_clears() {
let mut cache = ContactCache::<3>::new();
cache.store(BodyHandle(0), BodyHandle(1), SVector::zeros(), 1.0, 0.0);
assert_eq!(cache.pair_count(), 1);
cache.begin_frame();
assert_eq!(cache.pair_count(), 0);
}
}