use crate::{Point3, Scalar, Vec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Triangle3 {
pub a: Point3,
pub b: Point3,
pub c: Point3,
}
impl Triangle3 {
pub const fn new(a: Point3, b: Point3, c: Point3) -> Self {
Self { a, b, c }
}
pub fn normal(&self) -> Vec3 {
(self.b - self.a).cross(self.c - self.a)
}
pub fn area(&self) -> Scalar {
self.normal().length() * 0.5
}
pub fn centroid(&self) -> Point3 {
(self.a + self.b + self.c) / 3.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle3 {
pub origin: Point3,
pub x: Vec3,
pub y: Vec3,
}
impl Rectangle3 {
pub const fn new(origin: Point3, x: Vec3, y: Vec3) -> Self {
Self { origin, x, y }
}
pub fn corners(&self) -> [Point3; 4] {
[
self.origin,
self.origin + self.x,
self.origin + self.x + self.y,
self.origin + self.y,
]
}
pub fn normal(&self) -> Vec3 {
self.x.cross(self.y)
}
pub fn area(&self) -> Scalar {
self.normal().length()
}
pub fn center(&self) -> Point3 {
self.origin + (self.x + self.y) * 0.5
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Box3 {
pub origin: Point3,
pub x: Vec3,
pub y: Vec3,
pub z: Vec3,
}
impl Box3 {
pub const fn new(origin: Point3, x: Vec3, y: Vec3, z: Vec3) -> Self {
Self { origin, x, y, z }
}
pub fn corners(&self) -> [Point3; 8] {
let mut out = [self.origin; 8];
for (index, corner) in out.iter_mut().enumerate() {
let mut point = self.origin;
if index & 1 != 0 {
point += self.x;
}
if index & 2 != 0 {
point += self.y;
}
if index & 4 != 0 {
point += self.z;
}
*corner = point;
}
out
}
pub fn signed_volume(&self) -> Scalar {
self.x.cross(self.y).dot(self.z)
}
pub fn center(&self) -> Point3 {
self.origin + (self.x + self.y + self.z) * 0.5
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Polygon3 {
pub vertices: Vec<Point3>,
}
impl Polygon3 {
pub const fn new(vertices: Vec<Point3>) -> Self {
Self { vertices }
}
pub fn len(&self) -> usize {
self.vertices.len()
}
pub fn is_empty(&self) -> bool {
self.vertices.is_empty()
}
pub fn vector_area2(&self) -> Vec3 {
if self.vertices.len() < 3 {
return Vec3::ZERO;
}
let base = self.vertices[0];
let mut total = Vec3::ZERO;
for pair in self.vertices[1..].windows(2) {
total += (pair[0] - base).cross(pair[1] - base);
}
total
}
pub fn normal(&self) -> Vec3 {
self.vector_area2()
}
pub fn area(&self) -> Scalar {
self.vector_area2().length() * 0.5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn triangle_normal_follows_winding_and_area_is_half_its_length() {
let triangle = Triangle3::new(
Point3::ZERO,
Point3::new(2.0, 0.0, 0.0),
Point3::new(0.0, 3.0, 0.0),
);
assert_eq!(triangle.normal(), Vec3::new(0.0, 0.0, 6.0));
assert_eq!(triangle.area(), 3.0);
let reversed = Triangle3::new(triangle.a, triangle.c, triangle.b);
assert_eq!(reversed.normal(), -triangle.normal());
assert_eq!(reversed.area(), triangle.area());
}
#[test]
fn degenerate_triangle_has_zero_normal_rather_than_nan() {
let collinear = Triangle3::new(
Point3::ZERO,
Point3::new(1.0, 1.0, 1.0),
Point3::new(2.0, 2.0, 2.0),
);
assert_eq!(collinear.normal(), Vec3::ZERO);
assert_eq!(collinear.area(), 0.0);
}
#[test]
fn rectangle_corners_close_the_loop_and_area_matches_the_edges() {
let rectangle = Rectangle3::new(
Point3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.0, 4.0, 0.0),
);
let corners = rectangle.corners();
assert_eq!(corners[0], rectangle.origin);
assert_eq!((corners[0] + corners[2]) * 0.5, rectangle.center());
assert_eq!((corners[1] + corners[3]) * 0.5, rectangle.center());
assert_eq!(rectangle.area(), 8.0);
}
#[test]
fn box_corner_index_selects_edges_by_bit() {
let unit = Box3::new(Point3::ZERO, Vec3::X, Vec3::Y, Vec3::Z);
let corners = unit.corners();
assert_eq!(corners[0], Point3::ZERO);
assert_eq!(corners[1], Vec3::X);
assert_eq!(corners[2], Vec3::Y);
assert_eq!(corners[4], Vec3::Z);
assert_eq!(corners[7], Vec3::ONE);
assert_eq!(unit.center(), Vec3::splat(0.5));
}
#[test]
fn box_volume_is_signed_so_a_mirrored_frame_is_detectable() {
let right_handed = Box3::new(Point3::ZERO, Vec3::X, Vec3::Y, Vec3::Z);
assert_eq!(right_handed.signed_volume(), 1.0);
let mirrored = Box3::new(Point3::ZERO, Vec3::Y, Vec3::X, Vec3::Z);
assert_eq!(mirrored.signed_volume(), -1.0);
}
#[test]
fn polygon_area_is_winding_independent_and_normal_is_not() {
let square = Polygon3::new(vec![
Point3::ZERO,
Point3::new(2.0, 0.0, 0.0),
Point3::new(2.0, 2.0, 0.0),
Point3::new(0.0, 2.0, 0.0),
]);
assert_eq!(square.area(), 4.0);
assert_eq!(square.normal(), Vec3::new(0.0, 0.0, 8.0));
let mut reversed = square.vertices.clone();
reversed.reverse();
let reversed = Polygon3::new(reversed);
assert_eq!(reversed.area(), square.area());
assert_eq!(reversed.normal(), -square.normal());
}
#[test]
fn polygon_with_fewer_than_three_vertices_encloses_nothing() {
assert_eq!(Polygon3::new(Vec::new()).vector_area2(), Vec3::ZERO);
assert_eq!(
Polygon3::new(vec![Point3::ZERO, Vec3::X]).vector_area2(),
Vec3::ZERO
);
}
#[test]
fn polygon_area_is_translation_invariant_far_from_the_origin() {
let offset = Vec3::splat(6_000_000.0);
let local = Polygon3::new(vec![
Point3::ZERO,
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
]);
let far = Polygon3::new(local.vertices.iter().map(|v| *v + offset).collect());
assert_eq!(far.area(), local.area());
}
}