#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
#[inline]
fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn vec3_scale(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[inline]
fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn vec3_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]
fn vec3_len_sq(v: [f64; 3]) -> f64 {
vec3_dot(v, v)
}
#[inline]
fn vec3_len(v: [f64; 3]) -> f64 {
vec3_len_sq(v).sqrt()
}
#[inline]
fn vec3_normalize(v: [f64; 3]) -> Option<[f64; 3]> {
let len = vec3_len(v);
if len < 1e-14 {
None
} else {
Some(vec3_scale(v, 1.0 / len))
}
}
#[inline]
fn vec3_neg(v: [f64; 3]) -> [f64; 3] {
[-v[0], -v[1], -v[2]]
}
#[derive(Debug, Clone, Copy)]
pub struct SoftBodyAabb {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl SoftBodyAabb {
pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
Self { min, max }
}
pub fn empty() -> Self {
Self {
min: [f64::INFINITY; 3],
max: [f64::NEG_INFINITY; 3],
}
}
pub fn refit(&mut self, positions: &[[f64; 3]]) {
*self = Self::empty();
for &p in positions {
for i in 0..3 {
self.min[i] = self.min[i].min(p[i]);
self.max[i] = self.max[i].max(p[i]);
}
}
}
pub fn overlaps(&self, other: &SoftBodyAabb) -> bool {
for i in 0..3 {
if self.min[i] > other.max[i] || self.max[i] < other.min[i] {
return false;
}
}
true
}
pub fn contains_point(&self, point: [f64; 3]) -> bool {
(0..3).all(|i| point[i] >= self.min[i] && point[i] <= self.max[i])
}
pub fn expand(&self, margin: f64) -> Self {
Self {
min: [
self.min[0] - margin,
self.min[1] - margin,
self.min[2] - margin,
],
max: [
self.max[0] + margin,
self.max[1] + margin,
self.max[2] + margin,
],
}
}
pub fn center(&self) -> [f64; 3] {
[
(self.min[0] + self.max[0]) * 0.5,
(self.min[1] + self.max[1]) * 0.5,
(self.min[2] + self.max[2]) * 0.5,
]
}
pub fn half_extents(&self) -> [f64; 3] {
[
(self.max[0] - self.min[0]) * 0.5,
(self.max[1] - self.min[1]) * 0.5,
(self.max[2] - self.min[2]) * 0.5,
]
}
}
#[derive(Debug, Clone)]
pub struct TriangleMeshCollider {
pub triangles: Vec<[[f64; 3]; 3]>,
leaf_aabbs: Vec<SoftBodyAabb>,
}
impl TriangleMeshCollider {
pub fn new(triangles: Vec<[[f64; 3]; 3]>) -> Self {
let leaf_aabbs = triangles
.iter()
.map(|tri| {
let mut aabb = SoftBodyAabb::empty();
aabb.refit(tri);
aabb
})
.collect();
Self {
triangles,
leaf_aabbs,
}
}
pub fn num_triangles(&self) -> usize {
self.triangles.len()
}
pub fn broad_query(&self, query_aabb: &SoftBodyAabb) -> Vec<usize> {
self.leaf_aabbs
.iter()
.enumerate()
.filter(|(_, a)| a.overlaps(query_aabb))
.map(|(i, _)| i)
.collect()
}
pub fn total_aabb(&self) -> SoftBodyAabb {
let mut aabb = SoftBodyAabb::empty();
for tri in &self.triangles {
aabb.refit(tri);
}
aabb
}
}
#[derive(Debug, Clone)]
pub struct PointTriangleContact {
pub closest_point: [f64; 3],
pub barycentric: [f64; 3],
pub dist_sq: f64,
pub normal: [f64; 3],
}
impl PointTriangleContact {
pub fn compute(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Self {
let ab = vec3_sub(b, a);
let ac = vec3_sub(c, a);
let ap = vec3_sub(p, a);
let d1 = vec3_dot(ab, ap);
let d2 = vec3_dot(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
let bary = [1.0, 0.0, 0.0];
let closest = a;
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let bp = vec3_sub(p, b);
let d3 = vec3_dot(ab, bp);
let d4 = vec3_dot(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
let bary = [0.0, 1.0, 0.0];
let closest = b;
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
let bary = [1.0 - v, v, 0.0];
let closest = vec3_add(a, vec3_scale(ab, v));
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let cp = vec3_sub(p, c);
let d5 = vec3_dot(ab, cp);
let d6 = vec3_dot(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
let bary = [0.0, 0.0, 1.0];
let closest = c;
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
let bary = [1.0 - w, 0.0, w];
let closest = vec3_add(a, vec3_scale(ac, w));
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
let bary = [0.0, 1.0 - w, w];
let closest = vec3_add(b, vec3_scale(vec3_sub(c, b), w));
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
return Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
};
}
let denom = 1.0 / (va + vb + vc);
let v = vb * denom;
let w = vc * denom;
let u = 1.0 - v - w;
let bary = [u, v, w];
let closest = [
a[0] + ab[0] * v + ac[0] * w,
a[1] + ab[1] * v + ac[1] * w,
a[2] + ab[2] * v + ac[2] * w,
];
let diff = vec3_sub(p, closest);
let normal = vec3_normalize(vec3_cross(ab, ac)).unwrap_or([0.0, 1.0, 0.0]);
Self {
closest_point: closest,
barycentric: bary,
dist_sq: vec3_len_sq(diff),
normal,
}
}
pub fn signed_depth(&self, point: [f64; 3]) -> f64 {
let to_point = vec3_sub(point, self.closest_point);
vec3_dot(to_point, self.normal)
}
}
#[derive(Debug, Clone)]
pub struct EdgeEdgeContact {
pub t1: f64,
pub t2: f64,
pub point1: [f64; 3],
pub point2: [f64; 3],
pub dist_sq: f64,
}
impl EdgeEdgeContact {
pub fn compute(p1: [f64; 3], p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Self {
let d1 = vec3_sub(p2, p1);
let d2 = vec3_sub(p4, p3);
let r = vec3_sub(p1, p3);
let a = vec3_dot(d1, d1);
let e = vec3_dot(d2, d2);
let f = vec3_dot(d2, r);
let (t1, t2) = if a <= 1e-14 && e <= 1e-14 {
(0.0, 0.0)
} else if a <= 1e-14 {
(0.0, (f / e).clamp(0.0, 1.0))
} else {
let c = vec3_dot(d1, r);
if e <= 1e-14 {
((-c / a).clamp(0.0, 1.0), 0.0)
} else {
let b = vec3_dot(d1, d2);
let denom = a * e - b * b;
let s = if denom.abs() > 1e-14 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let t = (b * s + f) / e;
let (t_clamped, s_adjusted) = if t < 0.0 {
(0.0, (-c / a).clamp(0.0, 1.0))
} else if t > 1.0 {
(1.0, ((b - c) / a).clamp(0.0, 1.0))
} else {
(t, s)
};
(s_adjusted, t_clamped)
}
};
let point1 = vec3_add(p1, vec3_scale(d1, t1));
let point2 = vec3_add(p3, vec3_scale(d2, t2));
let diff = vec3_sub(point1, point2);
Self {
t1,
t2,
point1,
point2,
dist_sq: vec3_len_sq(diff),
}
}
}
#[derive(Debug, Clone)]
pub struct SoftBodyContact {
pub contact_point: [f64; 3],
pub normal: [f64; 3],
pub penetration_depth: f64,
pub particle_index: usize,
pub triangle_index: usize,
}
impl SoftBodyContact {
pub fn new(
contact_point: [f64; 3],
normal: [f64; 3],
penetration_depth: f64,
particle_index: usize,
triangle_index: usize,
) -> Self {
Self {
contact_point,
normal,
penetration_depth,
particle_index,
triangle_index,
}
}
pub fn is_penetrating(&self, threshold: f64) -> bool {
self.penetration_depth > threshold
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResponseMode {
Penalty,
Impulse,
}
#[derive(Debug, Clone)]
pub struct CollisionResponse {
pub mode: ResponseMode,
pub penalty_stiffness: f64,
pub restitution: f64,
}
impl CollisionResponse {
pub fn penalty(stiffness: f64) -> Self {
Self {
mode: ResponseMode::Penalty,
penalty_stiffness: stiffness,
restitution: 0.0,
}
}
pub fn impulse(restitution: f64) -> Self {
Self {
mode: ResponseMode::Impulse,
penalty_stiffness: 0.0,
restitution: restitution.clamp(0.0, 1.0),
}
}
pub fn penalty_force(&self, contact: &SoftBodyContact) -> [f64; 3] {
let mag = self.penalty_stiffness * contact.penetration_depth;
vec3_scale(contact.normal, mag)
}
pub fn position_correction(&self, contact: &SoftBodyContact) -> [f64; 3] {
vec3_scale(contact.normal, contact.penetration_depth)
}
pub fn resolve(
&self,
position: [f64; 3],
velocity: [f64; 3],
contact: &SoftBodyContact,
) -> ([f64; 3], [f64; 3]) {
if !contact.is_penetrating(0.0) {
return (position, velocity);
}
let new_pos = vec3_add(position, self.position_correction(contact));
let new_vel = match self.mode {
ResponseMode::Penalty => velocity,
ResponseMode::Impulse => {
let vn = vec3_dot(velocity, contact.normal);
if vn < 0.0 {
let impulse = -(1.0 + self.restitution) * vn;
vec3_add(velocity, vec3_scale(contact.normal, impulse))
} else {
velocity
}
}
};
(new_pos, new_vel)
}
}
#[derive(Debug, Clone)]
pub struct SelfCollision {
adjacency: Vec<Vec<usize>>,
pub thickness: f64,
}
impl SelfCollision {
pub fn new(num_particles: usize, triangles: &[[usize; 3]], thickness: f64) -> Self {
let mut adjacency = vec![Vec::new(); num_particles];
for (ti, tri) in triangles.iter().enumerate() {
for &vi in tri {
adjacency[vi].push(ti);
}
}
Self {
adjacency,
thickness,
}
}
fn is_adjacent(&self, particle_idx: usize, tri_idx: usize) -> bool {
self.adjacency
.get(particle_idx)
.map(|adj| adj.contains(&tri_idx))
.unwrap_or(false)
}
pub fn detect(&self, positions: &[[f64; 3]], triangles: &[[usize; 3]]) -> Vec<SoftBodyContact> {
let mut contacts = Vec::new();
for (pi, &pos) in positions.iter().enumerate() {
for (ti, tri) in triangles.iter().enumerate() {
if self.is_adjacent(pi, ti) {
continue;
}
let a = positions[tri[0]];
let b = positions[tri[1]];
let c = positions[tri[2]];
let result = PointTriangleContact::compute(pos, a, b, c);
let dist = result.dist_sq.sqrt();
if dist < self.thickness {
let depth = self.thickness - dist;
contacts.push(SoftBodyContact::new(
result.closest_point,
result.normal,
depth,
pi,
ti,
));
}
}
}
contacts
}
}
#[derive(Debug, Clone)]
pub struct FrictionResponse {
pub mu_static: f64,
pub mu_kinetic: f64,
}
impl FrictionResponse {
pub fn new(mu_static: f64, mu_kinetic: f64) -> Self {
Self {
mu_static,
mu_kinetic,
}
}
pub fn apply(&self, velocity: [f64; 3], normal: [f64; 3]) -> [f64; 3] {
let vn_mag = vec3_dot(velocity, normal);
let v_normal = vec3_scale(normal, vn_mag);
let v_tangent = vec3_sub(velocity, v_normal);
let vt_len = vec3_len(v_tangent);
if vt_len < 1e-14 {
return velocity;
}
let friction_limit = self.mu_kinetic * vn_mag.abs();
if vt_len <= friction_limit {
v_normal
} else {
let scale = (vt_len - friction_limit) / vt_len;
vec3_add(v_normal, vec3_scale(v_tangent, scale))
}
}
pub fn tangential_impulse(&self, normal_impulse: f64) -> f64 {
self.mu_kinetic * normal_impulse.abs()
}
}
#[derive(Debug, Clone)]
pub struct ContinuousCollision;
impl ContinuousCollision {
pub fn particle_vs_static_triangle(
p0: [f64; 3],
p1: [f64; 3],
a: [f64; 3],
b: [f64; 3],
c: [f64; 3],
) -> Option<f64> {
let ab = vec3_sub(b, a);
let ac = vec3_sub(c, a);
let n = vec3_normalize(vec3_cross(ab, ac))?;
let d = vec3_dot(n, a);
let d0 = vec3_dot(n, p0);
let d1 = vec3_dot(n, p1);
let denom = d1 - d0;
if denom.abs() < 1e-14 {
return None;
}
let t = (d - d0) / denom;
if !(0.0..=1.0).contains(&t) {
return None;
}
let dp = vec3_sub(p1, p0);
let impact = vec3_add(p0, vec3_scale(dp, t));
let ptc = PointTriangleContact::compute(impact, a, b, c);
if ptc.dist_sq < 1e-10 { Some(t) } else { None }
}
pub fn sphere_vs_plane(
p0: [f64; 3],
p1: [f64; 3],
plane_normal: [f64; 3],
plane_d: f64,
radius: f64,
) -> Option<f64> {
let h0 = vec3_dot(plane_normal, p0) - plane_d - radius;
let h1 = vec3_dot(plane_normal, p1) - plane_d - radius;
if h0 <= 0.0 {
return Some(0.0); }
if h1 >= h0 {
return None; }
let t = h0 / (h0 - h1);
if (0.0..=1.0).contains(&t) {
Some(t)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CollisionFilter {
pub group_mask: u32,
pub collides_with: u32,
}
impl CollisionFilter {
pub fn new(group_mask: u32, collides_with: u32) -> Self {
Self {
group_mask,
collides_with,
}
}
pub fn group(g: u32) -> Self {
Self {
group_mask: g,
collides_with: u32::MAX,
}
}
pub fn should_collide(&self, other: &CollisionFilter) -> bool {
(self.collides_with & other.group_mask) != 0 && (other.collides_with & self.group_mask) != 0
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_aabb_empty_state() {
let aabb = SoftBodyAabb::empty();
assert!(aabb.min[0].is_infinite() && aabb.min[0] > 0.0);
}
#[test]
fn test_aabb_refit_single_point() {
let mut aabb = SoftBodyAabb::empty();
aabb.refit(&[[1.0, 2.0, 3.0]]);
assert!((aabb.min[0] - 1.0).abs() < EPS);
assert!((aabb.max[2] - 3.0).abs() < EPS);
}
#[test]
fn test_aabb_refit_multiple_points() {
let mut aabb = SoftBodyAabb::empty();
aabb.refit(&[[-1.0, 0.0, 0.0], [1.0, 2.0, -3.0]]);
assert!((aabb.min[0] - (-1.0)).abs() < EPS);
assert!((aabb.max[1] - 2.0).abs() < EPS);
assert!((aabb.min[2] - (-3.0)).abs() < EPS);
}
#[test]
fn test_aabb_overlaps_true() {
let a = SoftBodyAabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let b = SoftBodyAabb::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0]);
assert!(a.overlaps(&b));
}
#[test]
fn test_aabb_overlaps_false() {
let a = SoftBodyAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = SoftBodyAabb::new([2.0, 0.0, 0.0], [3.0, 1.0, 1.0]);
assert!(!a.overlaps(&b));
}
#[test]
fn test_aabb_contains_point() {
let aabb = SoftBodyAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
assert!(aabb.contains_point([0.5, 0.5, 0.5]));
assert!(!aabb.contains_point([1.5, 0.5, 0.5]));
}
#[test]
fn test_aabb_expand() {
let aabb = SoftBodyAabb::new([0.0; 3], [1.0; 3]);
let expanded = aabb.expand(0.5);
assert!((expanded.min[0] - (-0.5)).abs() < EPS);
assert!((expanded.max[0] - 1.5).abs() < EPS);
}
#[test]
fn test_aabb_center() {
let aabb = SoftBodyAabb::new([0.0; 3], [2.0; 3]);
let c = aabb.center();
assert!((c[0] - 1.0).abs() < EPS);
}
#[test]
fn test_aabb_half_extents() {
let aabb = SoftBodyAabb::new([0.0; 3], [4.0; 3]);
let he = aabb.half_extents();
assert!((he[0] - 2.0).abs() < EPS);
}
#[test]
fn test_mesh_collider_num_triangles() {
let tris = vec![
[[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
];
let c = TriangleMeshCollider::new(tris);
assert_eq!(c.num_triangles(), 2);
}
#[test]
fn test_mesh_collider_broad_query_finds_overlapping() {
let tris = vec![[[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]];
let c = TriangleMeshCollider::new(tris);
let query = SoftBodyAabb::new([-0.1; 3], [0.5; 3]);
let hits = c.broad_query(&query);
assert!(!hits.is_empty());
}
#[test]
fn test_mesh_collider_broad_query_misses_non_overlapping() {
let tris = vec![[[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]];
let c = TriangleMeshCollider::new(tris);
let query = SoftBodyAabb::new([5.0; 3], [6.0; 3]);
let hits = c.broad_query(&query);
assert!(hits.is_empty());
}
#[test]
fn test_mesh_collider_total_aabb() {
let tris = vec![[[0.0; 3], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]]];
let c = TriangleMeshCollider::new(tris);
let aabb = c.total_aabb();
assert!((aabb.max[0] - 3.0).abs() < EPS);
}
#[test]
fn test_point_triangle_above_center() {
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 p = [0.25, 0.25, 1.0];
let res = PointTriangleContact::compute(p, a, b, c);
let bary = res.barycentric;
assert!(bary[0] >= -EPS && bary[1] >= -EPS && bary[2] >= -EPS);
assert!((res.dist_sq.sqrt() - 1.0).abs() < 1e-6);
}
#[test]
fn test_point_triangle_on_surface() {
let a = [0.0, 0.0, 0.0];
let b = [2.0, 0.0, 0.0];
let c = [0.0, 2.0, 0.0];
let p = [0.5, 0.5, 0.0]; let res = PointTriangleContact::compute(p, a, b, c);
assert!(
res.dist_sq < 1e-10,
"point on triangle should have dist_sq≈0"
);
}
#[test]
fn test_point_triangle_closest_is_vertex() {
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 p = [-1.0, -1.0, 0.0]; let res = PointTriangleContact::compute(p, a, b, c);
let da = vec3_len(vec3_sub(res.closest_point, a));
assert!(da < 1e-6, "closest point should be vertex a");
}
#[test]
fn test_point_triangle_normal_nonzero() {
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 p = [0.25, 0.25, 1.0];
let res = PointTriangleContact::compute(p, a, b, c);
let n_len = vec3_len(res.normal);
assert!((n_len - 1.0).abs() < 1e-6, "normal should be unit length");
}
#[test]
fn test_point_triangle_signed_depth_positive_above() {
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 p = [0.25, 0.25, 1.0];
let res = PointTriangleContact::compute(p, a, b, c);
let depth = res.signed_depth(p);
assert!(depth > 0.0, "point above plane should have positive depth");
}
#[test]
fn test_edge_edge_parallel() {
let p1 = [0.0, 0.0, 0.0];
let p2 = [1.0, 0.0, 0.0];
let p3 = [0.0, 1.0, 0.0];
let p4 = [1.0, 1.0, 0.0];
let res = EdgeEdgeContact::compute(p1, p2, p3, p4);
assert!((res.dist_sq.sqrt() - 1.0).abs() < 1e-6);
}
#[test]
fn test_edge_edge_perpendicular() {
let p1 = [0.0, 0.0, 0.0];
let p2 = [1.0, 0.0, 0.0];
let p3 = [0.5, -1.0, 0.0];
let p4 = [0.5, 1.0, 0.0]; let res = EdgeEdgeContact::compute(p1, p2, p3, p4);
assert!(res.dist_sq < 1e-10, "crossing edges have dist=0");
}
#[test]
fn test_edge_edge_t_in_range() {
let p1 = [0.0, 0.0, 0.0];
let p2 = [1.0, 0.0, 0.0];
let p3 = [0.0, 1.0, 0.0];
let p4 = [1.0, 1.0, 0.0];
let res = EdgeEdgeContact::compute(p1, p2, p3, p4);
assert!((0.0..=1.0).contains(&res.t1));
assert!((0.0..=1.0).contains(&res.t2));
}
#[test]
fn test_soft_body_contact_is_penetrating() {
let c = SoftBodyContact::new([0.0; 3], [0.0, 1.0, 0.0], 0.05, 0, 0);
assert!(c.is_penetrating(0.01));
assert!(!c.is_penetrating(0.1));
}
#[test]
fn test_penalty_force_direction() {
let resp = CollisionResponse::penalty(1000.0);
let contact = SoftBodyContact::new([0.0; 3], [0.0, 1.0, 0.0], 0.01, 0, 0);
let force = resp.penalty_force(&contact);
assert!(force[1] > 0.0);
}
#[test]
fn test_position_correction_moves_particle_out() {
let resp = CollisionResponse::penalty(1000.0);
let contact = SoftBodyContact::new([0.0; 3], [0.0, 1.0, 0.0], 0.1, 0, 0);
let correction = resp.position_correction(&contact);
assert!((correction[1] - 0.1).abs() < EPS);
}
#[test]
fn test_impulse_response_reflects_velocity() {
let resp = CollisionResponse::impulse(1.0); let contact = SoftBodyContact::new([0.0; 3], [0.0, 1.0, 0.0], 0.01, 0, 0);
let vel = [0.0, -5.0, 0.0];
let (_pos, new_vel) = resp.resolve([0.0; 3], vel, &contact);
assert!(
new_vel[1] > 0.0,
"velocity should be reflected, got {new_vel:?}"
);
}
#[test]
fn test_impulse_inelastic_absorbs_velocity() {
let resp = CollisionResponse::impulse(0.0); let contact = SoftBodyContact::new([0.0; 3], [0.0, 1.0, 0.0], 0.01, 0, 0);
let vel = [0.0, -5.0, 0.0];
let (_pos, new_vel) = resp.resolve([0.0; 3], vel, &contact);
assert!(
new_vel[1].abs() < EPS,
"inelastic: vy should be 0, got {}",
new_vel[1]
);
}
#[test]
fn test_friction_zero_tangential_no_change() {
let fr = FrictionResponse::new(0.5, 0.4);
let vel = [0.0, 5.0, 0.0];
let normal = [0.0, 1.0, 0.0];
let result = fr.apply(vel, normal);
assert!((result[0]).abs() < EPS && (result[2]).abs() < EPS);
}
#[test]
fn test_friction_reduces_tangential_velocity() {
let fr = FrictionResponse::new(0.5, 0.4);
let vel = [10.0, 1.0, 0.0];
let normal = [0.0, 1.0, 0.0];
let result = fr.apply(vel, normal);
let vt_before = (vel[0] * vel[0] + vel[2] * vel[2]).sqrt();
let vt_after = (result[0] * result[0] + result[2] * result[2]).sqrt();
assert!(
vt_after <= vt_before + EPS,
"friction should reduce tangential speed"
);
}
#[test]
fn test_friction_tangential_impulse_scales_with_normal() {
let fr = FrictionResponse::new(0.5, 0.3);
let j1 = fr.tangential_impulse(10.0);
let j2 = fr.tangential_impulse(20.0);
assert!((j2 - 2.0 * j1).abs() < EPS);
}
#[test]
fn test_ccd_particle_hits_triangle() {
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 p0 = [0.25, 0.25, 1.0]; let p1 = [0.25, 0.25, -1.0]; let toi = ContinuousCollision::particle_vs_static_triangle(p0, p1, a, b, c);
assert!(
toi.is_some(),
"particle sweeping through triangle should hit"
);
let t = toi.unwrap();
assert!((0.0..=1.0).contains(&t));
}
#[test]
fn test_ccd_particle_misses_triangle() {
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 p0 = [5.0, 5.0, 1.0];
let p1 = [5.0, 5.0, -1.0];
let toi = ContinuousCollision::particle_vs_static_triangle(p0, p1, a, b, c);
assert!(toi.is_none(), "particle outside triangle should miss");
}
#[test]
fn test_ccd_sphere_hits_plane() {
let p0 = [0.0, 2.0, 0.0];
let p1 = [0.0, -2.0, 0.0];
let n = [0.0, 1.0, 0.0];
let toi = ContinuousCollision::sphere_vs_plane(p0, p1, n, 0.0, 0.1);
assert!(toi.is_some());
let t = toi.unwrap();
let y = p0[1] + (p1[1] - p0[1]) * t;
assert!(
(y - 0.1).abs() < 1e-6,
"sphere should just touch plane, y={y}"
);
}
#[test]
fn test_ccd_sphere_misses_plane_moving_away() {
let p0 = [0.0, 2.0, 0.0];
let p1 = [0.0, 4.0, 0.0]; let n = [0.0, 1.0, 0.0];
let toi = ContinuousCollision::sphere_vs_plane(p0, p1, n, 0.0, 0.1);
assert!(toi.is_none(), "sphere moving away should not hit");
}
#[test]
fn test_collision_filter_same_group_collides() {
let a = CollisionFilter::group(0b01);
let b = CollisionFilter::group(0b01);
assert!(a.should_collide(&b));
}
#[test]
fn test_collision_filter_different_groups_no_collision() {
let a = CollisionFilter::new(0b01, 0b10); let b = CollisionFilter::new(0b100, 0b001); assert!(!a.should_collide(&b));
}
#[test]
fn test_collision_filter_bilateral() {
let a = CollisionFilter::new(0b01, 0b10);
let b = CollisionFilter::new(0b10, 0b01);
assert!(a.should_collide(&b));
assert!(b.should_collide(&a));
}
#[test]
fn test_collision_filter_self_exclusion() {
let a = CollisionFilter::new(0b01, 0b10); let a2 = CollisionFilter::new(0b01, 0b10);
assert!(!a.should_collide(&a2));
}
#[test]
fn test_collision_filter_all_groups() {
let a = CollisionFilter::new(0b0001, u32::MAX);
let b = CollisionFilter::new(0b1000, u32::MAX);
assert!(a.should_collide(&b));
}
#[test]
fn test_self_collision_adjacent_skipped() {
let triangles = [[0, 1, 2_usize], [1, 3, 2]];
let sc = SelfCollision::new(4, &triangles, 0.1);
let positions = [
[0.0_f64, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[1.5, 1.0, 0.0],
];
let contacts = sc.detect(&positions, &triangles);
for c in &contacts {
if c.particle_index == 0 {
assert_ne!(c.triangle_index, 0, "adjacent triangle must be skipped");
}
}
}
#[test]
fn test_self_collision_detect_penetration() {
let triangles = [[0, 1, 2_usize], [1, 3, 2]];
let mut positions = [
[0.0_f64, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[1.5, 1.0, 0.0],
];
let sc = SelfCollision::new(5, &triangles, 0.2);
positions[0][2] = 0.0; let contacts = sc.detect(&positions, &triangles);
let _ = contacts; }
}