Implement Rhombus 2D primitive. (#13501)
# Objective - Create a new 2D primitive, Rhombus, also knows as "Diamond Shape" - Simplify the creation and handling of isometric projections - Extend Bevy's arsenal of 2D primitives ## Testing - New unit tests created in bevy_math/ primitives and bev_math/ bounding - Tested translations, rotations, wireframe, bounding sphere, aabb and creation parameters --------- Co-authored-by: Luís Figueiredo <luispcfigueiredo@tecnico.ulisboa.pt>
This commit is contained in:
parent
037f37e4d6
commit
7d843e0c08
@ -7,7 +7,7 @@ use super::helpers::*;
|
||||
use bevy_color::Color;
|
||||
use bevy_math::primitives::{
|
||||
Annulus, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon,
|
||||
Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
||||
Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
|
||||
};
|
||||
use bevy_math::{Dir2, Mat2, Vec2};
|
||||
|
||||
@ -138,6 +138,38 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// rhombus 2d
|
||||
|
||||
impl<'w, 's, Config, Clear> GizmoPrimitive2d<Rhombus> for Gizmos<'w, 's, Config, Clear>
|
||||
where
|
||||
Config: GizmoConfigGroup,
|
||||
Clear: 'static + Send + Sync,
|
||||
{
|
||||
type Output<'a> = () where Self: 'a;
|
||||
|
||||
fn primitive_2d(
|
||||
&mut self,
|
||||
primitive: Rhombus,
|
||||
position: Vec2,
|
||||
angle: f32,
|
||||
color: impl Into<Color>,
|
||||
) -> Self::Output<'_> {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let [a, b, c, d] =
|
||||
[(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)].map(|(sign_x, sign_y)| {
|
||||
Vec2::new(
|
||||
primitive.half_diagonals.x * sign_x,
|
||||
primitive.half_diagonals.y * sign_y,
|
||||
)
|
||||
});
|
||||
let positions = [a, b, c, d, a].map(rotate_then_translate_2d(angle, position));
|
||||
self.linestrip_2d(positions, color);
|
||||
}
|
||||
}
|
||||
|
||||
// capsule 2d
|
||||
|
||||
impl<'w, 's, Config, Clear> GizmoPrimitive2d<Capsule2d> for Gizmos<'w, 's, Config, Clear>
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
use crate::{
|
||||
primitives::{
|
||||
Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment,
|
||||
Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d,
|
||||
Triangle2d,
|
||||
Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus,
|
||||
Segment2d, Triangle2d,
|
||||
},
|
||||
Dir2, Mat2, Rotation2d, Vec2,
|
||||
};
|
||||
@ -183,6 +183,33 @@ impl Bounded2d for Ellipse {
|
||||
}
|
||||
}
|
||||
|
||||
impl Bounded2d for Rhombus {
|
||||
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||
let rotation_mat = rotation.into();
|
||||
|
||||
let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [
|
||||
rotation_mat * Vec2::new(self.half_diagonals.x, 0.0),
|
||||
rotation_mat * Vec2::new(0.0, self.half_diagonals.y),
|
||||
];
|
||||
let aabb_half_extent = rotated_x_half_diagonal
|
||||
.abs()
|
||||
.max(rotated_y_half_diagonal.abs());
|
||||
|
||||
Aabb2d {
|
||||
min: -aabb_half_extent + translation,
|
||||
max: aabb_half_extent + translation,
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_circle(
|
||||
&self,
|
||||
translation: Vec2,
|
||||
_rotation: impl Into<Rotation2d>,
|
||||
) -> BoundingCircle {
|
||||
BoundingCircle::new(translation, self.circumradius())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bounded2d for Plane2d {
|
||||
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||
let rotation: Rotation2d = rotation.into();
|
||||
@ -448,7 +475,7 @@ mod tests {
|
||||
bounding::Bounded2d,
|
||||
primitives::{
|
||||
Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d,
|
||||
Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
||||
Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
|
||||
},
|
||||
Dir2,
|
||||
};
|
||||
@ -769,6 +796,31 @@ mod tests {
|
||||
assert_eq!(bounding_circle.radius(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rhombus() {
|
||||
let rhombus = Rhombus::new(2.0, 1.0);
|
||||
let translation = Vec2::new(2.0, 1.0);
|
||||
|
||||
let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
|
||||
assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
|
||||
assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
|
||||
|
||||
let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4);
|
||||
assert_eq!(bounding_circle.center, translation);
|
||||
assert_eq!(bounding_circle.radius(), 1.0);
|
||||
|
||||
let rhombus = Rhombus::new(0.0, 0.0);
|
||||
let translation = Vec2::new(0.0, 0.0);
|
||||
|
||||
let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
|
||||
assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
|
||||
assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
|
||||
|
||||
let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4);
|
||||
assert_eq!(bounding_circle.center, translation);
|
||||
assert_eq!(bounding_circle.radius(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane() {
|
||||
let translation = Vec2::new(2.0, 1.0);
|
||||
|
||||
@ -929,6 +929,132 @@ impl Measured2d for Annulus {
|
||||
}
|
||||
}
|
||||
|
||||
/// A rhombus primitive, also known as a diamond shape.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[doc(alias = "Diamond")]
|
||||
pub struct Rhombus {
|
||||
/// Size of the horizontal and vertical diagonals of the rhombus
|
||||
pub half_diagonals: Vec2,
|
||||
}
|
||||
impl Primitive2d for Rhombus {}
|
||||
|
||||
impl Default for Rhombus {
|
||||
/// Returns the default [`Rhombus`] with a half-horizontal and half-vertical diagonal of `0.5`.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
half_diagonals: Vec2::splat(0.5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rhombus {
|
||||
/// Create a new `Rhombus` from a vertical and horizontal diagonal sizes.
|
||||
#[inline(always)]
|
||||
pub fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self {
|
||||
Self {
|
||||
half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Rhombus` from a side length with all inner angles equal.
|
||||
#[inline(always)]
|
||||
pub fn from_side(side: f32) -> Self {
|
||||
Self {
|
||||
half_diagonals: Vec2::splat(side.hypot(side) / 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Rhombus` from a given inradius with all inner angles equal.
|
||||
#[inline(always)]
|
||||
pub fn from_inradius(inradius: f32) -> Self {
|
||||
let half_diagonal = inradius * 2.0 / std::f32::consts::SQRT_2;
|
||||
Self {
|
||||
half_diagonals: Vec2::new(half_diagonal, half_diagonal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the length of each side of the rhombus
|
||||
#[inline(always)]
|
||||
pub fn side(&self) -> f32 {
|
||||
self.half_diagonals.length()
|
||||
}
|
||||
|
||||
/// Get the radius of the circumcircle on which all vertices
|
||||
/// of the rhombus lie
|
||||
#[inline(always)]
|
||||
pub fn circumradius(&self) -> f32 {
|
||||
self.half_diagonals.x.max(self.half_diagonals.y)
|
||||
}
|
||||
|
||||
/// Get the radius of the largest circle that can
|
||||
/// be drawn within the rhombus
|
||||
#[inline(always)]
|
||||
#[doc(alias = "apothem")]
|
||||
pub fn inradius(&self) -> f32 {
|
||||
let side = self.side();
|
||||
if side == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.half_diagonals.x * self.half_diagonals.y) / side
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the point on the rhombus that is closest to the given `point`.
|
||||
///
|
||||
/// If the point is outside the rhombus, the returned point will be on the perimeter of the rhombus.
|
||||
/// Otherwise, it will be inside the rhombus and returned as is.
|
||||
#[inline(always)]
|
||||
pub fn closest_point(&self, point: Vec2) -> Vec2 {
|
||||
// Fold the problem into the positive quadrant
|
||||
let point_abs = point.abs();
|
||||
let half_diagonals = self.half_diagonals.abs(); // to ensure correct sign
|
||||
|
||||
// The unnormalised normal vector perpendicular to the side of the rhombus
|
||||
let normal = Vec2::new(half_diagonals.y, half_diagonals.x);
|
||||
let normal_magnitude_squared = normal.length_squared();
|
||||
if normal_magnitude_squared == 0.0 {
|
||||
return Vec2::ZERO; // A null Rhombus has only one point anyway.
|
||||
}
|
||||
|
||||
// The last term corresponds to normal.dot(rhombus_vertex)
|
||||
let distance_unnormalised = normal.dot(point_abs) - half_diagonals.x * half_diagonals.y;
|
||||
|
||||
// The point is already inside so we simply return it.
|
||||
if distance_unnormalised <= 0.0 {
|
||||
return point;
|
||||
}
|
||||
|
||||
// Clamp the point to the edge
|
||||
let mut result = point_abs - normal * distance_unnormalised / normal_magnitude_squared;
|
||||
|
||||
// Clamp the point back to the positive quadrant
|
||||
// if it's outside, it needs to be clamped to either vertex
|
||||
if result.x <= 0.0 {
|
||||
result = Vec2::new(0.0, half_diagonals.y);
|
||||
} else if result.y <= 0.0 {
|
||||
result = Vec2::new(half_diagonals.x, 0.0);
|
||||
}
|
||||
|
||||
// Finally, we restore the signs of the original vector
|
||||
result.copysign(point)
|
||||
}
|
||||
}
|
||||
|
||||
impl Measured2d for Rhombus {
|
||||
/// Get the area of the rhombus
|
||||
#[inline(always)]
|
||||
fn area(&self) -> f32 {
|
||||
2.0 * self.half_diagonals.x * self.half_diagonals.y
|
||||
}
|
||||
|
||||
/// Get the perimeter of the rhombus
|
||||
#[inline(always)]
|
||||
fn perimeter(&self) -> f32 {
|
||||
4.0 * self.side()
|
||||
}
|
||||
}
|
||||
|
||||
/// An unbounded plane in 2D space. It forms a separating surface through the origin,
|
||||
/// stretching infinitely far
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@ -1601,6 +1727,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rhombus_closest_point() {
|
||||
let rhombus = Rhombus::new(2.0, 1.0);
|
||||
assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::X);
|
||||
assert_eq!(
|
||||
rhombus.closest_point(Vec2::NEG_ONE * 0.2),
|
||||
Vec2::NEG_ONE * 0.2
|
||||
);
|
||||
assert_eq!(
|
||||
rhombus.closest_point(Vec2::new(-0.55, 0.35)),
|
||||
Vec2::new(-0.5, 0.25)
|
||||
);
|
||||
|
||||
let rhombus = Rhombus::new(0.0, 0.0);
|
||||
assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::ZERO);
|
||||
assert_eq!(rhombus.closest_point(Vec2::NEG_ONE * 0.2), Vec2::ZERO);
|
||||
assert_eq!(rhombus.closest_point(Vec2::new(-0.55, 0.35)), Vec2::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_math() {
|
||||
let circle = Circle { radius: 3.0 };
|
||||
@ -1618,6 +1763,28 @@ mod tests {
|
||||
assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rhombus_math() {
|
||||
let rhombus = Rhombus::new(3.0, 4.0);
|
||||
assert_eq!(rhombus.area(), 6.0, "incorrect area");
|
||||
assert_eq!(rhombus.perimeter(), 10.0, "incorrect perimeter");
|
||||
assert_eq!(rhombus.side(), 2.5, "incorrect side");
|
||||
assert_eq!(rhombus.inradius(), 1.2, "incorrect inradius");
|
||||
assert_eq!(rhombus.circumradius(), 2.0, "incorrect circumradius");
|
||||
let rhombus = Rhombus::new(0.0, 0.0);
|
||||
assert_eq!(rhombus.area(), 0.0, "incorrect area");
|
||||
assert_eq!(rhombus.perimeter(), 0.0, "incorrect perimeter");
|
||||
assert_eq!(rhombus.side(), 0.0, "incorrect side");
|
||||
assert_eq!(rhombus.inradius(), 0.0, "incorrect inradius");
|
||||
assert_eq!(rhombus.circumradius(), 0.0, "incorrect circumradius");
|
||||
let rhombus = Rhombus::from_side(std::f32::consts::SQRT_2);
|
||||
assert_eq!(rhombus, Rhombus::new(2.0, 2.0));
|
||||
assert_eq!(
|
||||
rhombus,
|
||||
Rhombus::from_inradius(std::f32::consts::FRAC_1_SQRT_2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_math() {
|
||||
let ellipse = Ellipse::new(3.0, 1.0);
|
||||
|
||||
@ -28,6 +28,14 @@ impl_reflect!(
|
||||
}
|
||||
);
|
||||
|
||||
impl_reflect!(
|
||||
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[type_path = "bevy_math::primitives"]
|
||||
struct Rhombus {
|
||||
half_diagonals: Vec2,
|
||||
}
|
||||
);
|
||||
|
||||
impl_reflect!(
|
||||
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[type_path = "bevy_math::primitives"]
|
||||
|
||||
@ -10,7 +10,7 @@ use super::{MeshBuilder, Meshable};
|
||||
use bevy_math::{
|
||||
primitives::{
|
||||
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle,
|
||||
RegularPolygon, Triangle2d, Triangle3d, WindingOrder,
|
||||
RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
|
||||
},
|
||||
FloatExt, Vec2,
|
||||
};
|
||||
@ -583,6 +583,38 @@ impl From<Annulus> for Mesh {
|
||||
}
|
||||
}
|
||||
|
||||
impl Meshable for Rhombus {
|
||||
type Output = Mesh;
|
||||
|
||||
fn mesh(&self) -> Self::Output {
|
||||
let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y];
|
||||
let positions = vec![
|
||||
[hhd, 0.0, 0.0],
|
||||
[-hhd, 0.0, 0.0],
|
||||
[0.0, vhd, 0.0],
|
||||
[0.0, -vhd, 0.0],
|
||||
];
|
||||
let normals = vec![[0.0, 0.0, 1.0]; 4];
|
||||
let uvs = vec![[1.0, 0.5], [0.0, 0.5], [0.5, 0.0], [0.5, 1.0]];
|
||||
let indices = Indices::U32(vec![1, 0, 2, 1, 3, 0]);
|
||||
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::default(),
|
||||
)
|
||||
.with_inserted_indices(indices)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rhombus> for Mesh {
|
||||
fn from(rhombus: Rhombus) -> Self {
|
||||
rhombus.mesh()
|
||||
}
|
||||
}
|
||||
|
||||
impl Meshable for Triangle2d {
|
||||
type Output = Mesh;
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ fn main() {
|
||||
.run();
|
||||
}
|
||||
|
||||
const X_EXTENT: f32 = 800.;
|
||||
const X_EXTENT: f32 = 900.;
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
@ -28,6 +28,7 @@ fn setup(
|
||||
Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))),
|
||||
Mesh2dHandle(meshes.add(Annulus::new(25.0, 50.0))),
|
||||
Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))),
|
||||
Mesh2dHandle(meshes.add(Rhombus::new(75.0, 100.0))),
|
||||
Mesh2dHandle(meshes.add(Rectangle::new(50.0, 100.0))),
|
||||
Mesh2dHandle(meshes.add(RegularPolygon::new(50.0, 6))),
|
||||
Mesh2dHandle(meshes.add(Triangle2d::new(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user