Skip to content

Add Annulus primitive to bevy_math::primitives #12706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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