Path: blob/main/crates/bevy_math/src/bounding/bounded2d/mod.rs
6598 views
mod primitive_impls;12use super::{BoundingVolume, IntersectsVolume};3use crate::{4ops,5prelude::{Mat2, Rot2, Vec2},6FloatPow, Isometry2d,7};89#[cfg(feature = "bevy_reflect")]10use bevy_reflect::Reflect;11#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]12use bevy_reflect::{ReflectDeserialize, ReflectSerialize};13#[cfg(feature = "serialize")]14use serde::{Deserialize, Serialize};1516/// Computes the geometric center of the given set of points.17#[inline(always)]18fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 {19assert!(20!points.is_empty(),21"cannot compute the center of an empty set of points"22);2324let denom = 1.0 / points.len() as f32;25points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom26}2728/// A trait with methods that return 2D bounding volumes for a shape.29pub trait Bounded2d {30/// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry.31fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d;32/// Get a bounding circle for the shape translated and rotated by the given isometry.33fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle;34}3536/// A 2D axis-aligned bounding box, or bounding rectangle37#[doc(alias = "BoundingRectangle")]38#[derive(Clone, Copy, Debug, PartialEq)]39#[cfg_attr(40feature = "bevy_reflect",41derive(Reflect),42reflect(Debug, PartialEq, Clone)43)]44#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]45#[cfg_attr(46all(feature = "serialize", feature = "bevy_reflect"),47reflect(Serialize, Deserialize)48)]49pub struct Aabb2d {50/// The minimum, conventionally bottom-left, point of the box51pub min: Vec2,52/// The maximum, conventionally top-right, point of the box53pub max: Vec2,54}5556impl Aabb2d {57/// Constructs an AABB from its center and half-size.58#[inline(always)]59pub fn new(center: Vec2, half_size: Vec2) -> Self {60debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0);61Self {62min: center - half_size,63max: center + half_size,64}65}6667/// Computes the smallest [`Aabb2d`] containing the given set of points,68/// transformed by the rotation and translation of the given isometry.69///70/// # Panics71///72/// Panics if the given set of points is empty.73#[inline(always)]74pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> Aabb2d {75let isometry = isometry.into();7677// Transform all points by rotation78let mut iter = points.iter().map(|point| isometry.rotation * *point);7980let first = iter81.next()82.expect("point cloud must contain at least one point for Aabb2d construction");8384let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {85(point.min(prev_min), point.max(prev_max))86});8788Aabb2d {89min: min + isometry.translation,90max: max + isometry.translation,91}92}9394/// Computes the smallest [`BoundingCircle`] containing this [`Aabb2d`].95#[inline(always)]96pub fn bounding_circle(&self) -> BoundingCircle {97let radius = self.min.distance(self.max) / 2.0;98BoundingCircle::new(self.center(), radius)99}100101/// Finds the point on the AABB that is closest to the given `point`.102///103/// If the point is outside the AABB, the returned point will be on the perimeter of the AABB.104/// Otherwise, it will be inside the AABB and returned as is.105#[inline(always)]106pub fn closest_point(&self, point: Vec2) -> Vec2 {107// Clamp point coordinates to the AABB108point.clamp(self.min, self.max)109}110}111112impl BoundingVolume for Aabb2d {113type Translation = Vec2;114type Rotation = Rot2;115type HalfSize = Vec2;116117#[inline(always)]118fn center(&self) -> Self::Translation {119(self.min + self.max) / 2.120}121122#[inline(always)]123fn half_size(&self) -> Self::HalfSize {124(self.max - self.min) / 2.125}126127#[inline(always)]128fn visible_area(&self) -> f32 {129let b = (self.max - self.min).max(Vec2::ZERO);130b.x * b.y131}132133#[inline(always)]134fn contains(&self, other: &Self) -> bool {135other.min.x >= self.min.x136&& other.min.y >= self.min.y137&& other.max.x <= self.max.x138&& other.max.y <= self.max.y139}140141#[inline(always)]142fn merge(&self, other: &Self) -> Self {143Self {144min: self.min.min(other.min),145max: self.max.max(other.max),146}147}148149#[inline(always)]150fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {151let amount = amount.into();152let b = Self {153min: self.min - amount,154max: self.max + amount,155};156debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);157b158}159160#[inline(always)]161fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {162let amount = amount.into();163let b = Self {164min: self.min + amount,165max: self.max - amount,166};167debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);168b169}170171#[inline(always)]172fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {173let scale = scale.into();174let b = Self {175min: self.center() - (self.half_size() * scale),176max: self.center() + (self.half_size() * scale),177};178debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);179b180}181182/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.183///184/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.185///186/// Note that the result may not be as tightly fitting as the original, and repeated rotations187/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,188/// and consider storing the original AABB and rotating that every time instead.189#[inline(always)]190fn transformed_by(191mut self,192translation: impl Into<Self::Translation>,193rotation: impl Into<Self::Rotation>,194) -> Self {195self.transform_by(translation, rotation);196self197}198199/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.200///201/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.202///203/// Note that the result may not be as tightly fitting as the original, and repeated rotations204/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,205/// and consider storing the original AABB and rotating that every time instead.206#[inline(always)]207fn transform_by(208&mut self,209translation: impl Into<Self::Translation>,210rotation: impl Into<Self::Rotation>,211) {212self.rotate_by(rotation);213self.translate_by(translation);214}215216#[inline(always)]217fn translate_by(&mut self, translation: impl Into<Self::Translation>) {218let translation = translation.into();219self.min += translation;220self.max += translation;221}222223/// Rotates the bounding volume around the origin by the given rotation.224///225/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.226///227/// Note that the result may not be as tightly fitting as the original, and repeated rotations228/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,229/// and consider storing the original AABB and rotating that every time instead.230#[inline(always)]231fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {232self.rotate_by(rotation);233self234}235236/// Rotates the bounding volume around the origin by the given rotation.237///238/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.239///240/// Note that the result may not be as tightly fitting as the original, and repeated rotations241/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,242/// and consider storing the original AABB and rotating that every time instead.243#[inline(always)]244fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {245let rot_mat = Mat2::from(rotation.into());246let half_size = rot_mat.abs() * self.half_size();247*self = Self::new(rot_mat * self.center(), half_size);248}249}250251impl IntersectsVolume<Self> for Aabb2d {252#[inline(always)]253fn intersects(&self, other: &Self) -> bool {254let x_overlaps = self.min.x <= other.max.x && self.max.x >= other.min.x;255let y_overlaps = self.min.y <= other.max.y && self.max.y >= other.min.y;256x_overlaps && y_overlaps257}258}259260impl IntersectsVolume<BoundingCircle> for Aabb2d {261#[inline(always)]262fn intersects(&self, circle: &BoundingCircle) -> bool {263let closest_point = self.closest_point(circle.center);264let distance_squared = circle.center.distance_squared(closest_point);265let radius_squared = circle.radius().squared();266distance_squared <= radius_squared267}268}269270#[cfg(test)]271mod aabb2d_tests {272use approx::assert_relative_eq;273274use super::Aabb2d;275use crate::{276bounding::{BoundingCircle, BoundingVolume, IntersectsVolume},277ops, Vec2,278};279280#[test]281fn center() {282let aabb = Aabb2d {283min: Vec2::new(-0.5, -1.),284max: Vec2::new(1., 1.),285};286assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < f32::EPSILON);287let aabb = Aabb2d {288min: Vec2::new(5., -10.),289max: Vec2::new(10., -5.),290};291assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < f32::EPSILON);292}293294#[test]295fn half_size() {296let aabb = Aabb2d {297min: Vec2::new(-0.5, -1.),298max: Vec2::new(1., 1.),299};300let half_size = aabb.half_size();301assert!((half_size - Vec2::new(0.75, 1.)).length() < f32::EPSILON);302}303304#[test]305fn area() {306let aabb = Aabb2d {307min: Vec2::new(-1., -1.),308max: Vec2::new(1., 1.),309};310assert!(ops::abs(aabb.visible_area() - 4.) < f32::EPSILON);311let aabb = Aabb2d {312min: Vec2::new(0., 0.),313max: Vec2::new(1., 0.5),314};315assert!(ops::abs(aabb.visible_area() - 0.5) < f32::EPSILON);316}317318#[test]319fn contains() {320let a = Aabb2d {321min: Vec2::new(-1., -1.),322max: Vec2::new(1., 1.),323};324let b = Aabb2d {325min: Vec2::new(-2., -1.),326max: Vec2::new(1., 1.),327};328assert!(!a.contains(&b));329let b = Aabb2d {330min: Vec2::new(-0.25, -0.8),331max: Vec2::new(1., 1.),332};333assert!(a.contains(&b));334}335336#[test]337fn merge() {338let a = Aabb2d {339min: Vec2::new(-1., -1.),340max: Vec2::new(1., 0.5),341};342let b = Aabb2d {343min: Vec2::new(-2., -0.5),344max: Vec2::new(0.75, 1.),345};346let merged = a.merge(&b);347assert!((merged.min - Vec2::new(-2., -1.)).length() < f32::EPSILON);348assert!((merged.max - Vec2::new(1., 1.)).length() < f32::EPSILON);349assert!(merged.contains(&a));350assert!(merged.contains(&b));351assert!(!a.contains(&merged));352assert!(!b.contains(&merged));353}354355#[test]356fn grow() {357let a = Aabb2d {358min: Vec2::new(-1., -1.),359max: Vec2::new(1., 1.),360};361let padded = a.grow(Vec2::ONE);362assert!((padded.min - Vec2::new(-2., -2.)).length() < f32::EPSILON);363assert!((padded.max - Vec2::new(2., 2.)).length() < f32::EPSILON);364assert!(padded.contains(&a));365assert!(!a.contains(&padded));366}367368#[test]369fn shrink() {370let a = Aabb2d {371min: Vec2::new(-2., -2.),372max: Vec2::new(2., 2.),373};374let shrunk = a.shrink(Vec2::ONE);375assert!((shrunk.min - Vec2::new(-1., -1.)).length() < f32::EPSILON);376assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON);377assert!(a.contains(&shrunk));378assert!(!shrunk.contains(&a));379}380381#[test]382fn scale_around_center() {383let a = Aabb2d {384min: Vec2::NEG_ONE,385max: Vec2::ONE,386};387let scaled = a.scale_around_center(Vec2::splat(2.));388assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON);389assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON);390assert!(!a.contains(&scaled));391assert!(scaled.contains(&a));392}393394#[test]395fn rotate() {396let a = Aabb2d {397min: Vec2::new(-2.0, -2.0),398max: Vec2::new(2.0, 2.0),399};400let rotated = a.rotated_by(core::f32::consts::PI);401assert_relative_eq!(rotated.min, a.min);402assert_relative_eq!(rotated.max, a.max);403}404405#[test]406fn transform() {407let a = Aabb2d {408min: Vec2::new(-2.0, -2.0),409max: Vec2::new(2.0, 2.0),410};411let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);412let half_length = ops::hypot(2.0, 2.0);413assert_eq!(414transformed.min,415Vec2::new(2.0 - half_length, -half_length - 2.0)416);417assert_eq!(418transformed.max,419Vec2::new(2.0 + half_length, half_length - 2.0)420);421}422423#[test]424fn closest_point() {425let aabb = Aabb2d {426min: Vec2::NEG_ONE,427max: Vec2::ONE,428};429assert_eq!(aabb.closest_point(Vec2::X * 10.0), Vec2::X);430assert_eq!(aabb.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE);431assert_eq!(432aabb.closest_point(Vec2::new(0.25, 0.1)),433Vec2::new(0.25, 0.1)434);435}436437#[test]438fn intersect_aabb() {439let aabb = Aabb2d {440min: Vec2::NEG_ONE,441max: Vec2::ONE,442};443assert!(aabb.intersects(&aabb));444assert!(aabb.intersects(&Aabb2d {445min: Vec2::new(0.5, 0.5),446max: Vec2::new(2.0, 2.0),447}));448assert!(aabb.intersects(&Aabb2d {449min: Vec2::new(-2.0, -2.0),450max: Vec2::new(-0.5, -0.5),451}));452assert!(!aabb.intersects(&Aabb2d {453min: Vec2::new(1.1, 0.0),454max: Vec2::new(2.0, 0.5),455}));456}457458#[test]459fn intersect_bounding_circle() {460let aabb = Aabb2d {461min: Vec2::NEG_ONE,462max: Vec2::ONE,463};464assert!(aabb.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));465assert!(aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));466assert!(aabb.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.5, 1.0)));467assert!(!aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.75, 1.0)));468}469}470471use crate::primitives::Circle;472473/// A bounding circle474#[derive(Clone, Copy, Debug, PartialEq)]475#[cfg_attr(476feature = "bevy_reflect",477derive(Reflect),478reflect(Debug, PartialEq, Clone)479)]480#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]481#[cfg_attr(482all(feature = "serialize", feature = "bevy_reflect"),483reflect(Serialize, Deserialize)484)]485pub struct BoundingCircle {486/// The center of the bounding circle487pub center: Vec2,488/// The circle489pub circle: Circle,490}491492impl BoundingCircle {493/// Constructs a bounding circle from its center and radius.494#[inline(always)]495pub fn new(center: Vec2, radius: f32) -> Self {496debug_assert!(radius >= 0.);497Self {498center,499circle: Circle { radius },500}501}502503/// Computes a [`BoundingCircle`] containing the given set of points,504/// transformed by the rotation and translation of the given isometry.505///506/// The bounding circle is not guaranteed to be the smallest possible.507#[inline(always)]508pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> BoundingCircle {509let isometry = isometry.into();510511let center = point_cloud_2d_center(points);512let mut radius_squared = 0.0;513514for point in points {515// Get squared version to avoid unnecessary sqrt calls516let distance_squared = point.distance_squared(center);517if distance_squared > radius_squared {518radius_squared = distance_squared;519}520}521522BoundingCircle::new(isometry * center, ops::sqrt(radius_squared))523}524525/// Get the radius of the bounding circle526#[inline(always)]527pub fn radius(&self) -> f32 {528self.circle.radius529}530531/// Computes the smallest [`Aabb2d`] containing this [`BoundingCircle`].532#[inline(always)]533pub fn aabb_2d(&self) -> Aabb2d {534Aabb2d {535min: self.center - Vec2::splat(self.radius()),536max: self.center + Vec2::splat(self.radius()),537}538}539540/// Finds the point on the bounding circle that is closest to the given `point`.541///542/// If the point is outside the circle, the returned point will be on the perimeter of the circle.543/// Otherwise, it will be inside the circle and returned as is.544#[inline(always)]545pub fn closest_point(&self, point: Vec2) -> Vec2 {546self.circle.closest_point(point - self.center) + self.center547}548}549550impl BoundingVolume for BoundingCircle {551type Translation = Vec2;552type Rotation = Rot2;553type HalfSize = f32;554555#[inline(always)]556fn center(&self) -> Self::Translation {557self.center558}559560#[inline(always)]561fn half_size(&self) -> Self::HalfSize {562self.radius()563}564565#[inline(always)]566fn visible_area(&self) -> f32 {567core::f32::consts::PI * self.radius() * self.radius()568}569570#[inline(always)]571fn contains(&self, other: &Self) -> bool {572let diff = self.radius() - other.radius();573self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff)574}575576#[inline(always)]577fn merge(&self, other: &Self) -> Self {578let diff = other.center - self.center;579let length = diff.length();580if self.radius() >= length + other.radius() {581return *self;582}583if other.radius() >= length + self.radius() {584return *other;585}586let dir = diff / length;587Self::new(588(self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),589(length + self.radius() + other.radius()) / 2.,590)591}592593#[inline(always)]594fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {595let amount = amount.into();596debug_assert!(amount >= 0.);597Self::new(self.center, self.radius() + amount)598}599600#[inline(always)]601fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {602let amount = amount.into();603debug_assert!(amount >= 0.);604debug_assert!(self.radius() >= amount);605Self::new(self.center, self.radius() - amount)606}607608#[inline(always)]609fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {610let scale = scale.into();611debug_assert!(scale >= 0.);612Self::new(self.center, self.radius() * scale)613}614615#[inline(always)]616fn translate_by(&mut self, translation: impl Into<Self::Translation>) {617self.center += translation.into();618}619620#[inline(always)]621fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {622let rotation: Rot2 = rotation.into();623self.center = rotation * self.center;624}625}626627impl IntersectsVolume<Self> for BoundingCircle {628#[inline(always)]629fn intersects(&self, other: &Self) -> bool {630let center_distance_squared = self.center.distance_squared(other.center);631let radius_sum_squared = (self.radius() + other.radius()).squared();632center_distance_squared <= radius_sum_squared633}634}635636impl IntersectsVolume<Aabb2d> for BoundingCircle {637#[inline(always)]638fn intersects(&self, aabb: &Aabb2d) -> bool {639aabb.intersects(self)640}641}642643#[cfg(test)]644mod bounding_circle_tests {645use super::BoundingCircle;646use crate::{647bounding::{BoundingVolume, IntersectsVolume},648ops, Vec2,649};650651#[test]652fn area() {653let circle = BoundingCircle::new(Vec2::ONE, 5.);654// Since this number is messy we check it with a higher threshold655assert!(ops::abs(circle.visible_area() - 78.5398) < 0.001);656}657658#[test]659fn contains() {660let a = BoundingCircle::new(Vec2::ONE, 5.);661let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.);662assert!(!a.contains(&b));663let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5);664assert!(a.contains(&b));665}666667#[test]668fn contains_identical() {669let a = BoundingCircle::new(Vec2::ONE, 5.);670assert!(a.contains(&a));671}672673#[test]674fn merge() {675// When merging two circles that don't contain each other, we find a center position that676// contains both677let a = BoundingCircle::new(Vec2::ONE, 5.);678let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);679let merged = a.merge(&b);680assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON);681assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON);682assert!(merged.contains(&a));683assert!(merged.contains(&b));684assert!(!a.contains(&merged));685assert!(!b.contains(&merged));686687// When one circle contains the other circle, we use the bigger circle688let b = BoundingCircle::new(Vec2::ZERO, 3.);689assert!(a.contains(&b));690let merged = a.merge(&b);691assert_eq!(merged.center, a.center);692assert_eq!(merged.radius(), a.radius());693694// When two circles are at the same point, we use the bigger radius695let b = BoundingCircle::new(Vec2::ONE, 6.);696let merged = a.merge(&b);697assert_eq!(merged.center, a.center);698assert_eq!(merged.radius(), b.radius());699}700701#[test]702fn merge_identical() {703let a = BoundingCircle::new(Vec2::ONE, 5.);704let merged = a.merge(&a);705assert_eq!(merged.center, a.center);706assert_eq!(merged.radius(), a.radius());707}708709#[test]710fn grow() {711let a = BoundingCircle::new(Vec2::ONE, 5.);712let padded = a.grow(1.25);713assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON);714assert!(padded.contains(&a));715assert!(!a.contains(&padded));716}717718#[test]719fn shrink() {720let a = BoundingCircle::new(Vec2::ONE, 5.);721let shrunk = a.shrink(0.5);722assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);723assert!(a.contains(&shrunk));724assert!(!shrunk.contains(&a));725}726727#[test]728fn scale_around_center() {729let a = BoundingCircle::new(Vec2::ONE, 5.);730let scaled = a.scale_around_center(2.);731assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);732assert!(!a.contains(&scaled));733assert!(scaled.contains(&a));734}735736#[test]737fn transform() {738let a = BoundingCircle::new(Vec2::ONE, 5.0);739let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);740assert_eq!(741transformed.center,742Vec2::new(2.0, core::f32::consts::SQRT_2 - 2.0)743);744assert_eq!(transformed.radius(), 5.0);745}746747#[test]748fn closest_point() {749let circle = BoundingCircle::new(Vec2::ZERO, 1.0);750assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X);751assert_eq!(752circle.closest_point(Vec2::NEG_ONE * 10.0),753Vec2::NEG_ONE.normalize()754);755assert_eq!(756circle.closest_point(Vec2::new(0.25, 0.1)),757Vec2::new(0.25, 0.1)758);759}760761#[test]762fn intersect_bounding_circle() {763let circle = BoundingCircle::new(Vec2::ZERO, 1.0);764assert!(circle.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));765assert!(circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.25, 1.0)));766assert!(circle.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.25, 1.0)));767assert!(!circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));768}769}770771772