use crate::physics_components::Transform2D;
use bevy::{math::Mat2, prelude::*};
mod aabb;
mod circle;
mod square;
mod capsule;
mod triangle;
pub use aabb::*;
pub use circle::*;
pub use square::*;
pub use capsule::*;
pub use triangle::*;
pub trait SAT {
fn aabb(&self, trans: &Transform2D) -> Aabb {
let (xmin, xmax) = self.project(trans, Vec2::X);
let (ymin, ymax) = self.project(trans, Vec2::Y);
let min = Vec2::new(xmin, ymin);
let max = Vec2::new(xmax, ymax);
let extents = (max - min) * 0.5;
let position = min + extents;
Aabb { extents, position }
}
fn get_normals(&self, trans: &Transform2D) -> Box<dyn Iterator<Item = Vec2> + '_>;
fn project(&self, trans: &Transform2D, normal: Vec2) -> (f32,f32);
fn get_closest_vertex(&self, trans: &Transform2D, vertex: Vec2) -> Vec2;
fn ray(&self, trans: &Transform2D, ray_origin: Vec2, ray_cast: Vec2) -> Option<f32>;
}
pub fn collide(a: &CollisionShape, trans_a: &Transform2D, b: &CollisionShape, trans_b: &Transform2D) -> Option<Vec2> {
if let CollisionShape::Multiple(v) = a {
let mut sum = Vec2::ZERO;
for s in v {
if let Some(c) = collide(s, trans_a, b, trans_b) {
sum += c;
}
}
if sum.length_squared() < 0.01 {
return None;
}
else {
return Some(sum);
}
}
if let CollisionShape::Multiple(v) = b {
let mut sum = Vec2::ZERO;
for s in v {
if let Some(c) = collide(a, trans_a, s, trans_b) {
sum += c;
}
}
if sum.length_squared() < 0.01 {
return None;
}
else {
return Some(sum);
}
}
let sat_a = a.sat();
let sat_b = b.sat();
match (sat_a, sat_b) {
(Some(a), Some(b)) => sat_normal(a, trans_a, b, trans_b),
(Some(a), None) => sat_special(a, trans_a, b, trans_b), (None, Some(b)) => sat_special(b, trans_b, a, trans_a).map(|c| -c), (None, None) => collide_special(a, trans_a, b, trans_b), }
}
fn sat_normal(a: &dyn SAT, ta: &Transform2D, b: &dyn SAT, tb: &Transform2D) -> Option<Vec2> {
let na = a.get_normals(ta);
let nb = b.get_normals(tb);
let mut minimal_dis = f32::INFINITY;
let mut minimal_n = Vec2::ZERO;
for n in na.chain(nb) {
let (mina, maxa) = a.project(ta, n);
let (minb, maxb) = b.project(tb, n);
if mina < maxb && minb < maxa {
let p1 = maxb - mina;
let p2 = minb - maxa;
let p = if p1.abs() < p2.abs() { p1 } else { p2 };
if p.abs() < minimal_dis.abs() {
minimal_dis = p;
minimal_n = n;
}
}
else {
return None;
}
}
Some(minimal_dis * minimal_n)
}
fn sat_special(a: &dyn SAT, ta: &Transform2D, b: &CollisionShape, tb: &Transform2D) -> Option<Vec2> {
let na = a.get_normals(ta);
let b_rot = Mat2::from_angle(tb.rotation());
let nb = match b {
CollisionShape::Circle(c) => {
let offset = b_rot * c.offset;
let v = a.get_closest_vertex(ta, tb.translation() + offset);
(tb.translation() + offset - v).normalize()
},
CollisionShape::Capsule(c) => {
let offset = b_rot * c.offset;
let v = a.get_closest_vertex(ta, tb.translation() + offset);
c.sat_normal(tb, v)
}
_ => panic!("Shouldn't happen, if this occur to you please report it as a bug(and how you got here)")
};
let mut minimal_dis = f32::INFINITY;
let mut minimal_n = Vec2::ZERO;
for n in na.chain([nb]) {
let (mina, maxa) = a.project(ta, n);
let (minb, maxb) = match b {
CollisionShape::Circle(c) => {
let center = tb.translation() + b_rot * c.offset;
let center = center.dot(n);
(center - c.radius, center + c.radius)
},
CollisionShape::Capsule(c) => c.project(tb, n),
_ => panic!("If you paniced here, something is REALLY wrong")
};
if mina < maxb && minb < maxa {
let p1 = maxb - mina;
let p2 = minb - maxa;
let p = if p1.abs() < p2.abs() { p1 } else { p2 };
if p.abs() < minimal_dis.abs() {
minimal_dis = p;
minimal_n = n;
}
}
else {
return None;
}
}
Some(minimal_dis * minimal_n)
}
fn collide_special(a: &CollisionShape, ta: &Transform2D, b: &CollisionShape, tb: &Transform2D) -> Option<Vec2> {
#[allow(clippy::enum_glob_use)]
use CollisionShape::*;
match (a, b) {
(Circle(a), Circle(b)) => {
let ac = ta.translation() + Mat2::from_angle(ta.rotation()) * a.offset;
let bc = tb.translation() + Mat2::from_angle(tb.rotation()) * b.offset;
let d = ac - bc;
let d_len = d.length();
if d_len < a.radius + b.radius {
Some((a.radius + b.radius - d_len) * (d / d_len))
}
else {
None
}
},
(Circle(a), Capsule(b)) => collide_circle_capsule(a, ta, b, tb),
(Capsule(a), Circle(b)) => collide_circle_capsule(b, tb, a, ta).map(|v| -v),
(Capsule(a), Capsule(b)) => {
let a_rot = Mat2::from_angle(ta.rotation());
let b_rot = Mat2::from_angle(tb.rotation());
let n1 = a_rot * Vec2::X;
let n2 = b_rot * Vec2::X;
let n3 = {
let b1 = b_rot * Vec2::new(0.0, b.half_height) + tb.translation() + b_rot * b.offset;
let b2 = b_rot * Vec2::new(0.0, -b.half_height) + tb.translation() + b_rot * b.offset;
let v = ta.translation() + a_rot * a.offset;
let d1 = b1 - v;
let d2 = b2 - v;
if d1.length_squared() < d2.length_squared() {
d1.normalize_or_zero()
}
else {
d2.normalize_or_zero()
}
};
let mut minimal_dis = f32::INFINITY;
let mut minimal_n = Vec2::ZERO;
for n in [n1,n2,n3] {
let (mina, maxa) = a.project(ta, n);
let (minb, maxb) = b.project(tb, n);
if mina < maxb && minb < maxa {
let p1 = maxb - mina;
let p2 = minb - maxa;
let p = if p1.abs() < p2.abs() { p1 } else { p2 };
if p.abs() < minimal_dis.abs() {
minimal_dis = p;
minimal_n = n;
}
}
else {
return None;
}
}
Some(minimal_dis * minimal_n)
},
_ => panic!("Something is missing, please report it on github(with the shapes used)"),
}
}
fn collide_circle_capsule(a: &Circle, ta: &Transform2D, b: &Capsule, tb: &Transform2D) -> Option<Vec2> {
let brot = Mat2::from_angle(tb.rotation());
let (ba, bb) = b.center_line(tb);
let acenter = ta.translation() + Mat2::from_angle(ta.rotation()) * a.offset;
let n = brot * Vec2::X;
let p = brot * Vec2::Y;
let bn = n.dot(ba); let bap = p.dot(ba);
let bbp = p.dot(bb);
let an = n.dot(acenter);
let ap = p.dot(acenter);
let bpmin = bap.min(bbp);
let bpmax = bap.max(bbp);
let dp = if ap > bpmax { ap - bpmax } else if ap < bpmin { ap - bpmin } else { 0.0 };
let dis = n * (an - bn) + p * dp;
let dis_n = dis.normalize();
let dis_l = dis.dot(dis_n);
if dis_l < (a.radius + b.radius) {
Some(dis_n * (a.radius + b.radius - dis_l))
} else {
None
}
}
#[derive(Component)]
pub enum CollisionShape {
Square(Square),
Triangle(Triangle),
Circle(Circle),
Capsule(Capsule),
Multiple(Vec<CollisionShape>),
Convex(Box<dyn SAT + Send + Sync>),
}
impl CollisionShape {
pub fn sat(&self) -> Option<&dyn SAT> {
match self {
CollisionShape::Square(s) => Some(s),
CollisionShape::Triangle(t) => Some(t),
CollisionShape::Circle(_) => None,
CollisionShape::Capsule(_) => None,
CollisionShape::Multiple(_) => None,
CollisionShape::Convex(s) => Some(s.as_ref())
}
}
pub fn aabb(&self, t: &Transform2D) -> Aabb {
if let Some(sat) = self.sat() {
sat.aabb(t)
}
else {
match self {
CollisionShape::Circle(c) => c.aabb(t),
CollisionShape::Capsule(c) => c.aabb(t),
CollisionShape::Multiple(v) => {
assert!(v.len() != 0, "CollisionShape::Multiple cannot be empty!");
let (mut min, mut max) = v[0].aabb(t).min_max();
for s in v.iter().skip(1) {
let (sn, sx) = s.aabb(t).min_max();
min = min.min(sn);
max = max.max(sx);
}
Aabb::from_min_max(min, max)
}
_ => panic!("Something is missing, please report on github(with the shape used)"),
}
}
}
pub fn ray(&self, trans: &Transform2D, ray_origin: Vec2, ray_cast: Vec2) -> Option<f32> {
if let Some(sat) = self.sat() {
sat.ray(trans, ray_origin, ray_cast)
}
else {
match self {
CollisionShape::Circle(c) => c.ray(trans, ray_origin, ray_cast),
CollisionShape::Capsule(c) => c.ray(trans, ray_origin, ray_cast),
CollisionShape::Multiple(v) => {
assert!(v.len() != 0, "CollisionShape::Multiple cannot be empty!");
let mut res = None;
for s in v {
if let Some(r) = s.ray(trans, ray_origin, ray_cast) {
if r < res.unwrap_or(f32::INFINITY) {
res = Some(r);
}
}
}
res
}
_ => panic!("Something is missing, please report on github(with the shape used)"),
}
}
}
}
impl Default for CollisionShape {
fn default() -> Self {
CollisionShape::Square(Square::default())
}
}
#[cfg(test)]
mod sat_tests {
use super::*;
use std::f32::consts::PI;
const EPSILON: f32 = 0.001;
#[test]
fn squares() {
let s1 = Square {
offset: Vec2::ZERO,
extents: Vec2::splat(1.0),
};
let t1 = Transform2D::new(
Vec2::ZERO,
0.0,
Vec2::splat(1.0),
);
let s2 = Square {
offset: Vec2::ZERO,
extents: Vec2::splat(1.0),
};
let t2 = Transform2D::new(
Vec2::new(1.5, 0.0),
0.0,
Vec2::splat(1.0),
);
let cs1 = CollisionShape::Square(s1);
let cs2 = CollisionShape::Square(s2);
assert_eq!(
collide(&cs1, &t1, &cs2, &t2),
Some(Vec2::new(-0.5, 0.0))
);
assert_eq!(
collide(&cs2, &t2, &cs1, &t1),
Some(Vec2::new(0.5,0.0))
);
}
#[test]
fn squares_rotation() {
let a = Square {
offset: Vec2::ZERO,
extents: Vec2::splat(1.0),
};
let ta = Transform2D::new(
Vec2::ZERO,
0.0,
Vec2::splat(1.0)
);
let b = Square {
offset: Vec2::ZERO,
extents: Vec2::splat(1.0),
};
let tb = Transform2D::new(
Vec2::new(2.0, 0.5),
PI * 0.25,
Vec2::splat(1.0),
);
let c = collide(&CollisionShape::Square(a), &ta, &CollisionShape::Square(b), &tb);
assert!((c.unwrap() + Vec2::new(2.0_f32.sqrt() - 1.0, 0.0)).length() < EPSILON);
}
}