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)]