diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 8dec29c23a..26155f3208 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -4,7 +4,7 @@ use crate::{ bounding::{Bounded2d, BoundingCircle}, primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, - Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, + Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, }, Dir3, Mat3, Quat, Vec2, Vec3, }; @@ -303,6 +303,59 @@ impl Bounded3d for Torus { } } +impl Bounded3d for Triangle3d { + /// Get the bounding box of the triangle. + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + let [a, b, c] = self.vertices; + + let a = rotation * a; + let b = rotation * b; + let c = rotation * c; + + let min = a.min(b).min(c); + let max = a.max(b).max(c); + + let bounding_center = (max + min) / 2.0 + translation; + let half_extents = (max - min) / 2.0; + + Aabb3d::new(bounding_center, half_extents) + } + + /// Get the bounding sphere of the triangle. + /// + /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as + /// the center of the sphere. For the others, the bounding sphere is the minimal sphere + /// that contains the largest side of the triangle. + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + if self.is_degenerate() { + let (p1, p2) = self.largest_side(); + let (segment, _) = Segment3d::from_points(p1, p2); + return segment.bounding_sphere(translation, rotation); + } + + let [a, b, c] = self.vertices; + + let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { + Some((b, c)) + } else if (c - b).dot(a - b) <= 0.0 { + Some((c, a)) + } else if (a - c).dot(b - c) <= 0.0 { + Some((a, b)) + } else { + None + }; + + if let Some((p1, p2)) = side_opposite_to_non_acute { + let (segment, _) = Segment3d::from_points(p1, p2); + segment.bounding_sphere(translation, rotation) + } else { + let circumcenter = self.circumcenter(); + let radius = circumcenter.distance(a); + BoundingSphere::new(circumcenter + translation, radius) + } + } +} + #[cfg(test)] mod tests { use glam::{Quat, Vec3}; diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 4ae6932eb1..14660edc5b 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -106,15 +106,27 @@ impl Ellipse { } } + #[inline(always)] + /// Returns the [eccentricity](https://en.wikipedia.org/wiki/Eccentricity_(mathematics)) of the ellipse. + /// It can be thought of as a measure of how "stretched" or elongated the ellipse is. + /// + /// The value should be in the range [0, 1), where 0 represents a circle, and 1 represents a parabola. + pub fn eccentricity(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + (a * a - b * b).sqrt() / a + } + /// 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 { + 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 { + pub fn semi_minor(&self) -> f32 { self.half_size.min_element() } @@ -839,6 +851,14 @@ mod tests { fn ellipse_math() { let ellipse = Ellipse::new(3.0, 1.0); assert_eq!(ellipse.area(), 9.424778, "incorrect area"); + + assert_eq!(ellipse.eccentricity(), 0.94280905, "incorrect eccentricity"); + + let line = Ellipse::new(1., 0.); + assert_eq!(line.eccentricity(), 1., "incorrect line eccentricity"); + + let circle = Ellipse::new(2., 2.); + assert_eq!(circle.eccentricity(), 0., "incorrect circle eccentricity"); } #[test] diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index a974f61d60..b3308acbf8 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,10 +1,7 @@ use std::f32::consts::{FRAC_PI_3, PI}; use super::{Circle, Primitive3d}; -use crate::{ - bounding::{Aabb3d, Bounded3d, BoundingSphere}, - Dir3, InvalidDirectionError, Mat3, Quat, Vec2, Vec3, -}; +use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3}; /// A sphere primitive #[derive(Clone, Copy, Debug, PartialEq)] @@ -767,7 +764,7 @@ impl Triangle3d { /// Checks if the triangle is degenerate, meaning it has zero area. /// - /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `f32::EPSILON`. + /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. /// This indicates that the three vertices are collinear or nearly collinear. #[inline(always)] pub fn is_degenerate(&self) -> bool { @@ -838,59 +835,6 @@ impl Triangle3d { } } -impl Bounded3d for Triangle3d { - /// Get the bounding box of the triangle. - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { - let [a, b, c] = self.vertices; - - let a = rotation * a; - let b = rotation * b; - let c = rotation * c; - - let min = a.min(b).min(c); - let max = a.max(b).max(c); - - let bounding_center = (max + min) / 2.0 + translation; - let half_extents = (max - min) / 2.0; - - Aabb3d::new(bounding_center, half_extents) - } - - /// Get the bounding sphere of the triangle. - /// - /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as - /// the center of the sphere. For the others, the bounding sphere is the minimal sphere - /// that contains the largest side of the triangle. - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { - if self.is_degenerate() { - let (p1, p2) = self.largest_side(); - let (segment, _) = Segment3d::from_points(p1, p2); - return segment.bounding_sphere(translation, rotation); - } - - let [a, b, c] = self.vertices; - - let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { - Some((b, c)) - } else if (c - b).dot(a - b) <= 0.0 { - Some((c, a)) - } else if (a - c).dot(b - c) <= 0.0 { - Some((a, b)) - } else { - None - }; - - if let Some((p1, p2)) = side_opposite_to_non_acute { - let (segment, _) = Segment3d::from_points(p1, p2); - segment.bounding_sphere(translation, rotation) - } else { - let circumcenter = self.circumcenter(); - let radius = circumcenter.distance(a); - BoundingSphere::new(circumcenter + translation, radius) - } - } -} - /// A tetrahedron primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -976,6 +920,7 @@ mod tests { // Reference values were computed by hand and/or with external tools use super::*; + use crate::Quat; use approx::assert_relative_eq; #[test] @@ -1174,4 +1119,31 @@ mod tests { ); assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO); } + + #[test] + fn triangle_math() { + let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)]; + let triangle = Triangle3d::new(a, b, c); + + assert!(!triangle.is_degenerate(), "incorrectly found degenerate"); + assert_eq!(triangle.area(), 3.0233467, "incorrect area"); + assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter"); + assert_eq!( + triangle.circumcenter(), + Vec3::new(-1., 1.75, 0.75), + "incorrect circumcenter" + ); + assert_eq!( + triangle.normal(), + Ok(Dir3::new_unchecked(Vec3::new( + -0.04134491, + -0.4134491, + 0.90958804 + ))), + "incorrect normal" + ); + + let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE); + assert!(degenerate.is_degenerate(), "did not find degenerate"); + } }