Path: blob/main/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
9353 views
//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).12use crate::{3bounding::BoundingVolume,4ops,5primitives::{6Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,7Plane2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d,8},9Dir2, Isometry2d, Mat2, Rot2, Vec2,10};11use core::f32::consts::{FRAC_PI_2, PI, TAU};1213#[cfg(feature = "alloc")]14use crate::primitives::{ConvexPolygon, Polygon, Polyline2d};1516use arrayvec::ArrayVec;1718use super::{Aabb2d, Bounded2d, BoundingCircle};1920impl Bounded2d for Circle {21fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {22let isometry = isometry.into();23Aabb2d::new(isometry.translation, Vec2::splat(self.radius))24}2526fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {27let isometry = isometry.into();28BoundingCircle::new(isometry.translation, self.radius)29}30}3132// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes.33// The return type has room for 7 points so that the CircularSector code can add an additional point.34#[inline]35fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> ArrayVec<Vec2, 7> {36// Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle.37// We need to compute which axis-aligned extrema are actually contained within the rotated arc.38let mut bounds = ArrayVec::<Vec2, 7>::new();39let rotation = rotation.into();40bounds.push(rotation * arc.left_endpoint());41bounds.push(rotation * arc.right_endpoint());4243// The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.44// Compute the normalized angles of the endpoints with the rotation taken into account, and then45// check if we are looking for an angle that is between or outside them.46let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU);47let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);48let inverted = left_angle < right_angle;49for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {50let angle = ops::rem_euclid(extremum.to_angle(), TAU);51// If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.52// There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.53// But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.54let angle_within_parameters = if inverted {55angle >= right_angle || angle <= left_angle56} else {57angle >= right_angle && angle <= left_angle58};59if angle_within_parameters {60bounds.push(extremum * arc.radius);61}62}63bounds64}6566impl Bounded2d for Arc2d {67fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {68// If our arc covers more than a circle, just return the bounding box of the circle.69if self.half_angle >= PI {70return Circle::new(self.radius).aabb_2d(isometry);71}7273let isometry = isometry.into();7475Aabb2d::from_point_cloud(76Isometry2d::from_translation(isometry.translation),77&arc_bounding_points(*self, isometry.rotation),78)79}8081fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {82let isometry = isometry.into();8384// There are two possibilities for the bounding circle.85if self.is_major() {86// If the arc is major, then the widest distance between two points is a diameter of the arc's circle;87// therefore, that circle is the bounding radius.88BoundingCircle::new(isometry.translation, self.radius)89} else {90// Otherwise, the widest distance between two points is the chord,91// so a circle of that diameter around the midpoint will contain the entire arc.92let center = isometry.rotation * self.chord_midpoint();93BoundingCircle::new(center + isometry.translation, self.half_chord_length())94}95}96}9798impl Bounded2d for CircularSector {99fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {100let isometry = isometry.into();101102// If our sector covers more than a circle, just return the bounding box of the circle.103if self.half_angle() >= PI {104return Circle::new(self.radius()).aabb_2d(isometry);105}106107// Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility.108let mut bounds = arc_bounding_points(self.arc, isometry.rotation);109bounds.push(Vec2::ZERO);110111Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds)112}113114fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {115if self.arc.is_major() {116let isometry = isometry.into();117118// If the arc is major, that is, greater than a semicircle,119// then bounding circle is just the circle defining the sector.120BoundingCircle::new(isometry.translation, self.arc.radius)121} else {122// However, when the arc is minor,123// we need our bounding circle to include both endpoints of the arc as well as the circle center.124// This means we need the circumcircle of those three points.125// The circumcircle will always have a greater curvature than the circle itself, so it will contain126// the entire circular sector.127Triangle2d::new(128Vec2::ZERO,129self.arc.left_endpoint(),130self.arc.right_endpoint(),131)132.bounding_circle(isometry)133}134}135}136137impl Bounded2d for CircularSegment {138fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {139self.arc.aabb_2d(isometry)140}141142fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {143self.arc.bounding_circle(isometry)144}145}146147impl Bounded2d for Ellipse {148fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {149let isometry = isometry.into();150151// V = (hh * cos(beta), hh * sin(beta))152// #####*#####153// ### | ###154// # hh | #155// # *---------* U = (hw * cos(alpha), hw * sin(alpha))156// # hw #157// ### ###158// ###########159160let (hw, hh) = (self.half_size.x, self.half_size.y);161162// Sine and cosine of rotation angle alpha.163let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos();164165// Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions:166// sin(beta) = sin(alpha + pi/2) = cos(alpha)167// cos(beta) = cos(alpha + pi/2) = -sin(alpha)168let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);169170// Compute points U and V, the extremes of the ellipse171let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);172let (vx, vy) = (hh * beta_cos, hh * beta_sin);173174let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy));175176Aabb2d::new(isometry.translation, half_size)177}178179fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {180let isometry = isometry.into();181BoundingCircle::new(isometry.translation, self.semi_major())182}183}184185impl Bounded2d for Annulus {186fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {187let isometry = isometry.into();188Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius))189}190191fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {192let isometry = isometry.into();193BoundingCircle::new(isometry.translation, self.outer_circle.radius)194}195}196197impl Bounded2d for Rhombus {198fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {199let isometry = isometry.into();200201let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [202isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0),203isometry.rotation * Vec2::new(0.0, self.half_diagonals.y),204];205let aabb_half_extent = rotated_x_half_diagonal206.abs()207.max(rotated_y_half_diagonal.abs());208209Aabb2d {210min: -aabb_half_extent + isometry.translation,211max: aabb_half_extent + isometry.translation,212}213}214215fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {216let isometry = isometry.into();217BoundingCircle::new(isometry.translation, self.circumradius())218}219}220221impl Bounded2d for Plane2d {222fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {223let isometry = isometry.into();224225let normal = isometry.rotation * *self.normal;226let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;227let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;228229// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations230// like growing or shrinking the AABB without breaking things.231let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };232let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };233let half_size = Vec2::new(half_width, half_height);234235Aabb2d::new(isometry.translation, half_size)236}237238fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {239let isometry = isometry.into();240BoundingCircle::new(isometry.translation, f32::MAX / 2.0)241}242}243244impl Bounded2d for Line2d {245fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {246let isometry = isometry.into();247248let direction = isometry.rotation * *self.direction;249250// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations251// like growing or shrinking the AABB without breaking things.252let max = f32::MAX / 2.0;253let half_width = if direction.x == 0.0 { 0.0 } else { max };254let half_height = if direction.y == 0.0 { 0.0 } else { max };255let half_size = Vec2::new(half_width, half_height);256257Aabb2d::new(isometry.translation, half_size)258}259260fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {261let isometry = isometry.into();262BoundingCircle::new(isometry.translation, f32::MAX / 2.0)263}264}265266impl Bounded2d for Segment2d {267fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {268Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()])269}270271fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {272let isometry: Isometry2d = isometry.into();273let local_center = self.center();274let radius = local_center.distance(self.point1());275let local_circle = BoundingCircle::new(local_center, radius);276local_circle.transformed_by(isometry.translation, isometry.rotation)277}278}279280#[cfg(feature = "alloc")]281impl Bounded2d for Polyline2d {282fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {283Aabb2d::from_point_cloud(isometry, &self.vertices)284}285286fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {287BoundingCircle::from_point_cloud(isometry, &self.vertices)288}289}290291impl Bounded2d for Triangle2d {292fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {293let isometry = isometry.into();294let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx);295296let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));297let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));298299Aabb2d {300min: min + isometry.translation,301max: max + isometry.translation,302}303}304305fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {306let isometry = isometry.into();307let [a, b, c] = self.vertices;308309// The points of the segment opposite to the obtuse or right angle if one exists310let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {311Some((b, c))312} else if (c - b).dot(a - b) <= 0.0 {313Some((c, a))314} else if (a - c).dot(b - c) <= 0.0 {315Some((a, b))316} else {317// The triangle is acute.318None319};320321// Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices.322// Otherwise, it's the circumcircle and passes through all three.323if let Some((point1, point2)) = side_opposite_to_non_acute {324// The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side.325// We can compute the minimum bounding circle from the line segment of the longest side.326let segment = Segment2d::new(point1, point2);327segment.bounding_circle(isometry)328} else {329// The triangle is acute, so the smallest bounding circle is the circumcircle.330let (Circle { radius }, circumcenter) = self.circumcircle();331BoundingCircle::new(isometry * circumcenter, radius)332}333}334}335336impl Bounded2d for Rectangle {337fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {338let isometry = isometry.into();339340// Compute the AABB of the rotated rectangle by transforming the half-extents341// by an absolute rotation matrix.342let (sin, cos) = isometry.rotation.sin_cos();343let abs_rot_mat =344Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]);345let half_size = abs_rot_mat * self.half_size;346347Aabb2d::new(isometry.translation, half_size)348}349350fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {351let isometry = isometry.into();352let radius = self.half_size.length();353BoundingCircle::new(isometry.translation, radius)354}355}356357#[cfg(feature = "alloc")]358impl Bounded2d for Polygon {359fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {360Aabb2d::from_point_cloud(isometry, &self.vertices)361}362363fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {364BoundingCircle::from_point_cloud(isometry, &self.vertices)365}366}367368#[cfg(feature = "alloc")]369impl Bounded2d for ConvexPolygon {370fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {371Aabb2d::from_point_cloud(isometry, self.vertices())372}373374fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {375BoundingCircle::from_point_cloud(isometry, self.vertices())376}377}378379impl Bounded2d for RegularPolygon {380fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {381let isometry = isometry.into();382383let mut min = Vec2::ZERO;384let mut max = Vec2::ZERO;385386for vertex in self.vertices(isometry.rotation.as_radians()) {387min = min.min(vertex);388max = max.max(vertex);389}390391Aabb2d {392min: min + isometry.translation,393max: max + isometry.translation,394}395}396397fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {398let isometry = isometry.into();399BoundingCircle::new(isometry.translation, self.circumcircle.radius)400}401}402403impl Bounded2d for Capsule2d {404fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {405let isometry = isometry.into();406407// Get the line segment between the semicircles of the rotated capsule408let segment = Segment2d::from_direction_and_length(409isometry.rotation * Dir2::Y,410self.half_length * 2.,411);412let (a, b) = (segment.point1(), segment.point2());413414// Expand the line segment by the capsule radius to get the capsule half-extents415let min = a.min(b) - Vec2::splat(self.radius);416let max = a.max(b) + Vec2::splat(self.radius);417418Aabb2d {419min: min + isometry.translation,420max: max + isometry.translation,421}422}423424fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {425let isometry = isometry.into();426BoundingCircle::new(isometry.translation, self.radius + self.half_length)427}428}429430impl<P: Bounded2d + Primitive2d> Bounded2d for Ring<P> {431fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {432self.outer_shape.aabb_2d(isometry)433}434435fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {436self.outer_shape.bounding_circle(isometry)437}438}439440#[cfg(test)]441#[expect(clippy::print_stdout, reason = "Allowed in tests.")]442mod tests {443use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};444use std::println;445446use approx::assert_abs_diff_eq;447use glam::Vec2;448449use crate::{450bounding::Bounded2d,451ops::{self, FloatPow},452primitives::{453Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,454Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,455Triangle2d,456},457Dir2, Isometry2d, Rot2,458};459460#[test]461fn circle() {462let circle = Circle { radius: 1.0 };463let translation = Vec2::new(2.0, 1.0);464let isometry = Isometry2d::from_translation(translation);465466let aabb = circle.aabb_2d(isometry);467assert_eq!(aabb.min, Vec2::new(1.0, 0.0));468assert_eq!(aabb.max, Vec2::new(3.0, 2.0));469470let bounding_circle = circle.bounding_circle(isometry);471assert_eq!(bounding_circle.center, translation);472assert_eq!(bounding_circle.radius(), 1.0);473}474475#[test]476// Arcs and circular segments have the same bounding shapes so they share test cases.477fn arc_and_segment() {478struct TestCase {479name: &'static str,480arc: Arc2d,481translation: Vec2,482rotation: f32,483aabb_min: Vec2,484aabb_max: Vec2,485bounding_circle_center: Vec2,486bounding_circle_radius: f32,487}488489impl TestCase {490fn isometry(&self) -> Isometry2d {491Isometry2d::new(self.translation, self.rotation.into())492}493}494495// The apothem of an arc covering 1/6th of a circle.496let apothem = ops::sqrt(3.0) / 2.0;497let tests = [498// Test case: a basic minor arc499TestCase {500name: "1/6th circle untransformed",501arc: Arc2d::from_radians(1.0, FRAC_PI_3),502translation: Vec2::ZERO,503rotation: 0.0,504aabb_min: Vec2::new(-0.5, apothem),505aabb_max: Vec2::new(0.5, 1.0),506bounding_circle_center: Vec2::new(0.0, apothem),507bounding_circle_radius: 0.5,508},509// Test case: a smaller arc, verifying that radius scaling works510TestCase {511name: "1/6th circle with radius 0.5",512arc: Arc2d::from_radians(0.5, FRAC_PI_3),513translation: Vec2::ZERO,514rotation: 0.0,515aabb_min: Vec2::new(-0.25, apothem / 2.0),516aabb_max: Vec2::new(0.25, 0.5),517bounding_circle_center: Vec2::new(0.0, apothem / 2.0),518bounding_circle_radius: 0.25,519},520// Test case: a larger arc, verifying that radius scaling works521TestCase {522name: "1/6th circle with radius 2.0",523arc: Arc2d::from_radians(2.0, FRAC_PI_3),524translation: Vec2::ZERO,525rotation: 0.0,526aabb_min: Vec2::new(-1.0, 2.0 * apothem),527aabb_max: Vec2::new(1.0, 2.0),528bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),529bounding_circle_radius: 1.0,530},531// Test case: translation of a minor arc532TestCase {533name: "1/6th circle translated",534arc: Arc2d::from_radians(1.0, FRAC_PI_3),535translation: Vec2::new(2.0, 3.0),536rotation: 0.0,537aabb_min: Vec2::new(1.5, 3.0 + apothem),538aabb_max: Vec2::new(2.5, 4.0),539bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),540bounding_circle_radius: 0.5,541},542// Test case: rotation of a minor arc543TestCase {544name: "1/6th circle rotated",545arc: Arc2d::from_radians(1.0, FRAC_PI_3),546translation: Vec2::ZERO,547// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.548rotation: FRAC_PI_6,549aabb_min: Vec2::new(-apothem, 0.5),550aabb_max: Vec2::new(0.0, 1.0),551// The exact coordinates here are not obvious, but can be computed by constructing552// an altitude from the midpoint of the chord to the y-axis and using the right triangle553// similarity theorem.554bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()),555bounding_circle_radius: 0.5,556},557// Test case: handling of axis-aligned extrema558TestCase {559name: "1/4er circle rotated to be axis-aligned",560arc: Arc2d::from_radians(1.0, FRAC_PI_2),561translation: Vec2::ZERO,562// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.563rotation: -FRAC_PI_4,564aabb_min: Vec2::ZERO,565aabb_max: Vec2::splat(1.0),566bounding_circle_center: Vec2::splat(0.5),567bounding_circle_radius: ops::sqrt(2.0) / 2.0,568},569// Test case: a basic major arc570TestCase {571name: "5/6th circle untransformed",572arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),573translation: Vec2::ZERO,574rotation: 0.0,575aabb_min: Vec2::new(-1.0, -apothem),576aabb_max: Vec2::new(1.0, 1.0),577bounding_circle_center: Vec2::ZERO,578bounding_circle_radius: 1.0,579},580// Test case: a translated major arc581TestCase {582name: "5/6th circle translated",583arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),584translation: Vec2::new(2.0, 3.0),585rotation: 0.0,586aabb_min: Vec2::new(1.0, 3.0 - apothem),587aabb_max: Vec2::new(3.0, 4.0),588bounding_circle_center: Vec2::new(2.0, 3.0),589bounding_circle_radius: 1.0,590},591// Test case: a rotated major arc, with inverted left/right angles592TestCase {593name: "5/6th circle rotated",594arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),595translation: Vec2::ZERO,596// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.597rotation: FRAC_PI_6,598aabb_min: Vec2::new(-1.0, -1.0),599aabb_max: Vec2::new(1.0, 1.0),600bounding_circle_center: Vec2::ZERO,601bounding_circle_radius: 1.0,602},603];604605for test in tests {606#[cfg(feature = "std")]607println!("subtest case: {}", test.name);608let segment: CircularSegment = test.arc.into();609610let arc_aabb = test.arc.aabb_2d(test.isometry());611assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);612assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);613let segment_aabb = segment.aabb_2d(test.isometry());614assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);615assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);616617let arc_bounding_circle = test.arc.bounding_circle(test.isometry());618assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);619assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());620let segment_bounding_circle = segment.bounding_circle(test.isometry());621assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);622assert_abs_diff_eq!(623test.bounding_circle_radius,624segment_bounding_circle.radius()625);626}627}628629#[test]630fn circular_sector() {631struct TestCase {632name: &'static str,633arc: Arc2d,634translation: Vec2,635rotation: f32,636aabb_min: Vec2,637aabb_max: Vec2,638bounding_circle_center: Vec2,639bounding_circle_radius: f32,640}641642impl TestCase {643fn isometry(&self) -> Isometry2d {644Isometry2d::new(self.translation, self.rotation.into())645}646}647648// The apothem of an arc covering 1/6th of a circle.649let apothem = ops::sqrt(3.0) / 2.0;650let inv_sqrt_3 = ops::sqrt(3.0).recip();651let tests = [652// Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center653TestCase {654name: "1/3rd circle",655arc: Arc2d::from_radians(1.0, TAU / 3.0),656translation: Vec2::ZERO,657rotation: 0.0,658aabb_min: Vec2::new(-apothem, 0.0),659aabb_max: Vec2::new(apothem, 1.0),660bounding_circle_center: Vec2::new(0.0, 0.5),661bounding_circle_radius: apothem,662},663// The remaining test cases are selected as for arc_and_segment.664TestCase {665name: "1/6th circle untransformed",666arc: Arc2d::from_radians(1.0, FRAC_PI_3),667translation: Vec2::ZERO,668rotation: 0.0,669aabb_min: Vec2::new(-0.5, 0.0),670aabb_max: Vec2::new(0.5, 1.0),671// The bounding circle is a circumcircle of an equilateral triangle with side length 1.672// The distance from the corner to the center of such a triangle is 1/sqrt(3).673bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),674bounding_circle_radius: inv_sqrt_3,675},676TestCase {677name: "1/6th circle with radius 0.5",678arc: Arc2d::from_radians(0.5, FRAC_PI_3),679translation: Vec2::ZERO,680rotation: 0.0,681aabb_min: Vec2::new(-0.25, 0.0),682aabb_max: Vec2::new(0.25, 0.5),683bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),684bounding_circle_radius: inv_sqrt_3 / 2.0,685},686TestCase {687name: "1/6th circle with radius 2.0",688arc: Arc2d::from_radians(2.0, FRAC_PI_3),689translation: Vec2::ZERO,690rotation: 0.0,691aabb_min: Vec2::new(-1.0, 0.0),692aabb_max: Vec2::new(1.0, 2.0),693bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),694bounding_circle_radius: 2.0 * inv_sqrt_3,695},696TestCase {697name: "1/6th circle translated",698arc: Arc2d::from_radians(1.0, FRAC_PI_3),699translation: Vec2::new(2.0, 3.0),700rotation: 0.0,701aabb_min: Vec2::new(1.5, 3.0),702aabb_max: Vec2::new(2.5, 4.0),703bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),704bounding_circle_radius: inv_sqrt_3,705},706TestCase {707name: "1/6th circle rotated",708arc: Arc2d::from_radians(1.0, FRAC_PI_3),709translation: Vec2::ZERO,710// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.711rotation: FRAC_PI_6,712aabb_min: Vec2::new(-apothem, 0.0),713aabb_max: Vec2::new(0.0, 1.0),714// The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2.715bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),716bounding_circle_radius: inv_sqrt_3,717},718TestCase {719name: "1/4er circle rotated to be axis-aligned",720arc: Arc2d::from_radians(1.0, FRAC_PI_2),721translation: Vec2::ZERO,722// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.723rotation: -FRAC_PI_4,724aabb_min: Vec2::ZERO,725aabb_max: Vec2::splat(1.0),726bounding_circle_center: Vec2::splat(0.5),727bounding_circle_radius: ops::sqrt(2.0) / 2.0,728},729TestCase {730name: "5/6th circle untransformed",731arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),732translation: Vec2::ZERO,733rotation: 0.0,734aabb_min: Vec2::new(-1.0, -apothem),735aabb_max: Vec2::new(1.0, 1.0),736bounding_circle_center: Vec2::ZERO,737bounding_circle_radius: 1.0,738},739TestCase {740name: "5/6th circle translated",741arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),742translation: Vec2::new(2.0, 3.0),743rotation: 0.0,744aabb_min: Vec2::new(1.0, 3.0 - apothem),745aabb_max: Vec2::new(3.0, 4.0),746bounding_circle_center: Vec2::new(2.0, 3.0),747bounding_circle_radius: 1.0,748},749TestCase {750name: "5/6th circle rotated",751arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),752translation: Vec2::ZERO,753// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.754rotation: FRAC_PI_6,755aabb_min: Vec2::new(-1.0, -1.0),756aabb_max: Vec2::new(1.0, 1.0),757bounding_circle_center: Vec2::ZERO,758bounding_circle_radius: 1.0,759},760];761762for test in tests {763#[cfg(feature = "std")]764println!("subtest case: {}", test.name);765let sector: CircularSector = test.arc.into();766767let aabb = sector.aabb_2d(test.isometry());768assert_abs_diff_eq!(test.aabb_min, aabb.min);769assert_abs_diff_eq!(test.aabb_max, aabb.max);770771let bounding_circle = sector.bounding_circle(test.isometry());772assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);773assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());774}775}776777#[test]778fn ellipse() {779let ellipse = Ellipse::new(1.0, 0.5);780let translation = Vec2::new(2.0, 1.0);781let isometry = Isometry2d::from_translation(translation);782783let aabb = ellipse.aabb_2d(isometry);784assert_eq!(aabb.min, Vec2::new(1.0, 0.5));785assert_eq!(aabb.max, Vec2::new(3.0, 1.5));786787let bounding_circle = ellipse.bounding_circle(isometry);788assert_eq!(bounding_circle.center, translation);789assert_eq!(bounding_circle.radius(), 1.0);790}791792#[test]793fn annulus() {794let annulus = Annulus::new(1.0, 2.0);795let translation = Vec2::new(2.0, 1.0);796let rotation = Rot2::radians(1.0);797let isometry = Isometry2d::new(translation, rotation);798799let aabb = annulus.aabb_2d(isometry);800assert_eq!(aabb.min, Vec2::new(0.0, -1.0));801assert_eq!(aabb.max, Vec2::new(4.0, 3.0));802803let bounding_circle = annulus.bounding_circle(isometry);804assert_eq!(bounding_circle.center, translation);805assert_eq!(bounding_circle.radius(), 2.0);806}807808#[test]809fn rhombus() {810let rhombus = Rhombus::new(2.0, 1.0);811let translation = Vec2::new(2.0, 1.0);812let rotation = Rot2::radians(FRAC_PI_4);813let isometry = Isometry2d::new(translation, rotation);814815let aabb = rhombus.aabb_2d(isometry);816assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));817assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));818819let bounding_circle = rhombus.bounding_circle(isometry);820assert_eq!(bounding_circle.center, translation);821assert_eq!(bounding_circle.radius(), 1.0);822823let rhombus = Rhombus::new(0.0, 0.0);824let translation = Vec2::new(0.0, 0.0);825let isometry = Isometry2d::new(translation, rotation);826827let aabb = rhombus.aabb_2d(isometry);828assert_eq!(aabb.min, Vec2::new(0.0, 0.0));829assert_eq!(aabb.max, Vec2::new(0.0, 0.0));830831let bounding_circle = rhombus.bounding_circle(isometry);832assert_eq!(bounding_circle.center, translation);833assert_eq!(bounding_circle.radius(), 0.0);834}835836#[test]837fn plane() {838let translation = Vec2::new(2.0, 1.0);839let isometry = Isometry2d::from_translation(translation);840841let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry);842assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));843assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));844845let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry);846assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));847assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));848849let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry);850assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));851assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));852853let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry);854assert_eq!(bounding_circle.center, translation);855assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);856}857858#[test]859fn line() {860let translation = Vec2::new(2.0, 1.0);861let isometry = Isometry2d::from_translation(translation);862863let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry);864assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));865assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));866867let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry);868assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));869assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));870871let aabb3 = Line2d {872direction: Dir2::from_xy(1.0, 1.0).unwrap(),873}874.aabb_2d(isometry);875assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));876assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));877878let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry);879assert_eq!(bounding_circle.center, translation);880assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);881}882883#[test]884fn segment() {885let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5));886let translation = Vec2::new(2.0, 1.0);887let isometry = Isometry2d::from_translation(translation);888889let aabb = segment.aabb_2d(isometry);890assert_eq!(aabb.min, Vec2::new(1.0, 0.5));891assert_eq!(aabb.max, Vec2::new(3.0, 1.5));892893let bounding_circle = segment.bounding_circle(isometry);894assert_eq!(bounding_circle.center, translation);895assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));896}897898#[test]899fn polyline() {900let polyline = Polyline2d::new([901Vec2::ONE,902Vec2::new(-1.0, 1.0),903Vec2::NEG_ONE,904Vec2::new(1.0, -1.0),905]);906let translation = Vec2::new(2.0, 1.0);907let isometry = Isometry2d::from_translation(translation);908909let aabb = polyline.aabb_2d(isometry);910assert_eq!(aabb.min, Vec2::new(1.0, 0.0));911assert_eq!(aabb.max, Vec2::new(3.0, 2.0));912913let bounding_circle = polyline.bounding_circle(isometry);914assert_eq!(bounding_circle.center, translation);915assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);916}917918#[test]919fn acute_triangle() {920let acute_triangle =921Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));922let translation = Vec2::new(2.0, 1.0);923let isometry = Isometry2d::from_translation(translation);924925let aabb = acute_triangle.aabb_2d(isometry);926assert_eq!(aabb.min, Vec2::new(1.0, 0.0));927assert_eq!(aabb.max, Vec2::new(3.0, 2.0));928929// For acute triangles, the center is the circumcenter930let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();931let bounding_circle = acute_triangle.bounding_circle(isometry);932assert_eq!(bounding_circle.center, circumcenter + translation);933assert_eq!(bounding_circle.radius(), radius);934}935936#[test]937fn obtuse_triangle() {938let obtuse_triangle = Triangle2d::new(939Vec2::new(0.0, 1.0),940Vec2::new(-10.0, -1.0),941Vec2::new(10.0, -1.0),942);943let translation = Vec2::new(2.0, 1.0);944let isometry = Isometry2d::from_translation(translation);945946let aabb = obtuse_triangle.aabb_2d(isometry);947assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));948assert_eq!(aabb.max, Vec2::new(12.0, 2.0));949950// For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle)951let bounding_circle = obtuse_triangle.bounding_circle(isometry);952assert_eq!(bounding_circle.center, translation - Vec2::Y);953assert_eq!(bounding_circle.radius(), 10.0);954}955956#[test]957fn rectangle() {958let rectangle = Rectangle::new(2.0, 1.0);959let translation = Vec2::new(2.0, 1.0);960961let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4)));962let expected_half_size = Vec2::splat(1.0606601);963assert_eq!(aabb.min, translation - expected_half_size);964assert_eq!(aabb.max, translation + expected_half_size);965966let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation));967assert_eq!(bounding_circle.center, translation);968assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));969}970971#[test]972fn polygon() {973let polygon = Polygon::new([974Vec2::ONE,975Vec2::new(-1.0, 1.0),976Vec2::NEG_ONE,977Vec2::new(1.0, -1.0),978]);979let translation = Vec2::new(2.0, 1.0);980let isometry = Isometry2d::from_translation(translation);981982let aabb = polygon.aabb_2d(isometry);983assert_eq!(aabb.min, Vec2::new(1.0, 0.0));984assert_eq!(aabb.max, Vec2::new(3.0, 2.0));985986let bounding_circle = polygon.bounding_circle(isometry);987assert_eq!(bounding_circle.center, translation);988assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);989}990991#[test]992fn regular_polygon() {993let regular_polygon = RegularPolygon::new(1.0, 5);994let translation = Vec2::new(2.0, 1.0);995let isometry = Isometry2d::from_translation(translation);996997let aabb = regular_polygon.aabb_2d(isometry);998assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);999assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);10001001let bounding_circle = regular_polygon.bounding_circle(isometry);1002assert_eq!(bounding_circle.center, translation);1003assert_eq!(bounding_circle.radius(), 1.0);1004}10051006#[test]1007fn capsule() {1008let capsule = Capsule2d::new(0.5, 2.0);1009let translation = Vec2::new(2.0, 1.0);1010let isometry = Isometry2d::from_translation(translation);10111012let aabb = capsule.aabb_2d(isometry);1013assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));1014assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));10151016let bounding_circle = capsule.bounding_circle(isometry);1017assert_eq!(bounding_circle.center, translation);1018assert_eq!(bounding_circle.radius(), 1.5);1019}1020}102110221023