Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define a basic set of Primitives #10466

Merged
merged 10 commits into from
Nov 15, 2023
5 changes: 3 additions & 2 deletions crates/bevy_math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

mod affine3;
pub mod cubic_splines;
pub mod primitives;
mod ray;
mod rects;

Expand All @@ -24,8 +25,8 @@ pub mod prelude {
CubicBSpline, CubicBezier, CubicCardinalSpline, CubicGenerator, CubicHermite,
CubicSegment,
},
BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray,
Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4,
primitives, BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4,
Quat, Ray, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4,
Vec4Swizzles,
};
}
Expand Down
181 changes: 181 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use super::Primitive2d;
use crate::Vec2;

/// A normalized vector pointing in a direction in 2D space
#[derive(Clone, Copy, Debug)]
pub struct Direction2d(Vec2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're using Vec2 and fn etc. in Rust, can we use Dir2d here and save some typing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from Quad (quadrilateral), it'd be the only abbreviated primitive, but it does resemble a struct like Vec2 more than an actual primitive shape... if it will be used a lot in public APIs, then perhaps Dir2d would be good, but otherwise I think I prefer Direction2d since it's generally better not to abbreviate names in my eyes. Other opinions on this would be useful though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's generally better not to abbreviate

Sure but have you been looking at Rust, didn't that ship float already? 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For very common types that appear everywhere like Vec, Vec2, Quat and Res, it makes sense to abbreviate, but for most other types I don't consider it good practise. But Dir2d could be considered to be a utility type similar to Vec2, so it could work here, not sure. (although we have the extra d there... Dir2 would be more consistent with Glam math types, but not with Bevy's other types)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's difficult to tell in advance. I don't mind either way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to be consistent and explicit here. I don't think it's common enough to warrant shortening.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d vote Direction2d for what it’s worth.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use Normalized<T>(T), it could be used for T = Vec2, Vec3, Quat, etc. Aliases could be defined as well.

I think it's okay to abbreviate often-used types, such as Vec2/Vec3 (integers and floats only get a single letter), but using an expanded name for less common types avoids confusion and time spent searching documentation.

Copy link
Contributor

@afonsolage afonsolage Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO Direction2d is better because it is more explicit. Since most users are usually using rust-analyzer, you can just type Dir2 and the fuzzy matching autocomplete will do the work for you, so you would type less anyways.

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can go for Direction2d since it's very explicit and consistent with other names. I kinda like the Normalized suggestion too, but we only need it for Vec2/Vec3 for now, so I think it's fine if we go for the non-generic approach until there's a real need for it. It would probably be aliased under Direction2d/Direction3d anyway and the API would be basically the same, so changing it later if necessary should be pretty straightforward.


impl From<Vec2> for Direction2d {
fn from(value: Vec2) -> Self {
Self(value.normalize())
}
}

impl Direction2d {
/// Create a direction from a [Vec2] that is already normalized
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn from_normalized(value: Vec2) -> Self {
debug_assert!(value.is_normalized());
Self(value)
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl std::ops::Deref for Direction2d {
type Target = Vec2;
fn deref(&self) -> &Self::Target {
&self.0
}
}

/// A circle primitive
#[derive(Clone, Copy, Debug)]
pub struct Circle {
/// The radius of the circle
pub radius: f32,
}
impl Primitive2d for Circle {}

/// An unbounded plane in 2D space. It forms a separating surface trough the origin,
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// stretching infinitely far
#[derive(Clone, Copy, Debug)]
pub struct Plane2d {
/// The normal of the plane, the plane will be placed perpendicular to this direction
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub normal: Direction2d,
}
impl Primitive2d for Plane2d {}

/// An infinite line along a direction in 2D space.
///
/// For a finite line: [`Segment2d`]
#[derive(Clone, Copy, Debug)]
pub struct Line2d {
/// The direction of the line, the line extends infinitely in both the positive
/// and negative of this direction
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub direction: Direction2d,
}
impl Primitive2d for Line2d {}

/// A segment of a line along a direction in 2D space.
#[doc(alias = "LineSegment2d")]
#[derive(Clone, Debug)]
pub struct Segment2d {
/// The direction of the line
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub direction: Direction2d,
/// Half the length of the line segment, the segment extends by this amount in both
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// the positive and negative direction
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub half_length: f32,
}
impl Primitive2d for Segment2d {}

impl Segment2d {
/// Create a line segment from a direction and full length of the line
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(direction: Direction2d, length: f32) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ask what you need, and don't assume what the user have; that will avoid them having to pass half_length * 2 only to be divided again.

Suggested change
pub fn new(direction: Direction2d, length: f32) -> Self {
pub fn new(direction: Direction2d, half_length: f32) -> Self {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main reason I made it like that is because it's not all that common start with a half length, and even if you did the struct fields are public and don't have the footgun of not realizing its a half length (because you need to type out the field name). But maybe having it be called new and then taking a different format is kind of confusing either way

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed: because the fields are public, the constructor methods should be intuitive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

Self {
direction,
half_length: length / 2.,
}
}

/// Get a line segment and translation from two points at each end of a line segment
///
/// Panics if point1 == point2
pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) {
let diff = point2 - point1;
let length = diff.length();
(
Self::new(Direction2d::from_normalized(diff / length), length),
(point1 + point2) / 2.,
)
}

/// Get the position of the first point on the line segment
pub fn point1(&self) -> Vec2 {
*self.direction * -self.half_length
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
}

/// Get the position of the second point on the line segment
pub fn point2(&self) -> Vec2 {
*self.direction * self.half_length
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// A series of connected line segments in 2D space.
///
/// For a version without generics: [`BoxedPolyline2d`]
#[derive(Clone, Debug)]
pub struct Polyline2d<const N: usize> {
/// The vertices of the polyline
pub vertices: [Vec2; N],
}
impl<const N: usize> Primitive2d for Polyline2d<N> {}

/// A series of connected line segments in 2D space.
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
///
/// For a version without alloc: [`Polyline2d`]
#[derive(Clone, Debug)]
pub struct BoxedPolyline2d {
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
/// The vertices of the polyline
pub vertices: Box<[Vec2]>,
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
}
impl Primitive2d for BoxedPolyline2d {}

/// A triangle in 2D space
#[derive(Clone, Debug)]
pub struct Triangle2d {
/// The vertices of the triangle
pub vertices: [Vec2; 3],
}
impl Primitive2d for Triangle2d {}

/// A rectangle primitive
#[doc(alias = "Quad")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A rectangle is a type of quadrilateral, but not the same thing; a quadrilateral is just a four-sided polygon, but it makes no guarantees on the positions of those vertices. But this is just an alias so it's probably fine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly there for anyone looking for a Quad so they get this as its the closest thing to what they might be looking for, since plenty of engines and programs don't offer you a rectangle but just a quad but then only allow you to set width/height anyway.

#[derive(Clone, Copy, Debug)]
pub struct Rectangle {
/// The half width of the rectangle
pub half_width: f32,
/// The half height of the rectangle
pub half_height: f32,
}
impl Primitive2d for Rectangle {}

impl Rectangle {
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// Create a Rectangle from a full width and height
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(width: f32, height: f32) -> Self {
Self::from_size(Vec2::new(width, height))
}

/// Create a Rectangle from the full size of a rectangle
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn from_size(size: Vec2) -> Self {
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
Self {
half_width: size.x / 2.,
half_height: size.y / 2.,
}
}
}

/// A polygon with N vertices
/// For a version without generics: [`BoxedPolygon`]
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Clone, Debug)]
pub struct Polygon<const N: usize> {
/// The vertices of the polygon
pub vertices: [Vec2; N],
}
impl<const N: usize> Primitive2d for Polygon<N> {}

/// A polygon with a variable number of vertices
/// For a version without alloc: [`Polygon`]
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Clone, Debug)]
pub struct BoxedPolygon {
/// The vertices of the polygon
pub vertices: Box<[Vec2]>,
}
impl Primitive2d for BoxedPolygon {}

/// A polygon where all vertices lie on a circle, equally far apart
#[derive(Clone, Copy, Debug)]
pub struct RegularPolygon {
/// The circumcircle on which all vertices lie
pub circumcircle: Circle,
/// The number of vertices
pub n_vertices: usize,
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
}
impl Primitive2d for RegularPolygon {}
172 changes: 172 additions & 0 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use super::Primitive3d;
use crate::Vec3;

/// A normalized vector pointing in a direction in 3D space
#[derive(Clone, Copy, Debug)]
pub struct Direction3d(Vec3);

impl From<Vec3> for Direction3d {
fn from(value: Vec3) -> Self {
Self(value.normalize())
}
}

impl Direction3d {
/// Create a direction from a [Vec3] that is already normalized
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn from_normalized(value: Vec3) -> Self {
debug_assert!(value.is_normalized());
Self(value)
}
}

impl std::ops::Deref for Direction3d {
type Target = Vec3;
fn deref(&self) -> &Self::Target {
&self.0
}
}

/// A sphere primitive
#[derive(Clone, Copy, Debug)]
pub struct Sphere {
/// The radius of the sphere
pub radius: f32,
}
impl Primitive3d for Sphere {}

/// An unbounded plane in 3D space. It forms a separating surface trough the origin,
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// stretching infinitely far
#[derive(Clone, Copy, Debug)]
pub struct Plane3d {
/// The normal of the plane, the plane will be placed perpendicular to this direction
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub normal: Direction3d,
}
impl Primitive3d for Plane3d {}

/// An infinite line along a direction in 3D space.
///
/// For a finite line: [`Segment3d`]
#[derive(Clone, Copy, Debug)]
pub struct Line3d {
/// The direction of the line
pub direction: Direction3d,
}
impl Primitive3d for Line3d {}

/// A segment of a line along a direction in 3D space.
#[doc(alias = "LineSegment3d")]
#[derive(Clone, Debug)]
pub struct Segment3d {
/// The direction of the line
pub direction: Direction3d,
/// Half the length of the line segment, the segment extends by this amount in both
/// the positive and negative direction
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub half_length: f32,
}
impl Primitive3d for Segment3d {}

impl Segment3d {
/// Create a line segment from a direction and full length of the line
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(direction: Direction3d, length: f32) -> Self {
Self {
direction,
half_length: length / 2.,
}
}

/// Get a line segment and translation from two points at each end of a line segment
///
/// Panics if point1 == point2
pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) {
let diff = point2 - point1;
let length = diff.length();
(
Self::new(Direction3d::from_normalized(diff / length), length),
(point1 + point2) / 2.,
)
}

/// Get the position of the first point on the line segment
pub fn point1(&self) -> Vec3 {
*self.direction * -self.half_length
}

/// Get the position of the second point on the line segment
pub fn point2(&self) -> Vec3 {
*self.direction * self.half_length
}
}

/// A series of connected line segments in 3D space.
///
/// For a version without generics: [`BoxedPolyline3d`]
#[derive(Clone, Debug)]
pub struct Polyline3d<const N: usize> {
/// The vertices of the polyline
pub vertices: [Vec3; N],
}
impl<const N: usize> Primitive3d for Polyline3d<N> {}

/// A series of connected line segments in 3D space.
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
///
/// For a version without alloc: [`Polyline3d`]
#[derive(Clone, Debug)]
pub struct BoxedPolyline3d {
/// The vertices of the polyline
pub vertices: Box<[Vec3]>,
}
impl Primitive3d for BoxedPolyline3d {}

/// A cuboid primitive, more commonly known as a box.
#[derive(Clone, Copy, Debug)]
pub struct Cuboid {
/// Half of the width, height and depth of the cuboid
pub half_extents: Vec3,
}
impl Primitive3d for Cuboid {}

impl Cuboid {
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// Create a cuboid from a full x, y and z length
pub fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {
Self::from_size(Vec3::new(x_length, y_length, z_length))
}

/// Create a cuboid from the full size of the cuboid
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn from_size(size: Vec3) -> Self {
Self {
half_extents: size / 2.,
}
}
}

/// A cylinder primitive
#[derive(Clone, Copy, Debug)]
pub struct Cylinder {
/// The radius of the cylinder
pub radius: f32,
/// The half height of the cylinder
pub half_height: f32,
}
impl Primitive3d for Cylinder {}

impl Cylinder {
/// Create a cylinder from a radius and full height
pub fn new(radius: f32, height: f32) -> Self {
Self {
radius,
half_height: height / 2.,
}
}
}

/// A capsule primitive.
/// A capsule is defined as a surface at a distance (radius) from a line
#[derive(Clone, Copy, Debug)]
pub struct Capsule {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably isn't really an issue, but using the same capsule shape for 2D and 3D might make some helper methods a bit more annoying. For example, capsule.area() is different for 2D and 3D since the surface area is interpreted differently, so there'd need to be separate area_2d and area_3d methods.

/// 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 {}
14 changes: 14 additions & 0 deletions crates/bevy_math/src/primitives/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! This module defines primitive shapes.
//! The origin is (0, 0) for 2D primitives and (0, 0, 0) for 3D primitives,
//! unless stated otherwise.

mod dim2;
pub use dim2::*;
mod dim3;
pub use dim3::*;

/// A marker trait for 2D primitives
pub trait Primitive2d {}

/// A marker trait for 3D primitives
pub trait Primitive3d {}