use crate::{
aabb::Aabb2,
distance::distance_2d,
error::GeometryError,
orientation::{Orientation2, orientation_2d, signed_twice_area_2d},
point::Point2,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Triangle {
a: Point2,
b: Point2,
c: Point2,
}
impl Triangle {
#[must_use]
pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
Self { a, b, c }
}
pub fn try_new(a: Point2, b: Point2, c: Point2) -> Result<Self, GeometryError> {
Ok(Self::new(a.validate()?, b.validate()?, c.validate()?))
}
#[must_use]
pub const fn a(self) -> Point2 {
self.a
}
#[must_use]
pub const fn b(self) -> Point2 {
self.b
}
#[must_use]
pub const fn c(self) -> Point2 {
self.c
}
#[must_use]
pub const fn vertices(self) -> [Point2; 3] {
[self.a(), self.b(), self.c()]
}
#[must_use]
pub fn twice_signed_area(self) -> f64 {
triangle_twice_signed_area(self.a(), self.b(), self.c())
}
#[must_use]
pub fn twice_area(self) -> f64 {
triangle_twice_area(self.a(), self.b(), self.c())
}
#[must_use]
pub fn orientation(self) -> Orientation2 {
orientation_2d(self.a(), self.b(), self.c())
}
#[must_use]
pub fn area(self) -> f64 {
self.twice_area() * 0.5
}
#[must_use]
pub fn sides(self) -> [f64; 3] {
[
distance_2d(self.a(), self.b()),
distance_2d(self.b(), self.c()),
distance_2d(self.c(), self.a()),
]
}
#[must_use]
pub fn perimeter(self) -> f64 {
self.sides().into_iter().sum()
}
#[must_use]
pub fn centroid(self) -> Point2 {
let [a, b, c] = self.vertices();
Point2::new((a.x() + b.x() + c.x()) / 3.0, (a.y() + b.y() + c.y()) / 3.0)
}
#[must_use]
pub fn is_degenerate(self) -> bool {
self.twice_signed_area() == 0.0
}
pub fn is_degenerate_with_tolerance(self, tolerance: f64) -> Result<bool, GeometryError> {
let tolerance = GeometryError::validate_tolerance(tolerance)?;
Ok(self.twice_signed_area().abs() <= tolerance)
}
#[must_use]
pub const fn aabb(self) -> Aabb2 {
let [a, b, c] = self.vertices();
let min_x = a.x().min(b.x()).min(c.x());
let min_y = a.y().min(b.y()).min(c.y());
let max_x = a.x().max(b.x()).max(c.x());
let max_y = a.y().max(b.y()).max(c.y());
Aabb2::from_points(Point2::new(min_x, min_y), Point2::new(max_x, max_y))
}
}
#[must_use]
pub fn triangle_twice_signed_area(a: Point2, b: Point2, c: Point2) -> f64 {
signed_twice_area_2d(a, b, c)
}
#[must_use]
pub fn triangle_twice_area(a: Point2, b: Point2, c: Point2) -> f64 {
triangle_twice_signed_area(a, b, c).abs()
}
#[must_use]
pub fn triangle_area(a: Point2, b: Point2, c: Point2) -> f64 {
triangle_twice_area(a, b, c) * 0.5
}
#[cfg(test)]
mod tests {
use super::{Triangle, triangle_area, triangle_twice_area, triangle_twice_signed_area};
use crate::{error::GeometryError, orientation::Orientation2, point::Point2};
fn approx_eq(left: f64, right: f64) -> bool {
(left - right).abs() < 1.0e-10
}
fn approx_eq_slice(left: [f64; 3], right: [f64; 3]) -> bool {
left.into_iter()
.zip(right)
.all(|(left_value, right_value)| approx_eq(left_value, right_value))
}
#[test]
fn constructs_triangles() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
);
assert_eq!(triangle.a(), Point2::new(0.0, 0.0));
}
#[test]
fn constructs_triangles_with_try_new() {
assert_eq!(
Triangle::try_new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
),
Ok(Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
))
);
}
#[test]
fn rejects_non_finite_triangle_vertices() {
assert!(matches!(
Triangle::try_new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, f64::NAN),
),
Err(GeometryError::NonFiniteComponent {
type_name: "Point2",
component: "y",
value,
}) if value.is_nan()
));
}
#[test]
fn computes_triangle_area() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
);
assert!(approx_eq(triangle.twice_signed_area(), 12.0));
assert!(approx_eq(triangle.twice_area(), 12.0));
assert!(approx_eq(triangle.area(), 6.0));
assert!(approx_eq(
triangle_twice_signed_area(triangle.a(), triangle.b(), triangle.c()),
12.0
));
assert!(approx_eq(
triangle_twice_area(triangle.a(), triangle.b(), triangle.c()),
12.0
));
assert!(approx_eq(
triangle_area(triangle.a(), triangle.b(), triangle.c()),
6.0
));
}
#[test]
fn signed_area_tracks_orientation() {
let counter_clockwise = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
);
let clockwise = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(0.0, 3.0),
Point2::new(4.0, 0.0),
);
assert!(approx_eq(counter_clockwise.twice_signed_area(), 12.0));
assert!(approx_eq(clockwise.twice_signed_area(), -12.0));
assert_eq!(
counter_clockwise.orientation(),
Orientation2::CounterClockwise
);
assert_eq!(clockwise.orientation(), Orientation2::Clockwise);
assert_eq!(
crate::orientation::orientation_2d(
Point2::new(0.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(2.0, 2.0)
),
Orientation2::Collinear
);
}
#[test]
fn computes_triangle_perimeter() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 3.0),
);
assert!(approx_eq_slice(triangle.sides(), [4.0, 5.0, 3.0]));
assert!(approx_eq(triangle.perimeter(), 12.0));
assert_eq!(
triangle.vertices(),
[triangle.a(), triangle.b(), triangle.c()]
);
assert_eq!(triangle.centroid(), Point2::new(4.0 / 3.0, 1.0));
}
#[test]
fn detects_degenerate_triangles() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(2.0, 2.0),
);
assert!(triangle.is_degenerate());
assert_eq!(triangle.is_degenerate_with_tolerance(0.0), Ok(true));
}
#[test]
fn detects_near_degenerate_triangles_with_tolerance() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(2.0, 2.0 + 1.0e-12),
);
assert!(!triangle.is_degenerate());
assert_eq!(triangle.is_degenerate_with_tolerance(1.0e-11), Ok(true));
}
#[test]
fn rejects_negative_degeneracy_tolerance() {
let triangle = Triangle::new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(2.0, 2.0),
);
assert_eq!(
triangle.is_degenerate_with_tolerance(-1.0),
Err(GeometryError::NegativeTolerance(-1.0))
);
}
#[test]
fn computes_triangle_bounds() {
let triangle = Triangle::new(
Point2::new(4.0, 1.0),
Point2::new(1.0, 3.0),
Point2::new(2.0, -1.0),
);
assert_eq!(triangle.aabb().min(), Point2::new(1.0, -1.0));
assert_eq!(triangle.aabb().max(), Point2::new(4.0, 3.0));
}
}