Add triangle_math tests and fix Triangle3d::bounding_sphere bug (#13467)
# Objective Adopted #12659. Resolved the merge conflicts on #12659; * I merged the `triangle_tests` added by this PR and by #13020. * I moved the [commented out code](https://github.com/bevyengine/bevy/pull/12659#discussion_r1536640427) from the original PR into a separate test with `#[should_panic]`. --------- Co-authored-by: Vitor Falcao <vitorfhc@protonmail.com> Co-authored-by: Ben Harper <ben@tukom.org>
This commit is contained in:
parent
1d950e6195
commit
bd5148e0f5
@ -323,29 +323,15 @@ impl Bounded3d for Triangle3d {
|
||||
/// 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() {
|
||||
fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
|
||||
if self.is_degenerate() || self.is_obtuse() {
|
||||
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))
|
||||
let mid_point = (p1 + p2) / 2.0;
|
||||
let radius = mid_point.distance(p1);
|
||||
BoundingSphere::new(mid_point + translation, radius)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let [a, _, _] = self.vertices;
|
||||
|
||||
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)
|
||||
@ -355,13 +341,14 @@ impl Bounded3d for Triangle3d {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::bounding::BoundingVolume;
|
||||
use glam::{Quat, Vec3, Vec3A};
|
||||
|
||||
use crate::{
|
||||
bounding::Bounded3d,
|
||||
primitives::{
|
||||
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
|
||||
Segment3d, Sphere, Torus,
|
||||
Segment3d, Sphere, Torus, Triangle3d,
|
||||
},
|
||||
Dir3,
|
||||
};
|
||||
@ -607,4 +594,69 @@ mod tests {
|
||||
assert_eq!(bounding_sphere.center, translation.into());
|
||||
assert_eq!(bounding_sphere.radius(), 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangle3d() {
|
||||
let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
|
||||
|
||||
let br = zero_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
br.center(),
|
||||
Vec3::ZERO.into(),
|
||||
"incorrect bounding box center"
|
||||
);
|
||||
assert_eq!(
|
||||
br.half_size(),
|
||||
Vec3::ZERO.into(),
|
||||
"incorrect bounding box half extents"
|
||||
);
|
||||
|
||||
let bs = zero_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
bs.center,
|
||||
Vec3::ZERO.into(),
|
||||
"incorrect bounding sphere center"
|
||||
);
|
||||
assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius");
|
||||
|
||||
let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
|
||||
let bs = dup_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
bs.center,
|
||||
Vec3::new(0.5, 0.0, 0.0).into(),
|
||||
"incorrect bounding sphere center"
|
||||
);
|
||||
assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius");
|
||||
let br = dup_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
br.center(),
|
||||
Vec3::new(0.5, 0.0, 0.0).into(),
|
||||
"incorrect bounding box center"
|
||||
);
|
||||
assert_eq!(
|
||||
br.half_size(),
|
||||
Vec3::new(0.5, 0.0, 0.0).into(),
|
||||
"incorrect bounding box half extents"
|
||||
);
|
||||
|
||||
let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
|
||||
let bs = collinear_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
bs.center,
|
||||
Vec3::ZERO.into(),
|
||||
"incorrect bounding sphere center"
|
||||
);
|
||||
assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius");
|
||||
let br = collinear_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
|
||||
assert_eq!(
|
||||
br.center(),
|
||||
Vec3::ZERO.into(),
|
||||
"incorrect bounding box center"
|
||||
);
|
||||
assert_eq!(
|
||||
br.half_size(),
|
||||
Vec3::new(1.0, 0.0, 0.0).into(),
|
||||
"incorrect bounding box half extents"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,6 +527,54 @@ impl Triangle2d {
|
||||
(Circle { radius }, center)
|
||||
}
|
||||
|
||||
/// 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 `10e-7`.
|
||||
/// This indicates that the three vertices are collinear or nearly collinear.
|
||||
#[inline(always)]
|
||||
pub fn is_degenerate(&self) -> bool {
|
||||
let [a, b, c] = self.vertices;
|
||||
let ab = (b - a).extend(0.);
|
||||
let ac = (c - a).extend(0.);
|
||||
ab.cross(ac).length() < 10e-7
|
||||
}
|
||||
|
||||
/// Checks if the triangle is acute, meaning all angles are less than 90 degrees
|
||||
#[inline(always)]
|
||||
pub fn is_acute(&self) -> bool {
|
||||
let [a, b, c] = self.vertices;
|
||||
let ab = b - a;
|
||||
let bc = c - b;
|
||||
let ca = a - c;
|
||||
|
||||
// a^2 + b^2 < c^2 for an acute triangle
|
||||
let mut side_lengths = [
|
||||
ab.length_squared(),
|
||||
bc.length_squared(),
|
||||
ca.length_squared(),
|
||||
];
|
||||
side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
side_lengths[0] + side_lengths[1] > side_lengths[2]
|
||||
}
|
||||
|
||||
/// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees
|
||||
#[inline(always)]
|
||||
pub fn is_obtuse(&self) -> bool {
|
||||
let [a, b, c] = self.vertices;
|
||||
let ab = b - a;
|
||||
let bc = c - b;
|
||||
let ca = a - c;
|
||||
|
||||
// a^2 + b^2 > c^2 for an obtuse triangle
|
||||
let mut side_lengths = [
|
||||
ab.length_squared(),
|
||||
bc.length_squared(),
|
||||
ca.length_squared(),
|
||||
];
|
||||
side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
side_lengths[0] + side_lengths[1] < side_lengths[2]
|
||||
}
|
||||
|
||||
/// Reverse the [`WindingOrder`] of the triangle
|
||||
/// by swapping the first and last vertices.
|
||||
#[inline(always)]
|
||||
@ -975,6 +1023,20 @@ mod tests {
|
||||
);
|
||||
assert_eq!(triangle.area(), 21.0, "incorrect area");
|
||||
assert_eq!(triangle.perimeter(), 22.097439, "incorrect perimeter");
|
||||
|
||||
let degenerate_triangle =
|
||||
Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(0., 0.), Vec2::new(1., 0.));
|
||||
assert!(degenerate_triangle.is_degenerate());
|
||||
|
||||
let acute_triangle =
|
||||
Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 5.));
|
||||
let obtuse_triangle =
|
||||
Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 0.5));
|
||||
|
||||
assert!(acute_triangle.is_acute());
|
||||
assert!(!acute_triangle.is_obtuse());
|
||||
assert!(!obtuse_triangle.is_acute());
|
||||
assert!(obtuse_triangle.is_obtuse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -780,6 +780,42 @@ impl Triangle3d {
|
||||
ab.cross(ac).length() < 10e-7
|
||||
}
|
||||
|
||||
/// Checks if the triangle is acute, meaning all angles are less than 90 degrees
|
||||
#[inline(always)]
|
||||
pub fn is_acute(&self) -> bool {
|
||||
let [a, b, c] = self.vertices;
|
||||
let ab = b - a;
|
||||
let bc = c - b;
|
||||
let ca = a - c;
|
||||
|
||||
// a^2 + b^2 < c^2 for an acute triangle
|
||||
let mut side_lengths = [
|
||||
ab.length_squared(),
|
||||
bc.length_squared(),
|
||||
ca.length_squared(),
|
||||
];
|
||||
side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
side_lengths[0] + side_lengths[1] > side_lengths[2]
|
||||
}
|
||||
|
||||
/// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees
|
||||
#[inline(always)]
|
||||
pub fn is_obtuse(&self) -> bool {
|
||||
let [a, b, c] = self.vertices;
|
||||
let ab = b - a;
|
||||
let bc = c - b;
|
||||
let ca = a - c;
|
||||
|
||||
// a^2 + b^2 > c^2 for an obtuse triangle
|
||||
let mut side_lengths = [
|
||||
ab.length_squared(),
|
||||
bc.length_squared(),
|
||||
ca.length_squared(),
|
||||
];
|
||||
side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
side_lengths[0] + side_lengths[1] < side_lengths[2]
|
||||
}
|
||||
|
||||
/// Reverse the triangle by swapping the first and last vertices.
|
||||
#[inline(always)]
|
||||
pub fn reverse(&mut self) {
|
||||
@ -1010,7 +1046,7 @@ mod tests {
|
||||
// Reference values were computed by hand and/or with external tools
|
||||
|
||||
use super::*;
|
||||
use crate::Quat;
|
||||
use crate::{InvalidDirectionError, Quat};
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
@ -1210,8 +1246,91 @@ mod tests {
|
||||
assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangle_math() {
|
||||
// Default triangle tests
|
||||
let mut default_triangle = Triangle3d::default();
|
||||
let reverse_default_triangle = Triangle3d::new(
|
||||
Vec3::new(0.5, -0.5, 0.0),
|
||||
Vec3::new(-0.5, -0.5, 0.0),
|
||||
Vec3::new(0.0, 0.5, 0.0),
|
||||
);
|
||||
assert_eq!(default_triangle.area(), 0.5, "incorrect area");
|
||||
assert_relative_eq!(
|
||||
default_triangle.perimeter(),
|
||||
1.0 + 2.0 * 1.25_f32.sqrt(),
|
||||
epsilon = 10e-9
|
||||
);
|
||||
assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");
|
||||
assert!(
|
||||
!default_triangle.is_degenerate(),
|
||||
"incorrect degenerate check"
|
||||
);
|
||||
assert_eq!(
|
||||
default_triangle.centroid(),
|
||||
Vec3::new(0.0, -0.16666667, 0.0),
|
||||
"incorrect centroid"
|
||||
);
|
||||
assert_eq!(
|
||||
default_triangle.largest_side(),
|
||||
(Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0))
|
||||
);
|
||||
default_triangle.reverse();
|
||||
assert_eq!(
|
||||
default_triangle, reverse_default_triangle,
|
||||
"incorrect reverse"
|
||||
);
|
||||
assert_eq!(
|
||||
default_triangle.circumcenter(),
|
||||
Vec3::new(0.0, -0.125, 0.0),
|
||||
"incorrect circumcenter"
|
||||
);
|
||||
|
||||
// Custom triangle tests
|
||||
let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y);
|
||||
let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0));
|
||||
let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0));
|
||||
|
||||
assert_eq!(
|
||||
right_triangle.circumcenter(),
|
||||
Vec3::new(0.5, 0.5, 0.0),
|
||||
"incorrect circumcenter"
|
||||
);
|
||||
assert_eq!(
|
||||
obtuse_triangle.circumcenter(),
|
||||
Vec3::new(0.0, -4.95, 0.0),
|
||||
"incorrect circumcenter"
|
||||
);
|
||||
assert_eq!(
|
||||
acute_triangle.circumcenter(),
|
||||
Vec3::new(0.5, 2.475, 0.0),
|
||||
"incorrect circumcenter"
|
||||
);
|
||||
|
||||
assert!(acute_triangle.is_acute());
|
||||
assert!(!acute_triangle.is_obtuse());
|
||||
assert!(!obtuse_triangle.is_acute());
|
||||
assert!(obtuse_triangle.is_obtuse());
|
||||
|
||||
// Arbitrary triangle tests
|
||||
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);
|
||||
|
||||
@ -1233,25 +1352,53 @@ mod tests {
|
||||
"incorrect normal"
|
||||
);
|
||||
|
||||
let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE);
|
||||
assert!(degenerate.is_degenerate(), "did not find degenerate");
|
||||
}
|
||||
// Degenerate triangle tests
|
||||
let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
|
||||
assert!(
|
||||
zero_degenerate_triangle.is_degenerate(),
|
||||
"incorrect degenerate check"
|
||||
);
|
||||
assert_eq!(
|
||||
zero_degenerate_triangle.normal(),
|
||||
Err(InvalidDirectionError::Zero),
|
||||
"incorrect normal"
|
||||
);
|
||||
assert_eq!(
|
||||
zero_degenerate_triangle.largest_side(),
|
||||
(Vec3::ZERO, Vec3::ZERO),
|
||||
"incorrect largest side"
|
||||
);
|
||||
|
||||
#[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 dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
|
||||
assert!(
|
||||
dup_degenerate_triangle.is_degenerate(),
|
||||
"incorrect degenerate check"
|
||||
);
|
||||
assert_eq!(
|
||||
dup_degenerate_triangle.normal(),
|
||||
Err(InvalidDirectionError::Zero),
|
||||
"incorrect normal"
|
||||
);
|
||||
assert_eq!(
|
||||
dup_degenerate_triangle.largest_side(),
|
||||
(Vec3::ZERO, Vec3::X),
|
||||
"incorrect largest side"
|
||||
);
|
||||
|
||||
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");
|
||||
let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
|
||||
assert!(
|
||||
collinear_degenerate_triangle.is_degenerate(),
|
||||
"incorrect degenerate check"
|
||||
);
|
||||
assert_eq!(
|
||||
collinear_degenerate_triangle.normal(),
|
||||
Err(InvalidDirectionError::Zero),
|
||||
"incorrect normal"
|
||||
);
|
||||
assert_eq!(
|
||||
collinear_degenerate_triangle.largest_side(),
|
||||
(Vec3::NEG_X, Vec3::X),
|
||||
"incorrect largest side"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user