mod primitive_impls;
use super::{BoundingVolume, IntersectsVolume};
use crate::prelude::{Mat2, Rot2, Vec2};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
#[inline(always)]
fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 {
assert!(
!points.is_empty(),
"cannot compute the center of an empty set of points"
);
let denom = 1.0 / points.len() as f32;
points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom
}
pub trait Bounded2d {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d;
fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle;
}
#[doc(alias = "BoundingRectangle")]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct Aabb2d {
pub min: Vec2,
pub max: Vec2,
}
impl Aabb2d {
#[inline(always)]
pub fn new(center: Vec2, half_size: Vec2) -> Self {
debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0);
Self {
min: center - half_size,
max: center + half_size,
}
}
#[inline(always)]
pub fn from_point_cloud(
translation: Vec2,
rotation: impl Into<Rot2>,
points: &[Vec2],
) -> Aabb2d {
let rotation: Rot2 = rotation.into();
let mut iter = points.iter().map(|point| rotation * *point);
let first = iter
.next()
.expect("point cloud must contain at least one point for Aabb2d construction");
let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {
(point.min(prev_min), point.max(prev_max))
});
Aabb2d {
min: min + translation,
max: max + translation,
}
}
#[inline(always)]
pub fn bounding_circle(&self) -> BoundingCircle {
let radius = self.min.distance(self.max) / 2.0;
BoundingCircle::new(self.center(), radius)
}
#[inline(always)]
pub fn closest_point(&self, point: Vec2) -> Vec2 {
point.clamp(self.min, self.max)
}
}
impl BoundingVolume for Aabb2d {
type Translation = Vec2;
type Rotation = Rot2;
type HalfSize = Vec2;
#[inline(always)]
fn center(&self) -> Self::Translation {
(self.min + self.max) / 2.
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
(self.max - self.min) / 2.
}
#[inline(always)]
fn visible_area(&self) -> f32 {
let b = self.max - self.min;
b.x * b.y
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
other.min.x >= self.min.x
&& other.min.y >= self.min.y
&& other.max.x <= self.max.x
&& other.max.y <= self.max.y
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
Self {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
#[inline(always)]
fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
let b = Self {
min: self.min - amount,
max: self.max + amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
#[inline(always)]
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
let b = Self {
min: self.min + amount,
max: self.max - amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
#[inline(always)]
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
let b = Self {
min: self.center() - (self.half_size() * scale),
max: self.center() + (self.half_size() * scale),
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
#[inline(always)]
fn transformed_by(
mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) -> Self {
self.transform_by(translation, rotation);
self
}
#[inline(always)]
fn transform_by(
&mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) {
self.rotate_by(rotation);
self.translate_by(translation);
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
let translation = translation.into();
self.min += translation;
self.max += translation;
}
#[inline(always)]
fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
self.rotate_by(rotation);
self
}
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rotation: Rot2 = rotation.into();
let abs_rot_mat = Mat2::from_cols(
Vec2::new(rotation.cos, rotation.sin),
Vec2::new(rotation.sin, rotation.cos),
);
let half_size = abs_rot_mat * self.half_size();
*self = Self::new(rotation * self.center(), half_size);
}
}
impl IntersectsVolume<Self> for Aabb2d {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
let x_overlaps = self.min.x <= other.max.x && self.max.x >= other.min.x;
let y_overlaps = self.min.y <= other.max.y && self.max.y >= other.min.y;
x_overlaps && y_overlaps
}
}
impl IntersectsVolume<BoundingCircle> for Aabb2d {
#[inline(always)]
fn intersects(&self, circle: &BoundingCircle) -> bool {
let closest_point = self.closest_point(circle.center);
let distance_squared = circle.center.distance_squared(closest_point);
let radius_squared = circle.radius().powi(2);
distance_squared <= radius_squared
}
}
#[cfg(test)]
mod aabb2d_tests {
use super::Aabb2d;
use crate::{
bounding::{BoundingCircle, BoundingVolume, IntersectsVolume},
Vec2,
};
#[test]
fn center() {
let aabb = Aabb2d {
min: Vec2::new(-0.5, -1.),
max: Vec2::new(1., 1.),
};
assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(5., -10.),
max: Vec2::new(10., -5.),
};
assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < f32::EPSILON);
}
#[test]
fn half_size() {
let aabb = Aabb2d {
min: Vec2::new(-0.5, -1.),
max: Vec2::new(1., 1.),
};
let half_size = aabb.half_size();
assert!((half_size - Vec2::new(0.75, 1.)).length() < f32::EPSILON);
}
#[test]
fn area() {
let aabb = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(0., 0.),
max: Vec2::new(1., 0.5),
};
assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn contains() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
let b = Aabb2d {
min: Vec2::new(-2., -1.),
max: Vec2::new(1., 1.),
};
assert!(!a.contains(&b));
let b = Aabb2d {
min: Vec2::new(-0.25, -0.8),
max: Vec2::new(1., 1.),
};
assert!(a.contains(&b));
}
#[test]
fn merge() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 0.5),
};
let b = Aabb2d {
min: Vec2::new(-2., -0.5),
max: Vec2::new(0.75, 1.),
};
let merged = a.merge(&b);
assert!((merged.min - Vec2::new(-2., -1.)).length() < f32::EPSILON);
assert!((merged.max - Vec2::new(1., 1.)).length() < f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
}
#[test]
fn grow() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
let padded = a.grow(Vec2::ONE);
assert!((padded.min - Vec2::new(-2., -2.)).length() < f32::EPSILON);
assert!((padded.max - Vec2::new(2., 2.)).length() < f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = Aabb2d {
min: Vec2::new(-2., -2.),
max: Vec2::new(2., 2.),
};
let shrunk = a.shrink(Vec2::ONE);
assert!((shrunk.min - Vec2::new(-1., -1.)).length() < f32::EPSILON);
assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
let scaled = a.scale_around_center(Vec2::splat(2.));
assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON);
assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn transform() {
let a = Aabb2d {
min: Vec2::new(-2.0, -2.0),
max: Vec2::new(2.0, 2.0),
};
let transformed = a.transformed_by(Vec2::new(2.0, -2.0), std::f32::consts::FRAC_PI_4);
let half_length = 2_f32.hypot(2.0);
assert_eq!(
transformed.min,
Vec2::new(2.0 - half_length, -half_length - 2.0)
);
assert_eq!(
transformed.max,
Vec2::new(2.0 + half_length, half_length - 2.0)
);
}
#[test]
fn closest_point() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert_eq!(aabb.closest_point(Vec2::X * 10.0), Vec2::X);
assert_eq!(aabb.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE);
assert_eq!(
aabb.closest_point(Vec2::new(0.25, 0.1)),
Vec2::new(0.25, 0.1)
);
}
#[test]
fn intersect_aabb() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert!(aabb.intersects(&aabb));
assert!(aabb.intersects(&Aabb2d {
min: Vec2::new(0.5, 0.5),
max: Vec2::new(2.0, 2.0),
}));
assert!(aabb.intersects(&Aabb2d {
min: Vec2::new(-2.0, -2.0),
max: Vec2::new(-0.5, -0.5),
}));
assert!(!aabb.intersects(&Aabb2d {
min: Vec2::new(1.1, 0.0),
max: Vec2::new(2.0, 0.5),
}));
}
#[test]
fn intersect_bounding_circle() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert!(aabb.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
assert!(aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
assert!(aabb.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.5, 1.0)));
assert!(!aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.75, 1.0)));
}
}
use crate::primitives::Circle;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct BoundingCircle {
pub center: Vec2,
pub circle: Circle,
}
impl BoundingCircle {
#[inline(always)]
pub fn new(center: Vec2, radius: f32) -> Self {
debug_assert!(radius >= 0.);
Self {
center,
circle: Circle { radius },
}
}
#[inline(always)]
pub fn from_point_cloud(
translation: Vec2,
rotation: impl Into<Rot2>,
points: &[Vec2],
) -> BoundingCircle {
let rotation: Rot2 = rotation.into();
let center = point_cloud_2d_center(points);
let mut radius_squared = 0.0;
for point in points {
let distance_squared = point.distance_squared(center);
if distance_squared > radius_squared {
radius_squared = distance_squared;
}
}
BoundingCircle::new(rotation * center + translation, radius_squared.sqrt())
}
#[inline(always)]
pub fn radius(&self) -> f32 {
self.circle.radius
}
#[inline(always)]
pub fn aabb_2d(&self) -> Aabb2d {
Aabb2d {
min: self.center - Vec2::splat(self.radius()),
max: self.center + Vec2::splat(self.radius()),
}
}
#[inline(always)]
pub fn closest_point(&self, point: Vec2) -> Vec2 {
self.circle.closest_point(point - self.center) + self.center
}
}
impl BoundingVolume for BoundingCircle {
type Translation = Vec2;
type Rotation = Rot2;
type HalfSize = f32;
#[inline(always)]
fn center(&self) -> Self::Translation {
self.center
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
self.radius()
}
#[inline(always)]
fn visible_area(&self) -> f32 {
std::f32::consts::PI * self.radius() * self.radius()
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff)
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
let diff = other.center - self.center;
let length = diff.length();
if self.radius() >= length + other.radius() {
return *self;
}
if other.radius() >= length + self.radius() {
return *other;
}
let dir = diff / length;
Self::new(
(self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
(length + self.radius() + other.radius()) / 2.,
)
}
#[inline(always)]
fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
Self::new(self.center, self.radius() + amount)
}
#[inline(always)]
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
debug_assert!(self.radius() >= amount);
Self::new(self.center, self.radius() - amount)
}
#[inline(always)]
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
debug_assert!(scale >= 0.);
Self::new(self.center, self.radius() * scale)
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
self.center += translation.into();
}
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rotation: Rot2 = rotation.into();
self.center = rotation * self.center;
}
}
impl IntersectsVolume<Self> for BoundingCircle {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
let center_distance_squared = self.center.distance_squared(other.center);
let radius_sum_squared = (self.radius() + other.radius()).powi(2);
center_distance_squared <= radius_sum_squared
}
}
impl IntersectsVolume<Aabb2d> for BoundingCircle {
#[inline(always)]
fn intersects(&self, aabb: &Aabb2d) -> bool {
aabb.intersects(self)
}
}
#[cfg(test)]
mod bounding_circle_tests {
use super::BoundingCircle;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
Vec2,
};
#[test]
fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.);
assert!((circle.visible_area() - 78.5398).abs() < 0.001);
}
#[test]
fn contains() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.);
assert!(!a.contains(&b));
let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5);
assert!(a.contains(&b));
}
#[test]
fn contains_identical() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
assert!(a.contains(&a));
}
#[test]
fn merge() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
let merged = a.merge(&b);
assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
let b = BoundingCircle::new(Vec2::ZERO, 3.);
assert!(a.contains(&b));
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
let b = BoundingCircle::new(Vec2::ONE, 6.);
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), b.radius());
}
#[test]
fn merge_identical() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let merged = a.merge(&a);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
}
#[test]
fn grow() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let scaled = a.scale_around_center(2.);
assert!((scaled.radius() - 10.).abs() < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn transform() {
let a = BoundingCircle::new(Vec2::ONE, 5.0);
let transformed = a.transformed_by(Vec2::new(2.0, -2.0), std::f32::consts::FRAC_PI_4);
assert_eq!(
transformed.center,
Vec2::new(2.0, std::f32::consts::SQRT_2 - 2.0)
);
assert_eq!(transformed.radius(), 5.0);
}
#[test]
fn closest_point() {
let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X);
assert_eq!(
circle.closest_point(Vec2::NEG_ONE * 10.0),
Vec2::NEG_ONE.normalize()
);
assert_eq!(
circle.closest_point(Vec2::new(0.25, 0.1)),
Vec2::new(0.25, 0.1)
);
}
#[test]
fn intersect_bounding_circle() {
let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
assert!(circle.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
assert!(circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.25, 1.0)));
assert!(circle.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.25, 1.0)));
assert!(!circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
}
}