Skip to content

Commit

Permalink
Implement Rhombus 2D primitive. (bevyengine#13501)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
salvadorcarvalhinho and LuisFigueiredo73 authored May 26, 2024
1 parent 037f37e commit 7d843e0
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 6 deletions.
34 changes: 33 additions & 1 deletion crates/bevy_gizmos/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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>
Expand Down
58 changes: 55 additions & 3 deletions crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);
Expand Down
167 changes: 167 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 };
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_reflect/src/impls/math/primitives2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
34 changes: 33 additions & 1 deletion crates/bevy_render/src/mesh/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 7d843e0

Please sign in to comment.