//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). use glam::{Mat2, Vec2}; use crate::primitives::{ BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; use super::{Aabb2d, Bounded2d, BoundingCircle}; impl Bounded2d for Circle { fn aabb_2d(&self, translation: Vec2, _rotation: f32) -> Aabb2d { Aabb2d { min: translation - Vec2::splat(self.radius), max: translation + Vec2::splat(self.radius), } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, self.radius) } } impl Bounded2d for Ellipse { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // V = (hh * cos(beta), hh * sin(beta)) // #####*##### // ### | ### // # hh | # // # *---------* U = (hw * cos(alpha), hw * sin(alpha)) // # hw # // ### ### // ########### let (hw, hh) = (self.half_width, self.half_height); // Sine and cosine of rotation angle alpha. let (alpha_sin, alpha_cos) = rotation.sin_cos(); // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions: // sin(beta) = sin(alpha + pi/2) = cos(alpha) // cos(beta) = cos(alpha + pi/2) = -sin(alpha) let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin); // Compute points U and V, the extremes of the ellipse let (ux, uy) = (hw * alpha_cos, hw * alpha_sin); let (vx, vy) = (hh * beta_cos, hh * beta_sin); let half_extents = Vec2::new(ux.hypot(vx), uy.hypot(vy)); Aabb2d { min: translation - half_extents, max: translation + half_extents, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, self.half_width.max(self.half_height)) } } impl Bounded2d for Plane2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { let normal = Mat2::from_angle(rotation) * *self.normal; let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations // like growing or shrinking the AABB without breaking things. let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; let half_size = Vec2::new(half_width, half_height); Aabb2d { min: translation - half_size, max: translation + half_size, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, f32::MAX / 2.0) } } impl Bounded2d for Line2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { let direction = Mat2::from_angle(rotation) * *self.direction; // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations // like growing or shrinking the AABB without breaking things. let max = f32::MAX / 2.0; let half_width = if direction.x == 0.0 { 0.0 } else { max }; let half_height = if direction.y == 0.0 { 0.0 } else { max }; let half_size = Vec2::new(half_width, half_height); Aabb2d { min: translation - half_size, max: translation + half_size, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, f32::MAX / 2.0) } } impl Bounded2d for Segment2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // Rotate the segment by `rotation` let direction = Mat2::from_angle(rotation) * *self.direction; let half_extent = (self.half_length * direction).abs(); Aabb2d { min: translation - half_extent, max: translation + half_extent, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, self.half_length) } } impl Bounded2d for Polyline2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for BoxedPolyline2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for Triangle2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { let rotation_mat = Mat2::from_angle(rotation); let [a, b, c] = self.vertices.map(|vtx| rotation_mat * vtx); let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y)); let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y)); Aabb2d { min: min + translation, max: max + translation, } } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { let rotation_mat = Mat2::from_angle(rotation); let [a, b, c] = self.vertices; // The points of the segment opposite to the obtuse or right angle if one exists let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { Some((b, c)) } else if (c - b).dot(a - b) <= 0.0 { Some((c, a)) } else if (a - c).dot(b - c) <= 0.0 { Some((a, b)) } else { // The triangle is acute. None }; // Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices. // Otherwise, it's the circumcircle and passes through all three. if let Some((point1, point2)) = side_opposite_to_non_acute { // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. // We can compute the minimum bounding circle from the line segment of the longest side. let (segment, center) = Segment2d::from_points(point1, point2); segment.bounding_circle(rotation_mat * center + translation, rotation) } else { // The triangle is acute, so the smallest bounding circle is the circumcircle. let (Circle { radius }, circumcenter) = self.circumcircle(); BoundingCircle::new(rotation_mat * circumcenter + translation, radius) } } } impl Bounded2d for Rectangle { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { let half_size = Vec2::new(self.half_width, self.half_height); // Compute the AABB of the rotated rectangle by transforming the half-extents // by an absolute rotation matrix. let (sin, cos) = rotation.sin_cos(); let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]); let half_extents = abs_rot_mat * half_size; Aabb2d { min: translation - half_extents, max: translation + half_extents, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { let radius = self.half_width.hypot(self.half_height); BoundingCircle::new(translation, radius) } } impl Bounded2d for Polygon { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for BoxedPolygon { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for RegularPolygon { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { let mut min = Vec2::ZERO; let mut max = Vec2::ZERO; for vertex in self.vertices(rotation) { min = min.min(vertex); max = max.max(vertex); } Aabb2d { min: min + translation, max: max + translation, } } fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { BoundingCircle::new(translation, self.circumcircle.radius) } } #[cfg(test)] mod tests { use glam::Vec2; use crate::{ bounding::Bounded2d, primitives::{ Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, }; #[test] fn circle() { let circle = Circle { radius: 1.0 }; let translation = Vec2::new(2.0, 1.0); let aabb = circle.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); let bounding_circle = circle.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } #[test] fn ellipse() { let ellipse = Ellipse { half_width: 1.0, half_height: 0.5, }; let translation = Vec2::new(2.0, 1.0); let aabb = ellipse.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); let bounding_circle = ellipse.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } #[test] fn plane() { let translation = Vec2::new(2.0, 1.0); let aabb1 = Plane2d::new(Vec2::X).aabb_2d(translation, 0.0); assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(translation, 0.0); assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(translation, 0.0); assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } #[test] fn line() { let translation = Vec2::new(2.0, 1.0); let aabb1 = Line2d { direction: Direction2d::Y, } .aabb_2d(translation, 0.0); assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); let aabb2 = Line2d { direction: Direction2d::X, } .aabb_2d(translation, 0.0); assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); let aabb3 = Line2d { direction: Direction2d::from_xy(1.0, 1.0).unwrap(), } .aabb_2d(translation, 0.0); assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); let bounding_circle = Line2d { direction: Direction2d::Y, } .bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } #[test] fn segment() { let translation = Vec2::new(2.0, 1.0); let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0; let aabb = segment.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); let bounding_circle = segment.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); } #[test] fn polyline() { let polyline = Polyline2d::<4>::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0), ]); let translation = Vec2::new(2.0, 1.0); let aabb = polyline.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); let bounding_circle = polyline.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); } #[test] fn acute_triangle() { let acute_triangle = Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0)); let translation = Vec2::new(2.0, 1.0); let aabb = acute_triangle.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); // For acute triangles, the center is the circumcenter let (Circle { radius }, circumcenter) = acute_triangle.circumcircle(); let bounding_circle = acute_triangle.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, circumcenter + translation); assert_eq!(bounding_circle.radius(), radius); } #[test] fn obtuse_triangle() { let obtuse_triangle = Triangle2d::new( Vec2::new(0.0, 1.0), Vec2::new(-10.0, -1.0), Vec2::new(10.0, -1.0), ); let translation = Vec2::new(2.0, 1.0); let aabb = obtuse_triangle.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(-8.0, 0.0)); assert_eq!(aabb.max, Vec2::new(12.0, 2.0)); // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle) let bounding_circle = obtuse_triangle.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation - Vec2::Y); assert_eq!(bounding_circle.radius(), 10.0); } #[test] fn rectangle() { let rectangle = Rectangle::new(2.0, 1.0); let translation = Vec2::new(2.0, 1.0); let aabb = rectangle.aabb_2d(translation, std::f32::consts::FRAC_PI_4); let expected_half_size = Vec2::splat(1.0606601); assert_eq!(aabb.min, translation - expected_half_size); assert_eq!(aabb.max, translation + expected_half_size); let bounding_circle = rectangle.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); } #[test] fn polygon() { let polygon = Polygon::<4>::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0), ]); let translation = Vec2::new(2.0, 1.0); let aabb = polygon.aabb_2d(translation, 0.0); assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); let bounding_circle = polygon.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); } #[test] fn regular_polygon() { let regular_polygon = RegularPolygon::new(1.0, 5); let translation = Vec2::new(2.0, 1.0); let aabb = regular_polygon.aabb_2d(translation, 0.0); assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6); assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6); let bounding_circle = regular_polygon.bounding_circle(translation, 0.0); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } }