Math primitives cleanup (#13020)
# Objective - General clenup of the primitives in `bevy_math` - Add `eccentricity()` to `Ellipse` ## Solution - Moved `Bounded3d` implementation for `Triangle3d` to the `bounded` module - Added `eccentricity()` to `Ellipse` - `Ellipse::semi_major()` and `::semi_minor()` now accept `&self` instead of `self` - `Triangle3d::is_degenerate()` actually uses `f32::EPSILON` as documented - Added tests for `Triangle3d`-maths --------- Co-authored-by: Joona Aalto <jondolf.dev@gmail.com> Co-authored-by: Miles Silberling-Cook <nth.tensor@gmail.com>
This commit is contained in:
parent
f68bc01544
commit
cd80b10d43
@ -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};
|
||||
|
@ -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]
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user