diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 056c3d388eb3e2..ee942e52dd73e0 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -3,8 +3,8 @@ use glam::{Mat2, Vec2}; use crate::primitives::{ - BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, - Rectangle, RegularPolygon, Segment2d, Triangle2d, + BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; use super::{Aabb2d, Bounded2d, BoundingCircle}; @@ -230,6 +230,31 @@ impl Bounded2d for RegularPolygon { } } +impl Bounded2d for Capsule2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // Get the line segment between the hemicircles of the rotated capsule + let segment = Segment2d { + // Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector. + direction: Direction2d::new_unchecked(Mat2::from_angle(rotation) * Vec2::Y), + half_length: self.half_length, + }; + let (a, b) = (segment.point1(), segment.point2()); + + // Expand the line segment by the capsule radius to get the capsule half-extents + let min = a.min(b) - Vec2::splat(self.radius); + let max = a.max(b) + Vec2::splat(self.radius); + + Aabb2d { + min: min + translation, + max: max + translation, + } + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, self.radius + self.half_length) + } +} + #[cfg(test)] mod tests { use glam::Vec2; @@ -237,8 +262,8 @@ mod tests { use crate::{ bounding::Bounded2d, primitives::{ - Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, - RegularPolygon, Segment2d, Triangle2d, + Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, + Rectangle, RegularPolygon, Segment2d, Triangle2d, }, }; @@ -440,4 +465,18 @@ mod tests { assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } + + #[test] + fn capsule() { + let capsule = Capsule2d::new(0.5, 2.0); + let translation = Vec2::new(2.0, 1.0); + + let aabb = capsule.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5)); + assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5)); + + let bounding_circle = capsule.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.5); + } } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index b992fb517ea222..67302547938779 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -5,7 +5,7 @@ use glam::{Mat3, Quat, Vec2, Vec3}; use crate::{ bounding::{Bounded2d, BoundingCircle}, primitives::{ - BoxedPolyline3d, Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, + BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, }, }; @@ -146,7 +146,7 @@ impl Bounded3d for Cylinder { } } -impl Bounded3d for Capsule { +impl Bounded3d for Capsule3d { fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { // Get the line segment between the hemispheres of the rotated capsule let segment = Segment3d { @@ -311,7 +311,7 @@ mod tests { use crate::{ bounding::Bounded3d, primitives::{ - Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d, + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d, Polyline3d, Segment3d, Sphere, Torus, }, }; @@ -463,7 +463,7 @@ mod tests { #[test] fn capsule() { - let capsule = Capsule::new(0.5, 2.0); + let capsule = Capsule3d::new(0.5, 2.0); let translation = Vec3::new(2.0, 1.0, 0.0); let aabb = capsule.aabb_3d(translation, Quat::IDENTITY); diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 906850a999ebb4..a413b180930d67 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -471,6 +471,7 @@ pub struct Rectangle { /// Half of the width and height of the rectangle pub half_size: Vec2, } +impl Primitive2d for Rectangle {} impl Default for Rectangle { /// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`. @@ -721,6 +722,30 @@ impl RegularPolygon { } } +/// A 2D capsule primitive, also known as a stadium or pill shape. +/// +/// A two-dimensional capsule is defined as a neighborhood of points at a distance (radius) from a line +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[doc(alias = "stadium", alias = "pill")] +pub struct Capsule2d { + /// The radius of the capsule + pub radius: f32, + /// Half the height of the capsule, excluding the hemicircles + pub half_length: f32, +} +impl Primitive2d for Capsule2d {} + +impl Capsule2d { + /// Create a new `Capsule2d` from a radius and length + pub fn new(radius: f32, length: f32) -> Self { + Self { + radius, + half_length: length / 2.0, + } + } +} + #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 62ccd588be69f2..7d0b34ad2def8f 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -423,22 +423,20 @@ impl Cylinder { } } -/// A capsule primitive. -/// A capsule is defined as a surface at a distance (radius) from a line +/// A 3D capsule primitive. +/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct Capsule { +pub struct Capsule3d { /// The radius of the capsule pub radius: f32, /// Half the height of the capsule, excluding the hemispheres pub half_length: f32, } -impl super::Primitive2d for Capsule {} -impl Primitive3d for Capsule {} +impl Primitive3d for Capsule3d {} -impl Capsule { - /// Create a new `Capsule` from a radius and length - #[inline(always)] +impl Capsule3d { + /// Create a new `Capsule3d` from a radius and length pub fn new(radius: f32, length: f32) -> Self { Self { radius, @@ -704,7 +702,7 @@ mod tests { #[test] fn capsule_math() { - let capsule = Capsule::new(2.0, 9.0); + let capsule = Capsule3d::new(2.0, 9.0); assert_eq!( capsule.to_cylinder(), Cylinder::new(2.0, 9.0), diff --git a/crates/bevy_reflect/src/impls/math/primitives2d.rs b/crates/bevy_reflect/src/impls/math/primitives2d.rs index 92725d4d4d5af1..e584dee9ce721e 100644 --- a/crates/bevy_reflect/src/impls/math/primitives2d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives2d.rs @@ -91,3 +91,12 @@ impl_reflect_struct!( sides: usize, } ); + +impl_reflect_struct!( + #[reflect(Debug, PartialEq, Serialize, Deserialize)] + #[type_path = "bevy_math::primitives"] + struct Capsule2d { + radius: f32, + half_length: f32, + } +); diff --git a/crates/bevy_reflect/src/impls/math/primitives3d.rs b/crates/bevy_reflect/src/impls/math/primitives3d.rs index 7120781949dad7..9724ae19a1ffc8 100644 --- a/crates/bevy_reflect/src/impls/math/primitives3d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives3d.rs @@ -71,7 +71,7 @@ impl_reflect_struct!( impl_reflect_struct!( #[reflect(Debug, PartialEq, Serialize, Deserialize)] #[type_path = "bevy_math::primitives"] - struct Capsule { + struct Capsule3d { radius: f32, half_length: f32, }