Skip to content

Commit

Permalink
Add Annulus primitive to bevy_math::primitives (#12706)
Browse files Browse the repository at this point in the history
# Objective

- #10572

There is no 2D primitive available for the common shape of an annulus
(ring).

## Solution

This PR introduces a new type to the existing math primitives:

- `Annulus`: the region between two concentric circles

---

## Changelog

### Added

- `Annulus` primitive to the `bevy_math` crate
- `Annulus` tests (`diameter`, `thickness`, `area`, `perimeter` and
`closest_point` methods)

---------

Co-authored-by: Joona Aalto <[email protected]>
  • Loading branch information
Chubercik and Jondolf authored Mar 25, 2024
1 parent cb9789b commit 31d9146
Showing 1 changed file with 109 additions and 0 deletions.
109 changes: 109 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 };
Expand All @@ -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);
Expand Down

0 comments on commit 31d9146

Please sign in to comment.