diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index f995729ddd075..4ae6932eb1c5e 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -125,6 +125,92 @@ impl Ellipse { } } +/// A primitive shape formed by the region between two circles, also known as a ring. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[doc(alias = "Ring")] +pub struct Annulus { + /// The inner circle of the annulus + pub inner_circle: Circle, + /// The outer circle of the annulus + pub outer_circle: Circle, +} +impl Primitive2d for Annulus {} + +impl Default for Annulus { + /// Returns the default [`Annulus`] with radii of `0.5` and `1.0`. + fn default() -> Self { + Self { + inner_circle: Circle::new(0.5), + outer_circle: Circle::new(1.0), + } + } +} + +impl Annulus { + /// Create a new [`Annulus`] from the radii of the inner and outer circle + #[inline(always)] + pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { + Self { + inner_circle: Circle::new(inner_radius), + outer_circle: Circle::new(outer_radius), + } + } + + /// Get the diameter of the annulus + #[inline(always)] + pub fn diameter(&self) -> f32 { + self.outer_circle.diameter() + } + + /// Get the thickness of the annulus + #[inline(always)] + pub fn thickness(&self) -> f32 { + self.outer_circle.radius - self.inner_circle.radius + } + + /// Get the area of the annulus + #[inline(always)] + pub fn area(&self) -> f32 { + PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) + } + + /// Get the perimeter or circumference of the annulus, + /// which is the sum of the perimeters of the inner and outer circles. + #[inline(always)] + #[doc(alias = "circumference")] + pub fn perimeter(&self) -> f32 { + 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) + } + + /// Finds the point on the annulus that is closest to the given `point`: + /// + /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. + /// - If the point is inside of the inner circle (hole) of the annulus, the returned point will be on the inner perimeter. + /// - Otherwise, the returned point is overlapping the annulus and returned as is. + #[inline(always)] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + let distance_squared = point.length_squared(); + + if self.inner_circle.radius.powi(2) <= distance_squared { + if distance_squared <= self.outer_circle.radius.powi(2) { + // The point is inside the annulus. + point + } else { + // The point is outside the annulus and closer to the outer perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / distance_squared.sqrt(); + self.outer_circle.radius * dir_to_point + } + } else { + // The point is outside the annulus and closer to the inner perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / distance_squared.sqrt(); + self.inner_circle.radius * dir_to_point + } + } +} + /// An unbounded plane in 2D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -718,6 +804,20 @@ mod tests { ); } + #[test] + fn annulus_closest_point() { + let annulus = Annulus::new(1.5, 2.0); + assert_eq!(annulus.closest_point(Vec2::X * 10.0), Vec2::X * 2.0); + assert_eq!( + annulus.closest_point(Vec2::NEG_ONE), + Vec2::NEG_ONE.normalize() * 1.5 + ); + assert_eq!( + annulus.closest_point(Vec2::new(1.55, 0.85)), + Vec2::new(1.55, 0.85) + ); + } + #[test] fn circle_math() { let circle = Circle { radius: 3.0 }; @@ -726,6 +826,15 @@ mod tests { assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); } + #[test] + fn annulus_math() { + let annulus = Annulus::new(2.5, 3.5); + assert_eq!(annulus.diameter(), 7.0, "incorrect diameter"); + assert_eq!(annulus.thickness(), 1.0, "incorrect thickness"); + assert_eq!(annulus.area(), 18.849556, "incorrect area"); + assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter"); + } + #[test] fn ellipse_math() { let ellipse = Ellipse::new(3.0, 1.0);