use gizmo_core::entity::Entity;
use gizmo_math::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
pub struct ContactPoint {
pub point: Vec3,
pub normal: Vec3,
pub penetration: f32,
pub local_point_a: Vec3,
pub local_point_b: Vec3,
pub normal_impulse: f32,
pub tangent_impulse: Vec3,
}
#[derive(Debug, Clone)]
pub struct ContactManifold {
pub entity_a: Entity,
pub entity_b: Entity,
pub contacts: Vec<ContactPoint>,
pub friction: f32,
pub static_friction: f32,
pub restitution: f32,
pub lifetime: u32,
}
impl ContactManifold {
pub fn new(entity_a: Entity, entity_b: Entity) -> Self {
let (entity_a, entity_b) = if entity_a.id() <= entity_b.id() {
(entity_a, entity_b)
} else {
(entity_b, entity_a)
};
Self {
entity_a,
entity_b,
contacts: Vec::with_capacity(4),
friction: 0.5,
static_friction: 0.5,
restitution: 0.3,
lifetime: 0,
}
}
pub fn add_contact(&mut self, contact: ContactPoint) {
const MERGE_RADIUS_SQ: f32 = 0.02 * 0.02;
for existing in &mut self.contacts {
if (existing.point - contact.point).length_squared() < MERGE_RADIUS_SQ {
let saved_normal = existing.normal_impulse;
let saved_tangent = existing.tangent_impulse;
*existing = contact;
existing.normal_impulse = saved_normal;
existing.tangent_impulse = saved_tangent;
return;
}
}
if self.contacts.len() < 4 {
self.contacts.push(contact);
return;
}
let mut pool = [ContactPoint::default(); 5];
pool[..4].copy_from_slice(&self.contacts);
pool[4] = contact;
self.contacts.clear();
self.contacts.extend_from_slice(&select_4_contacts(&pool));
}
pub fn clear(&mut self) {
self.contacts.clear();
}
pub fn is_stale(&self, max_lifetime: u32) -> bool {
self.lifetime > max_lifetime
}
}
fn select_4_contacts(pool: &[ContactPoint; 5]) -> [ContactPoint; 4] {
let i0 = (0..5)
.max_by(|&a, &b| pool[a].penetration.total_cmp(&pool[b].penetration))
.unwrap();
let p0 = pool[i0].point;
let i1 = (0..5)
.filter(|&i| i != i0)
.max_by(|&a, &b| {
(pool[a].point - p0)
.length_squared()
.total_cmp(&(pool[b].point - p0).length_squared())
})
.unwrap();
let p1 = pool[i1].point;
let seg = (p1 - p0).normalize_or_zero();
let i2 = (0..5)
.filter(|&i| i != i0 && i != i1)
.max_by(|&a, &b| {
dist_sq_to_line(pool[a].point, p0, seg).total_cmp(&dist_sq_to_line(
pool[b].point,
p0,
seg,
))
})
.unwrap();
let p2 = pool[i2].point;
let i3 = (0..5)
.filter(|&i| i != i0 && i != i1 && i != i2)
.max_by(|&a, &b| {
let score = |idx: usize| -> f32 {
let q = pool[idx].point;
(q - p0).cross(q - p1).length_squared()
+ (q - p1).cross(q - p2).length_squared()
+ (q - p2).cross(q - p0).length_squared()
};
score(a).total_cmp(&score(b))
})
.unwrap();
[pool[i0], pool[i1], pool[i2], pool[i3]]
}
#[inline]
fn dist_sq_to_line(point: Vec3, origin: Vec3, dir: Vec3) -> f32 {
let d = point - origin;
let along = dir * d.dot(dir);
(d - along).length_squared()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CollisionEventType {
Started,
Persisting,
Ended,
}
#[derive(Debug, Clone)]
pub struct CollisionEvent {
pub entity_a: Entity,
pub entity_b: Entity,
pub event_type: CollisionEventType,
pub contact_points: arrayvec::ArrayVec<ContactPoint, 4>,
}
#[derive(Debug, Clone)]
pub struct TriggerEvent {
pub trigger_entity: Entity,
pub other_entity: Entity,
pub event_type: CollisionEventType,
}
#[derive(Debug, Clone, Copy)]
pub struct FractureEvent {
pub entity: Entity,
pub impact_point: Vec3,
pub impact_force: f32,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entity(id: u32) -> Entity {
Entity::new(id, 0)
}
fn pt(x: f32, y: f32, pen: f32) -> ContactPoint {
ContactPoint {
point: Vec3::new(x, y, 0.0),
normal: Vec3::Y,
penetration: pen,
..Default::default()
}
}
#[test]
fn manifold_normalises_entity_order() {
let e_high = make_entity(10);
let e_low = make_entity(5);
let m = ContactManifold::new(e_high, e_low);
assert_eq!(m.entity_a.id(), 5);
assert_eq!(m.entity_b.id(), 10);
}
#[test]
fn manifold_same_order_when_already_sorted() {
let e1 = make_entity(1);
let e2 = make_entity(2);
let m = ContactManifold::new(e1, e2);
assert_eq!(m.entity_a.id(), 1);
assert_eq!(m.entity_b.id(), 2);
}
#[test]
fn warm_start_preserves_impulses_on_merge() {
let mut m = ContactManifold::new(make_entity(1), make_entity(2));
let mut first = pt(1.0, 0.0, 0.1);
first.normal_impulse = 5.0;
first.tangent_impulse = Vec3::new(1.0, 0.0, 0.0);
m.add_contact(first);
let updated = pt(1.001, 0.0, 0.2);
m.add_contact(updated);
assert_eq!(m.contacts.len(), 1, "near-duplicate should merge, not add");
assert_eq!(
m.contacts[0].normal_impulse, 5.0,
"accumulated normal impulse must be preserved"
);
assert_eq!(
m.contacts[0].tangent_impulse,
Vec3::new(1.0, 0.0, 0.0),
"accumulated tangent impulse must be preserved"
);
assert!(
(m.contacts[0].penetration - 0.2).abs() < 1e-6,
"geometry (penetration) must be updated"
);
}
#[test]
fn contact_limit_enforced_at_4() {
let mut m = ContactManifold::new(make_entity(1), make_entity(2));
m.add_contact(pt(0.0, 0.0, 1.0));
m.add_contact(pt(10.0, 0.0, 1.0));
m.add_contact(pt(0.0, 10.0, 1.0));
m.add_contact(pt(10.0, 10.0, 1.0));
assert_eq!(m.contacts.len(), 4);
m.add_contact(pt(0.5, 0.5, 0.1));
assert_eq!(m.contacts.len(), 4, "must stay at 4 contacts");
assert!(
!m.contacts
.iter()
.any(|c| (c.penetration - 0.1).abs() < 1e-6),
"shallowest near-duplicate contact should be dropped"
);
}
#[test]
fn deepest_contact_always_retained() {
let mut m = ContactManifold::new(make_entity(1), make_entity(2));
m.add_contact(pt(0.0, 0.0, 0.5));
m.add_contact(pt(1.0, 0.0, 0.5));
m.add_contact(pt(0.0, 1.0, 0.5));
m.add_contact(pt(1.0, 1.0, 0.5));
m.add_contact(pt(0.5, 0.5, 99.0));
assert!(
m.contacts
.iter()
.any(|c| (c.penetration - 99.0).abs() < 1e-6),
"deepest contact must always be retained"
);
}
#[test]
fn is_stale_respects_lifetime() {
let mut m = ContactManifold::new(make_entity(1), make_entity(2));
assert!(!m.is_stale(3));
m.lifetime = 4;
assert!(m.is_stale(3));
m.lifetime = 3;
assert!(!m.is_stale(3));
}
#[test]
fn clear_removes_contacts_but_not_lifetime() {
let mut m = ContactManifold::new(make_entity(1), make_entity(2));
m.add_contact(pt(0.0, 0.0, 1.0));
m.lifetime = 7;
m.clear();
assert!(m.contacts.is_empty(), "contacts should be cleared");
assert_eq!(m.lifetime, 7, "lifetime must not be touched by clear()");
}
#[test]
fn select_4_keeps_deepest_and_maximises_spread() {
let pool = [
pt(0.0, 0.0, 1.0),
pt(10.0, 0.0, 1.0),
pt(0.0, 10.0, 1.0),
pt(10.0, 10.0, 1.0),
pt(5.0, 5.0, 5.0), ];
let result = select_4_contacts(&pool);
assert!(
result.iter().any(|c| (c.penetration - 5.0).abs() < 1e-6),
"deepest point must be selected"
);
for i in 0..4 {
for j in (i + 1)..4 {
assert_ne!(
result[i].point, result[j].point,
"selected contacts must be distinct"
);
}
}
}
}