Skip to content

Commit 31d9146

Browse files
ChubercikJondolf
andauthored
Add Annulus primitive to bevy_math::primitives (#12706)
# 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]>
1 parent cb9789b commit 31d9146

File tree

1 file changed

+109
-0
lines changed
  • crates/bevy_math/src/primitives

1 file changed

+109
-0
lines changed

crates/bevy_math/src/primitives/dim2.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,92 @@ impl Ellipse {
125125
}
126126
}
127127

128+
/// A primitive shape formed by the region between two circles, also known as a ring.
129+
#[derive(Clone, Copy, Debug, PartialEq)]
130+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
131+
#[doc(alias = "Ring")]
132+
pub struct Annulus {
133+
/// The inner circle of the annulus
134+
pub inner_circle: Circle,
135+
/// The outer circle of the annulus
136+
pub outer_circle: Circle,
137+
}
138+
impl Primitive2d for Annulus {}
139+
140+
impl Default for Annulus {
141+
/// Returns the default [`Annulus`] with radii of `0.5` and `1.0`.
142+
fn default() -> Self {
143+
Self {
144+
inner_circle: Circle::new(0.5),
145+
outer_circle: Circle::new(1.0),
146+
}
147+
}
148+
}
149+
150+
impl Annulus {
151+
/// Create a new [`Annulus`] from the radii of the inner and outer circle
152+
#[inline(always)]
153+
pub const fn new(inner_radius: f32, outer_radius: f32) -> Self {
154+
Self {
155+
inner_circle: Circle::new(inner_radius),
156+
outer_circle: Circle::new(outer_radius),
157+
}
158+
}
159+
160+
/// Get the diameter of the annulus
161+
#[inline(always)]
162+
pub fn diameter(&self) -> f32 {
163+
self.outer_circle.diameter()
164+
}
165+
166+
/// Get the thickness of the annulus
167+
#[inline(always)]
168+
pub fn thickness(&self) -> f32 {
169+
self.outer_circle.radius - self.inner_circle.radius
170+
}
171+
172+
/// Get the area of the annulus
173+
#[inline(always)]
174+
pub fn area(&self) -> f32 {
175+
PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2))
176+
}
177+
178+
/// Get the perimeter or circumference of the annulus,
179+
/// which is the sum of the perimeters of the inner and outer circles.
180+
#[inline(always)]
181+
#[doc(alias = "circumference")]
182+
pub fn perimeter(&self) -> f32 {
183+
2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius)
184+
}
185+
186+
/// Finds the point on the annulus that is closest to the given `point`:
187+
///
188+
/// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter.
189+
/// - If the point is inside of the inner circle (hole) of the annulus, the returned point will be on the inner perimeter.
190+
/// - Otherwise, the returned point is overlapping the annulus and returned as is.
191+
#[inline(always)]
192+
pub fn closest_point(&self, point: Vec2) -> Vec2 {
193+
let distance_squared = point.length_squared();
194+
195+
if self.inner_circle.radius.powi(2) <= distance_squared {
196+
if distance_squared <= self.outer_circle.radius.powi(2) {
197+
// The point is inside the annulus.
198+
point
199+
} else {
200+
// The point is outside the annulus and closer to the outer perimeter.
201+
// Find the closest point on the perimeter of the annulus.
202+
let dir_to_point = point / distance_squared.sqrt();
203+
self.outer_circle.radius * dir_to_point
204+
}
205+
} else {
206+
// The point is outside the annulus and closer to the inner perimeter.
207+
// Find the closest point on the perimeter of the annulus.
208+
let dir_to_point = point / distance_squared.sqrt();
209+
self.inner_circle.radius * dir_to_point
210+
}
211+
}
212+
}
213+
128214
/// An unbounded plane in 2D space. It forms a separating surface through the origin,
129215
/// stretching infinitely far
130216
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -718,6 +804,20 @@ mod tests {
718804
);
719805
}
720806

807+
#[test]
808+
fn annulus_closest_point() {
809+
let annulus = Annulus::new(1.5, 2.0);
810+
assert_eq!(annulus.closest_point(Vec2::X * 10.0), Vec2::X * 2.0);
811+
assert_eq!(
812+
annulus.closest_point(Vec2::NEG_ONE),
813+
Vec2::NEG_ONE.normalize() * 1.5
814+
);
815+
assert_eq!(
816+
annulus.closest_point(Vec2::new(1.55, 0.85)),
817+
Vec2::new(1.55, 0.85)
818+
);
819+
}
820+
721821
#[test]
722822
fn circle_math() {
723823
let circle = Circle { radius: 3.0 };
@@ -726,6 +826,15 @@ mod tests {
726826
assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter");
727827
}
728828

829+
#[test]
830+
fn annulus_math() {
831+
let annulus = Annulus::new(2.5, 3.5);
832+
assert_eq!(annulus.diameter(), 7.0, "incorrect diameter");
833+
assert_eq!(annulus.thickness(), 1.0, "incorrect thickness");
834+
assert_eq!(annulus.area(), 18.849556, "incorrect area");
835+
assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter");
836+
}
837+
729838
#[test]
730839
fn ellipse_math() {
731840
let ellipse = Ellipse::new(3.0, 1.0);

0 commit comments

Comments
 (0)