From 92567490a9b67fae80f85c1f09bee926cb545bed Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Mon, 29 Jan 2024 18:04:51 +0200 Subject: [PATCH] Add more constructors and math helpers for primitive shapes (#10632) # Objective Working towards finishing a part of #10572, this PR adds a ton of math helpers and useful constructors for primitive shapes. I also tried fixing some naming inconsistencies. ## Solution - Add mathematical helpers like `area`, `volume`, `perimeter`, `RegularPolygon::inradius` and so on, trying to cover all core mathematical properties of each shape - Add some constructors like `Rectangle::from_corners`, `Cuboid::from_corners` and `Plane3d::from_points` I also derived `PartialEq` for the shapes where it's trivial. Primitives like `Line2d` and `Segment2d` are not trivial because you could argue that they would be equal if they had an opposite direction. All mathematical methods have tests with reference values computed by hand or with external tools. ## Todo - [x] Add tests to verify that the values from mathematical helpers are correct --------- Co-authored-by: IQuick 143 --- crates/bevy_math/Cargo.toml | 3 + crates/bevy_math/src/primitives/dim2.rs | 262 +++++++++++++++++-- crates/bevy_math/src/primitives/dim3.rs | 334 +++++++++++++++++++++++- 3 files changed, 567 insertions(+), 32 deletions(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 48b7959d481f0..9c696b9a09474 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -12,6 +12,9 @@ keywords = ["bevy"] glam = { version = "0.25", features = ["bytemuck"] } serde = { version = "1", features = ["derive"], optional = true } +[dev-dependencies] +approx = "0.5" + [features] serialize = ["dep:serde", "glam/serde"] # Enable approx for glam types to approximate floating point equality comparisons and assertions diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 579fdc0cd0766..c7b87afa68735 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,3 +1,5 @@ +use std::f32::consts::PI; + use super::{InvalidDirectionError, Primitive2d, WindingOrder}; use crate::Vec2; @@ -95,6 +97,25 @@ impl Circle { Self { radius } } + /// Get the diameter of the circle + #[inline(always)] + pub fn diameter(&self) -> f32 { + 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. @@ -130,7 +151,7 @@ impl Ellipse { /// Create a new `Ellipse` from half of its width and height. /// /// This corresponds to the two perpendicular radii defining the ellipse. - #[inline] + #[inline(always)] pub const fn new(half_width: f32, half_height: f32) -> Self { Self { half_size: Vec2::new(half_width, half_height), @@ -140,7 +161,7 @@ impl Ellipse { /// Create a new `Ellipse` from a given full size. /// /// `size.x` is the diameter along the X axis, and `size.y` is the diameter along the Y axis. - #[inline] + #[inline(always)] pub fn from_size(size: Vec2) -> Self { Self { half_size: size / 2.0, @@ -148,16 +169,22 @@ impl Ellipse { } /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. - #[inline] + #[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] + #[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 + } } /// An unbounded plane in 2D space. It forms a separating surface through the origin, @@ -176,7 +203,7 @@ impl Plane2d { /// # Panics /// /// Panics if the given `normal` is zero (or very close to zero), or non-finite. - #[inline] + #[inline(always)] pub fn new(normal: Vec2) -> Self { Self { normal: Direction2d::new(normal).expect("normal must be nonzero and finite"), @@ -197,9 +224,9 @@ pub struct Line2d { impl Primitive2d for Line2d {} /// A segment of a line along a direction in 2D space. -#[doc(alias = "LineSegment2d")] #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[doc(alias = "LineSegment2d")] pub struct Segment2d { /// The direction of the line segment pub direction: Direction2d, @@ -210,17 +237,21 @@ pub struct Segment2d { impl Primitive2d for Segment2d {} impl Segment2d { - /// Create a line segment from a direction and full length of the segment + /// Create a new `Segment2d` from a direction and full length of the segment + #[inline(always)] pub fn new(direction: Direction2d, length: f32) -> Self { Self { direction, - half_length: length / 2., + half_length: length / 2.0, } } - /// Get a line segment and translation from two points at each end of a line segment + /// Create a new `Segment2d` from its endpoints and compute its geometric center + /// + /// # Panics /// - /// Panics if point1 == point2 + /// Panics if `point1 == point2` + #[inline(always)] pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) { let diff = point2 - point1; let length = diff.length(); @@ -233,11 +264,13 @@ impl Segment2d { } /// Get the position of the first point on the line segment + #[inline(always)] pub fn point1(&self) -> Vec2 { *self.direction * -self.half_length } /// Get the position of the second point on the line segment + #[inline(always)] pub fn point2(&self) -> Vec2 { *self.direction * self.half_length } @@ -312,13 +345,34 @@ impl Primitive2d for Triangle2d {} impl Triangle2d { /// Create a new `Triangle2d` from points `a`, `b`, and `c` + #[inline(always)] pub fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { Self { vertices: [a, b, c], } } + /// 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")] pub fn winding_order(&self) -> WindingOrder { let [a, b, c] = self.vertices; @@ -369,33 +423,62 @@ impl Triangle2d { /// Reverse the [`WindingOrder`] of the triangle /// by swapping the second and third vertices + #[inline(always)] pub fn reverse(&mut self) { self.vertices.swap(1, 2); } } /// A rectangle primitive -#[doc(alias = "Quad")] #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[doc(alias = "Quad")] pub struct Rectangle { /// Half of the width and height of the rectangle pub half_size: Vec2, } impl Rectangle { - /// Create a rectangle from a full width and height + /// Create a new `Rectangle` from a full width and height + #[inline(always)] pub fn new(width: f32, height: f32) -> Self { Self::from_size(Vec2::new(width, height)) } - /// Create a rectangle from a given full size + /// Create a new `Rectangle` from a given full size + #[inline(always)] pub fn from_size(size: Vec2) -> Self { Self { - half_size: size / 2., + half_size: size / 2.0, + } + } + + /// Create a new `Rectangle` from two corner points + #[inline(always)] + pub fn from_corners(point1: Vec2, point2: Vec2) -> Self { + Self { + half_size: (point2 - point1).abs() / 2.0, } } + /// Get the size of the rectangle + #[inline(always)] + pub fn size(&self) -> Vec2 { + 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. @@ -482,17 +565,92 @@ impl RegularPolygon { /// /// # Panics /// - /// Panics if `circumcircle_radius` is non-positive - pub fn new(circumcircle_radius: f32, sides: usize) -> Self { - assert!(circumcircle_radius > 0.0); + /// Panics if `circumradius` is non-positive + #[inline(always)] + pub fn new(circumradius: f32, sides: usize) -> Self { + assert!(circumradius > 0.0, "polygon has a non-positive radius"); + assert!(sides > 2, "polygon has less than 3 sides"); + Self { circumcircle: Circle { - radius: circumcircle_radius, + radius: circumradius, }, sides, } } + /// Get the radius of the circumcircle on which all vertices + /// of the regular polygon lie + #[inline(always)] + pub fn circumradius(&self) -> f32 { + self.circumcircle.radius + } + + /// Get the inradius or apothem of the regular polygon. + /// This is the radius of the largest circle that can + /// be drawn within the polygon + #[inline(always)] + #[doc(alias = "apothem")] + pub fn inradius(&self) -> f32 { + self.circumradius() * (PI / self.sides as f32).cos() + } + + /// Get the length of one side of the regular polygon + #[inline(always)] + pub fn side_length(&self) -> f32 { + 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 + /// within the angle being in the interior of the polygon + #[inline(always)] + pub fn internal_angle_degrees(&self) -> f32 { + (self.sides - 2) as f32 / self.sides as f32 * 180.0 + } + + /// Get the internal angle of the regular polygon in radians. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the interior of the polygon + #[inline(always)] + pub fn internal_angle_radians(&self) -> f32 { + (self.sides - 2) as f32 * PI / self.sides as f32 + } + + /// Get the external angle of the regular polygon in degrees. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the exterior of the polygon + #[inline(always)] + pub fn external_angle_degrees(&self) -> f32 { + 360.0 / self.sides as f32 + } + + /// Get the external angle of the regular polygon in radians. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the exterior of the polygon + #[inline(always)] + pub fn external_angle_radians(&self) -> f32 { + 2.0 * PI / self.sides as f32 + } + /// Returns an iterator over the vertices of the regular polygon, /// rotated counterclockwise by the given angle in radians. /// @@ -512,7 +670,35 @@ impl RegularPolygon { #[cfg(test)] mod tests { + // Reference values were computed by hand and/or with external tools + use super::*; + use approx::assert_relative_eq; + + #[test] + fn circle_math() { + let circle = Circle { radius: 3.0 }; + assert_eq!(circle.diameter(), 6.0, "incorrect diameter"); + assert_eq!(circle.area(), 28.274334, "incorrect area"); + assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); + } + + #[test] + fn ellipse_math() { + let ellipse = Ellipse::new(3.0, 1.0); + assert_eq!(ellipse.area(), 9.424778, "incorrect area"); + } + + #[test] + fn triangle_math() { + let triangle = Triangle2d::new( + Vec2::new(-2.0, -1.0), + Vec2::new(1.0, 4.0), + Vec2::new(7.0, 0.0), + ); + assert_eq!(triangle.area(), 21.0, "incorrect area"); + assert_eq!(triangle.perimeter(), 22.097439, "incorrect perimeter"); + } #[test] fn direction_creation() { @@ -568,6 +754,46 @@ mod tests { assert_eq!(invalid_triangle.winding_order(), WindingOrder::Invalid); } + #[test] + fn rectangle_math() { + let rectangle = Rectangle::new(3.0, 7.0); + assert_eq!( + rectangle, + Rectangle::from_corners(Vec2::new(-1.5, -3.5), Vec2::new(1.5, 3.5)) + ); + assert_eq!(rectangle.area(), 21.0, "incorrect area"); + assert_eq!(rectangle.perimeter(), 20.0, "incorrect perimeter"); + } + + #[test] + fn regular_polygon_math() { + let polygon = RegularPolygon::new(3.0, 6); + assert_eq!(polygon.inradius(), 2.598076, "incorrect inradius"); + assert_eq!(polygon.side_length(), 3.0, "incorrect side length"); + assert_relative_eq!(polygon.area(), 23.38268, epsilon = 0.00001); + assert_eq!(polygon.perimeter(), 18.0, "incorrect perimeter"); + assert_eq!( + polygon.internal_angle_degrees(), + 120.0, + "incorrect internal angle" + ); + assert_eq!( + polygon.internal_angle_radians(), + 120_f32.to_radians(), + "incorrect internal angle" + ); + assert_eq!( + polygon.external_angle_degrees(), + 60.0, + "incorrect external angle" + ); + assert_eq!( + polygon.external_angle_radians(), + 60_f32.to_radians(), + "incorrect external angle" + ); + } + #[test] fn triangle_circumcenter() { let triangle = Triangle2d::new( diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 56f3f46167fb4..62ccd588be69f 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,4 +1,6 @@ -use super::{InvalidDirectionError, Primitive3d}; +use std::f32::consts::{FRAC_PI_3, PI}; + +use super::{Circle, InvalidDirectionError, Primitive3d}; use crate::Vec3; /// A normalized vector pointing in a direction in 3D space @@ -99,6 +101,24 @@ impl Sphere { Self { radius } } + /// Get the diameter of the sphere + #[inline(always)] + pub fn diameter(&self) -> f32 { + 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. @@ -135,12 +155,31 @@ impl Plane3d { /// # Panics /// /// Panics if the given `normal` is zero (or very close to zero), or non-finite. - #[inline] + #[inline(always)] pub fn new(normal: Vec3) -> Self { Self { normal: Direction3d::new(normal).expect("normal must be nonzero and finite"), } } + + /// Create a new `Plane3d` based on three points and compute the geometric center + /// of those points. + /// + /// The direction of the plane normal is determined by the winding order + /// of the triangular shape formed by the points. + /// + /// # Panics + /// + /// Panics if a valid normal can not be computed, for example when the points + /// are *collinear* and lie on the same line. + #[inline(always)] + pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { + let normal = Direction3d::new((b - a).cross(c - a)) + .expect("plane must be defined by three finite points that don't lie on the same line"); + let translation = (a + b + c) / 3.0; + + (Self { normal }, translation) + } } /// An infinite line along a direction in 3D space. @@ -168,17 +207,21 @@ pub struct Segment3d { impl Primitive3d for Segment3d {} impl Segment3d { - /// Create a line segment from a direction and full length of the segment + /// Create a new `Segment3d` from a direction and full length of the segment + #[inline(always)] pub fn new(direction: Direction3d, length: f32) -> Self { Self { direction, - half_length: length / 2., + half_length: length / 2.0, } } - /// Get a line segment and translation from two points at each end of a line segment + /// Create a new `Segment3d` from its endpoints and compute its geometric center + /// + /// # Panics /// - /// Panics if point1 == point2 + /// Panics if `point1 == point2` + #[inline(always)] pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) { let diff = point2 - point1; let length = diff.length(); @@ -191,11 +234,13 @@ impl Segment3d { } /// Get the position of the first point on the line segment + #[inline(always)] pub fn point1(&self) -> Vec3 { *self.direction * -self.half_length } /// Get the position of the second point on the line segment + #[inline(always)] pub fn point2(&self) -> Vec3 { *self.direction * self.half_length } @@ -269,18 +314,48 @@ pub struct Cuboid { impl Primitive3d for Cuboid {} impl Cuboid { - /// Create a cuboid from a full x, y, and z length + /// Create a new `Cuboid` from a full x, y, and z length + #[inline(always)] 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 a given full size + /// Create a new `Cuboid` from a given full size + #[inline(always)] pub fn from_size(size: Vec3) -> Self { Self { - half_size: size / 2., + half_size: size / 2.0, } } + /// Create a new `Cuboid` from two corner points + #[inline(always)] + pub fn from_corners(point1: Vec3, point2: Vec3) -> Self { + Self { + half_size: (point2 - point1).abs() / 2.0, + } + } + + /// Get the size of the cuboid + #[inline(always)] + pub fn size(&self) -> Vec3 { + 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. @@ -304,13 +379,48 @@ pub struct Cylinder { impl Primitive3d for Cylinder {} impl Cylinder { - /// Create a cylinder from a radius and full height + /// Create a new `Cylinder` from a radius and full height + #[inline(always)] pub fn new(radius: f32, height: f32) -> Self { Self { radius, - half_height: height / 2., + half_height: height / 2.0, + } + } + + /// Get the base of the cylinder as a [`Circle`] + #[inline(always)] + pub fn base(&self) -> Circle { + Circle { + radius: self.radius, } } + + /// Get the surface area of the side of the cylinder, + /// also known as the lateral area + #[inline(always)] + #[doc(alias = "side_area")] + pub fn lateral_area(&self) -> f32 { + 4.0 * PI * self.radius * self.half_height + } + + /// Get the surface area of one base of the cylinder + #[inline(always)] + pub fn base_area(&self) -> f32 { + PI * self.radius.powi(2) + } + + /// Get the total surface area of the cylinder + #[inline(always)] + pub 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 { + self.base_area() * 2.0 * self.half_height + } } /// A capsule primitive. @@ -328,12 +438,38 @@ impl Primitive3d for Capsule {} impl Capsule { /// Create a new `Capsule` from a radius and length + #[inline(always)] pub fn new(radius: f32, length: f32) -> Self { Self { radius, half_length: length / 2.0, } } + + /// Get the part connecting the hemispherical ends + /// of the capsule as a [`Cylinder`] + #[inline(always)] + pub fn to_cylinder(&self) -> Cylinder { + Cylinder { + radius: self.radius, + half_height: self.half_length, + } + } + + /// Get the surface area of the capsule + #[inline(always)] + pub 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 { + // 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) + } } /// A cone primitive. @@ -347,6 +483,50 @@ pub struct Cone { } impl Primitive3d for Cone {} +impl Cone { + /// Get the base of the cone as a [`Circle`] + #[inline(always)] + pub fn base(&self) -> Circle { + Circle { + radius: self.radius, + } + } + + /// Get the slant height of the cone, the length of the line segment + /// connecting a point on the base to the apex + #[inline(always)] + #[doc(alias = "side_length")] + pub fn slant_height(&self) -> f32 { + self.radius.hypot(self.height) + } + + /// Get the surface area of the side of the cone, + /// also known as the lateral area + #[inline(always)] + #[doc(alias = "side_area")] + pub fn lateral_area(&self) -> f32 { + PI * self.radius * self.slant_height() + } + + /// Get the surface area of the base of the cone + #[inline(always)] + pub fn base_area(&self) -> f32 { + PI * self.radius.powi(2) + } + + /// Get the total surface area of the cone + #[inline(always)] + pub fn area(&self) -> f32 { + self.base_area() + self.lateral_area() + } + + /// Get the volume of the cone + #[inline(always)] + pub fn volume(&self) -> f32 { + (self.base_area() * self.height) / 3.0 + } +} + /// A conical frustum primitive. /// A conical frustum can be created /// by slicing off a section of a cone. @@ -402,6 +582,7 @@ impl Torus { /// /// The inner radius is the radius of the hole, and the outer radius /// is the radius of the entire object + #[inline(always)] pub fn new(inner_radius: f32, outer_radius: f32) -> Self { let minor_radius = (outer_radius - inner_radius) / 2.0; let major_radius = outer_radius - minor_radius; @@ -415,7 +596,7 @@ impl Torus { /// Get the inner radius of the torus. /// For a ring torus, this corresponds to the radius of the hole, /// or `major_radius - minor_radius` - #[inline] + #[inline(always)] pub fn inner_radius(&self) -> f32 { self.major_radius - self.minor_radius } @@ -423,7 +604,7 @@ impl Torus { /// Get the outer radius of the torus. /// This corresponds to the overall radius of the entire object, /// or `major_radius + minor_radius` - #[inline] + #[inline(always)] pub fn outer_radius(&self) -> f32 { self.major_radius + self.minor_radius } @@ -436,7 +617,7 @@ impl Torus { /// /// If the minor or major radius is non-positive, infinite, or `NaN`, /// [`TorusKind::Invalid`] is returned - #[inline] + #[inline(always)] pub fn kind(&self) -> TorusKind { // Invalid if minor or major radius is non-positive, infinite, or NaN if self.minor_radius <= 0.0 @@ -453,6 +634,131 @@ impl Torus { std::cmp::Ordering::Less => TorusKind::Spindle, } } + + /// 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 { + 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 { + 2.0 * PI.powi(2) * self.major_radius * self.minor_radius.powi(2) + } +} + +#[cfg(test)] +mod tests { + // Reference values were computed by hand and/or with external tools + + use super::*; + use approx::assert_relative_eq; + + #[test] + fn sphere_math() { + let sphere = Sphere { radius: 4.0 }; + assert_eq!(sphere.diameter(), 8.0, "incorrect diameter"); + assert_eq!(sphere.area(), 201.06193, "incorrect area"); + assert_eq!(sphere.volume(), 268.08257, "incorrect volume"); + } + + #[test] + fn plane_from_points() { + let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); + assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); + assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); + } + + #[test] + fn cuboid_math() { + let cuboid = Cuboid::new(3.0, 7.0, 2.0); + assert_eq!( + cuboid, + Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)), + "incorrect dimensions when created from corners" + ); + assert_eq!(cuboid.area(), 82.0, "incorrect area"); + assert_eq!(cuboid.volume(), 42.0, "incorrect volume"); + } + + #[test] + fn cylinder_math() { + let cylinder = Cylinder::new(2.0, 9.0); + assert_eq!( + cylinder.base(), + Circle { radius: 2.0 }, + "base produces incorrect circle" + ); + assert_eq!( + cylinder.lateral_area(), + 113.097336, + "incorrect lateral area" + ); + assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area"); + assert_relative_eq!(cylinder.area(), 138.23007); + assert_eq!(cylinder.volume(), 113.097336, "incorrect volume"); + } + + #[test] + fn capsule_math() { + let capsule = Capsule::new(2.0, 9.0); + assert_eq!( + capsule.to_cylinder(), + Cylinder::new(2.0, 9.0), + "cylinder wasn't created correctly from a capsule" + ); + assert_eq!(capsule.area(), 163.36282, "incorrect area"); + assert_relative_eq!(capsule.volume(), 146.60765); + } + + #[test] + fn cone_math() { + let cone = Cone { + radius: 2.0, + height: 9.0, + }; + assert_eq!( + cone.base(), + Circle { radius: 2.0 }, + "base produces incorrect circle" + ); + assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height"); + assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area"); + assert_eq!(cone.base_area(), 12.566371, "incorrect base area"); + assert_relative_eq!(cone.area(), 70.49447); + assert_eq!(cone.volume(), 37.699111, "incorrect volume"); + } + + #[test] + fn torus_math() { + let torus = Torus { + minor_radius: 0.3, + major_radius: 2.8, + }; + assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius"); + assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius"); + assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind"); + assert_eq!( + Torus::new(0.0, 1.0).kind(), + TorusKind::Horn, + "incorrect torus kind" + ); + assert_eq!( + Torus::new(-0.5, 1.0).kind(), + TorusKind::Spindle, + "incorrect torus kind" + ); + assert_eq!( + Torus::new(1.5, 1.0).kind(), + TorusKind::Invalid, + "torus should be invalid" + ); + assert_relative_eq!(torus.area(), 33.16187); + assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001); + } } #[cfg(test)]