Add Capsule2d
primitive (#11585)
# Objective Currently, the `Capsule` primitive is technically dimension-agnostic in that it implements both `Primitive2d` and `Primitive3d`. This seems good on paper, but it can often be useful to have separate 2D and 3D versions of primitives. For example, one might want a two-dimensional capsule mesh. We can't really implement both 2D and 3D meshing for the same type using the upcoming `Meshable` trait (see #11431). We also currently don't implement `Bounded2d` for `Capsule`, see https://github.com/bevyengine/bevy/pull/11336#issuecomment-1890797788. Having 2D and 3D separate at a type level is more explicit, and also more consistent with the existing primitives, as there are no other types that implement both `Primitive2d` and `Primitive3d` at the same time. ## Solution Rename `Capsule` to `Capsule3d` and add `Capsule2d`. `Capsule2d` implements `Bounded2d`. For now, I went for `Capsule2d` for the sake of consistency and clarity. Mathematically the more accurate term would be `Stadium` or `Pill` (see [Wikipedia](https://en.wikipedia.org/wiki/Stadium_(geometry))), but those might be less obvious to game devs. For reference, Godot has [`CapsuleShape2D`](https://docs.godotengine.org/en/stable/classes/class_capsuleshape2d.html). I can rename it if others think the geometrically correct name is better though. --- ## Changelog - Renamed `Capsule` to `Capsule3d` - Added `Capsule2d` with `Bounded2d` implemented --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
1bc293f33a
commit
a9f061e909
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user