# Objective

- Adds a basic `Extrusion<T: Primitive2d>` shape, suggestion of #10572 

## Solution

- Adds `Measured2d` and `Measured3d` traits for getting the
perimeter/area or area/volume of shapes. This allows implementing
`.volume()` and `.area()` for all extrusions `Extrusion<T: Primitive2d +
Measured2d>` within `bevy_math`
- All existing perimeter, area and volume implementations for primitves
have been moved into implementations of `Measured2d` and `Measured3d`
- Shapes should be extruded along the Z-axis since an extrusion of depth
`0.` should be equivalent in everything but name to the base shape

## Caviats

- I am not sure about the naming. `Extrusion<T>` could also be
`Prism<T>` and the `MeasuredNd` could also be something like
`MeasuredPrimitiveNd`. If you have any other suggestions, please fell
free to share them :)

## Future work

This PR adds a basic `Extrusion` shape and does not implement a lot of
things you might want it to. Some of the future possibilities include:
- [ ] bounding for extrusions
- [ ] making extrusions work with gizmos
- [ ] meshing

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Lynn 2024-05-07 16:41:55 +02:00 committed by GitHub
parent 22305acf66
commit 03f4cc5dde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 269 additions and 165 deletions

View File

@ -1,6 +1,6 @@
use std::f32::consts::PI;
use super::{Primitive2d, WindingOrder};
use super::{Measured2d, Primitive2d, WindingOrder};
use crate::{Dir2, Vec2};
/// A circle primitive
@ -32,19 +32,6 @@ impl Circle {
2.0 * self.radius
}
/// Get the area of the circle
#[inline(always)]
pub fn area(&self) -> f32 {
PI * self.radius.powi(2)
}
/// Get the perimeter or circumference of the circle
#[inline(always)]
#[doc(alias = "circumference")]
pub fn perimeter(&self) -> f32 {
2.0 * PI * self.radius
}
/// Finds the point on the circle that is closest to the given `point`.
///
/// If the point is outside the circle, the returned point will be on the perimeter of the circle.
@ -65,6 +52,21 @@ impl Circle {
}
}
impl Measured2d for Circle {
/// Get the area of the circle
#[inline(always)]
fn area(&self) -> f32 {
PI * self.radius.powi(2)
}
/// Get the perimeter or circumference of the circle
#[inline(always)]
#[doc(alias = "circumference")]
fn perimeter(&self) -> f32 {
2.0 * PI * self.radius
}
}
/// An ellipse primitive
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -129,11 +131,31 @@ impl Ellipse {
(a * a - b * b).sqrt()
}
/// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse.
#[inline(always)]
pub fn semi_major(&self) -> f32 {
self.half_size.max_element()
}
/// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse.
#[inline(always)]
pub fn semi_minor(&self) -> f32 {
self.half_size.min_element()
}
}
impl Measured2d for Ellipse {
/// Get the area of the ellipse
#[inline(always)]
fn area(&self) -> f32 {
PI * self.half_size.x * self.half_size.y
}
#[inline(always)]
/// Get an approximation for the perimeter or circumference of the ellipse.
///
/// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases.
pub fn perimeter(&self) -> f32 {
fn perimeter(&self) -> f32 {
let a = self.semi_major();
let b = self.semi_minor();
@ -184,24 +206,6 @@ impl Ellipse {
.map(|i| BINOMIAL_COEFFICIENTS[i] * h.powi(i as i32))
.sum::<f32>()
}
/// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse.
#[inline(always)]
pub fn semi_major(&self) -> f32 {
self.half_size.max_element()
}
/// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse.
#[inline(always)]
pub fn semi_minor(&self) -> f32 {
self.half_size.min_element()
}
/// Get the area of the ellipse
#[inline(always)]
pub fn area(&self) -> f32 {
PI * self.half_size.x * self.half_size.y
}
}
/// A primitive shape formed by the region between two circles, also known as a ring.
@ -248,20 +252,6 @@ impl Annulus {
self.outer_circle.radius - self.inner_circle.radius
}
/// Get the area of the annulus
#[inline(always)]
pub fn area(&self) -> f32 {
PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2))
}
/// Get the perimeter or circumference of the annulus,
/// which is the sum of the perimeters of the inner and outer circles.
#[inline(always)]
#[doc(alias = "circumference")]
pub fn perimeter(&self) -> f32 {
2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius)
}
/// Finds the point on the annulus that is closest to the given `point`:
///
/// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter.
@ -290,6 +280,22 @@ impl Annulus {
}
}
impl Measured2d for Annulus {
/// Get the area of the annulus
#[inline(always)]
fn area(&self) -> f32 {
PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2))
}
/// Get the perimeter or circumference of the annulus,
/// which is the sum of the perimeters of the inner and outer circles.
#[inline(always)]
#[doc(alias = "circumference")]
fn perimeter(&self) -> f32 {
2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius)
}
}
/// An unbounded plane in 2D space. It forms a separating surface through the origin,
/// stretching infinitely far
#[derive(Clone, Copy, Debug, PartialEq)]
@ -471,25 +477,6 @@ impl Triangle2d {
}
}
/// Get the area of the triangle
#[inline(always)]
pub fn area(&self) -> f32 {
let [a, b, c] = self.vertices;
(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0
}
/// Get the perimeter of the triangle
#[inline(always)]
pub fn perimeter(&self) -> f32 {
let [a, b, c] = self.vertices;
let ab = a.distance(b);
let bc = b.distance(c);
let ca = c.distance(a);
ab + bc + ca
}
/// Get the [`WindingOrder`] of the triangle
#[inline(always)]
#[doc(alias = "orientation")]
@ -548,6 +535,27 @@ impl Triangle2d {
}
}
impl Measured2d for Triangle2d {
/// Get the area of the triangle
#[inline(always)]
fn area(&self) -> f32 {
let [a, b, c] = self.vertices;
(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0
}
/// Get the perimeter of the triangle
#[inline(always)]
fn perimeter(&self) -> f32 {
let [a, b, c] = self.vertices;
let ab = a.distance(b);
let bc = b.distance(c);
let ca = c.distance(a);
ab + bc + ca
}
}
/// A rectangle primitive
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -605,18 +613,6 @@ impl Rectangle {
2.0 * self.half_size
}
/// Get the area of the rectangle
#[inline(always)]
pub fn area(&self) -> f32 {
4.0 * self.half_size.x * self.half_size.y
}
/// Get the perimeter of the rectangle
#[inline(always)]
pub fn perimeter(&self) -> f32 {
4.0 * (self.half_size.x + self.half_size.y)
}
/// Finds the point on the rectangle that is closest to the given `point`.
///
/// If the point is outside the rectangle, the returned point will be on the perimeter of the rectangle.
@ -628,6 +624,20 @@ impl Rectangle {
}
}
impl Measured2d for Rectangle {
/// Get the area of the rectangle
#[inline(always)]
fn area(&self) -> f32 {
4.0 * self.half_size.x * self.half_size.y
}
/// Get the perimeter of the rectangle
#[inline(always)]
fn perimeter(&self) -> f32 {
4.0 * (self.half_size.x + self.half_size.y)
}
}
/// A polygon with N vertices.
///
/// For a version without generics: [`BoxedPolygon`]
@ -749,20 +759,6 @@ impl RegularPolygon {
2.0 * self.circumradius() * (PI / self.sides as f32).sin()
}
/// Get the area of the regular polygon
#[inline(always)]
pub fn area(&self) -> f32 {
let angle: f32 = 2.0 * PI / (self.sides as f32);
(self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0
}
/// Get the perimeter of the regular polygon.
/// This is the sum of its sides
#[inline(always)]
pub fn perimeter(&self) -> f32 {
self.sides as f32 * self.side_length()
}
/// Get the internal angle of the regular polygon in degrees.
///
/// This is the angle formed by two adjacent sides with points
@ -816,6 +812,22 @@ impl RegularPolygon {
}
}
impl Measured2d for RegularPolygon {
/// Get the area of the regular polygon
#[inline(always)]
fn area(&self) -> f32 {
let angle: f32 = 2.0 * PI / (self.sides as f32);
(self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0
}
/// Get the perimeter of the regular polygon.
/// This is the sum of its sides
#[inline(always)]
fn perimeter(&self) -> f32 {
self.sides as f32 * self.side_length()
}
}
/// 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

View File

@ -1,6 +1,6 @@
use std::f32::consts::{FRAC_PI_3, PI};
use super::{Circle, Primitive3d};
use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};
use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3};
/// A sphere primitive
@ -32,18 +32,6 @@ impl Sphere {
2.0 * self.radius
}
/// Get the surface area of the sphere
#[inline(always)]
pub fn area(&self) -> f32 {
4.0 * PI * self.radius.powi(2)
}
/// Get the volume of the sphere
#[inline(always)]
pub fn volume(&self) -> f32 {
4.0 * FRAC_PI_3 * self.radius.powi(3)
}
/// Finds the point on the sphere that is closest to the given `point`.
///
/// If the point is outside the sphere, the returned point will be on the surface of the sphere.
@ -64,6 +52,20 @@ impl Sphere {
}
}
impl Measured3d for Sphere {
/// Get the surface area of the sphere
#[inline(always)]
fn area(&self) -> f32 {
4.0 * PI * self.radius.powi(2)
}
/// Get the volume of the sphere
#[inline(always)]
fn volume(&self) -> f32 {
4.0 * FRAC_PI_3 * self.radius.powi(3)
}
}
/// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -360,20 +362,6 @@ impl Cuboid {
2.0 * self.half_size
}
/// Get the surface area of the cuboid
#[inline(always)]
pub fn area(&self) -> f32 {
8.0 * (self.half_size.x * self.half_size.y
+ self.half_size.y * self.half_size.z
+ self.half_size.x * self.half_size.z)
}
/// Get the volume of the cuboid
#[inline(always)]
pub fn volume(&self) -> f32 {
8.0 * self.half_size.x * self.half_size.y * self.half_size.z
}
/// Finds the point on the cuboid that is closest to the given `point`.
///
/// If the point is outside the cuboid, the returned point will be on the surface of the cuboid.
@ -385,6 +373,22 @@ impl Cuboid {
}
}
impl Measured3d for Cuboid {
/// Get the surface area of the cuboid
#[inline(always)]
fn area(&self) -> f32 {
8.0 * (self.half_size.x * self.half_size.y
+ self.half_size.y * self.half_size.z
+ self.half_size.x * self.half_size.z)
}
/// Get the volume of the cuboid
#[inline(always)]
fn volume(&self) -> f32 {
8.0 * self.half_size.x * self.half_size.y * self.half_size.z
}
}
/// A cylinder primitive
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -437,16 +441,18 @@ impl Cylinder {
pub fn base_area(&self) -> f32 {
PI * self.radius.powi(2)
}
}
impl Measured3d for Cylinder {
/// Get the total surface area of the cylinder
#[inline(always)]
pub fn area(&self) -> f32 {
fn area(&self) -> f32 {
2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)
}
/// Get the volume of the cylinder
#[inline(always)]
pub fn volume(&self) -> f32 {
fn volume(&self) -> f32 {
self.base_area() * 2.0 * self.half_height
}
}
@ -492,17 +498,19 @@ impl Capsule3d {
half_height: self.half_length,
}
}
}
impl Measured3d for Capsule3d {
/// Get the surface area of the capsule
#[inline(always)]
pub fn area(&self) -> f32 {
fn area(&self) -> f32 {
// Modified version of 2pi * r * (2r + h)
4.0 * PI * self.radius * (self.radius + self.half_length)
}
/// Get the volume of the capsule
#[inline(always)]
pub fn volume(&self) -> f32 {
fn volume(&self) -> f32 {
// Modified version of pi * r^2 * (4/3 * r + a)
let diameter = self.radius * 2.0;
PI * self.radius * diameter * (diameter / 3.0 + self.half_length)
@ -550,16 +558,18 @@ impl Cone {
pub fn base_area(&self) -> f32 {
PI * self.radius.powi(2)
}
}
impl Measured3d for Cone {
/// Get the total surface area of the cone
#[inline(always)]
pub fn area(&self) -> f32 {
fn area(&self) -> f32 {
self.base_area() + self.lateral_area()
}
/// Get the volume of the cone
#[inline(always)]
pub fn volume(&self) -> f32 {
fn volume(&self) -> f32 {
(self.base_area() * self.height) / 3.0
}
}
@ -681,18 +691,20 @@ impl Torus {
std::cmp::Ordering::Less => TorusKind::Spindle,
}
}
}
impl Measured3d for Torus {
/// Get the surface area of the torus. Note that this only produces
/// the expected result when the torus has a ring and isn't self-intersecting
#[inline(always)]
pub fn area(&self) -> f32 {
fn area(&self) -> f32 {
4.0 * PI.powi(2) * self.major_radius * self.minor_radius
}
/// Get the volume of the torus. Note that this only produces
/// the expected result when the torus has a ring and isn't self-intersecting
#[inline(always)]
pub fn volume(&self) -> f32 {
fn volume(&self) -> f32 {
2.0 * PI.powi(2) * self.major_radius * self.minor_radius.powi(2)
}
}
@ -729,22 +741,6 @@ impl Triangle3d {
}
}
/// Get the area of the triangle.
#[inline(always)]
pub fn area(&self) -> f32 {
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
ab.cross(ac).length() / 2.0
}
/// Get the perimeter of the triangle.
#[inline(always)]
pub fn perimeter(&self) -> f32 {
let [a, b, c] = self.vertices;
a.distance(b) + b.distance(c) + c.distance(a)
}
/// Get the normal of the triangle in the direction of the right-hand rule, assuming
/// the vertices are ordered in a counter-clockwise direction.
///
@ -835,6 +831,24 @@ impl Triangle3d {
}
}
impl Measured2d for Triangle3d {
/// Get the area of the triangle.
#[inline(always)]
fn area(&self) -> f32 {
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
ab.cross(ac).length() / 2.0
}
/// Get the perimeter of the triangle.
#[inline(always)]
fn perimeter(&self) -> f32 {
let [a, b, c] = self.vertices;
a.distance(b) + b.distance(c) + c.distance(a)
}
}
/// A tetrahedron primitive.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -868,28 +882,6 @@ impl Tetrahedron {
}
}
/// Get the surface area of the tetrahedron.
#[inline(always)]
pub fn area(&self) -> f32 {
let [a, b, c, d] = self.vertices;
let ab = b - a;
let ac = c - a;
let ad = d - a;
let bc = c - b;
let bd = d - b;
(ab.cross(ac).length()
+ ab.cross(ad).length()
+ ac.cross(ad).length()
+ bc.cross(bd).length())
/ 2.0
}
/// Get the volume of the tetrahedron.
#[inline(always)]
pub fn volume(&self) -> f32 {
self.signed_volume().abs()
}
/// Get the signed volume of the tetrahedron.
///
/// If it's negative, the normal vector of the face defined by
@ -915,6 +907,70 @@ impl Tetrahedron {
}
}
impl Measured3d for Tetrahedron {
/// Get the surface area of the tetrahedron.
#[inline(always)]
fn area(&self) -> f32 {
let [a, b, c, d] = self.vertices;
let ab = b - a;
let ac = c - a;
let ad = d - a;
let bc = c - b;
let bd = d - b;
(ab.cross(ac).length()
+ ab.cross(ad).length()
+ ac.cross(ad).length()
+ bc.cross(bd).length())
/ 2.0
}
/// Get the volume of the tetrahedron.
#[inline(always)]
fn volume(&self) -> f32 {
self.signed_volume().abs()
}
}
/// A 3D shape representing an extruded 2D `base_shape`.
///
/// Extruding a shape effectively "thickens" a 2D shapes,
/// creating a shape with the same cross-section over the entire depth.
///
/// The resulting volumes are prisms.
/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder.
#[doc(alias = "Prism")]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Extrusion<T: Primitive2d> {
/// The base shape of the extrusion
pub base_shape: T,
/// Half of the depth of the extrusion
pub half_depth: f32,
}
impl<T: Primitive2d> Primitive3d for Extrusion<T> {}
impl<T: Primitive2d> Extrusion<T> {
/// Create a new `Extrusion<T>` from a given `base_shape` and `depth`
pub fn new(base_shape: T, depth: f32) -> Self {
Self {
base_shape,
half_depth: depth / 2.,
}
}
}
impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {
/// Get the surface area of the extrusion
fn area(&self) -> f32 {
2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())
}
/// Get the volume of the extrusion
fn volume(&self) -> f32 {
2. * self.base_shape.area() * self.half_depth
}
}
#[cfg(test)]
mod tests {
// Reference values were computed by hand and/or with external tools
@ -1146,4 +1202,22 @@ mod tests {
let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE);
assert!(degenerate.is_degenerate(), "did not find degenerate");
}
#[test]
fn extrusion_math() {
let circle = Circle::new(0.75);
let cylinder = Extrusion::new(circle, 2.5);
assert_eq!(cylinder.area(), 15.315264, "incorrect surface area");
assert_eq!(cylinder.volume(), 4.417865, "incorrect volume");
let annulus = crate::primitives::Annulus::new(0.25, 1.375);
let tube = Extrusion::new(annulus, 0.333);
assert_eq!(tube.area(), 14.886437, "incorrect surface area");
assert_eq!(tube.volume(), 1.9124937, "incorrect volume");
let polygon = crate::primitives::RegularPolygon::new(3.8, 7);
let regular_prism = Extrusion::new(polygon, 1.25);
assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area");
assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume");
}
}

View File

@ -29,3 +29,21 @@ pub enum WindingOrder {
#[doc(alias("Degenerate", "Collinear"))]
Invalid,
}
/// A trait for getting measurements of 2D shapes
pub trait Measured2d {
/// Get the perimeter of the shape
fn perimeter(&self) -> f32;
/// Get the area of the shape
fn area(&self) -> f32;
}
/// A trait for getting measurements of 3D shapes
pub trait Measured3d {
/// Get the surface area of the shape
fn area(&self) -> f32;
/// Get the volume of the shape
fn volume(&self) -> f32;
}