use core::f32::consts::{FRAC_PI_3, PI};
use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};
use crate::{
ops::{self, FloatPow},
Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use glam::Quat;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Sphere {
pub radius: f32,
}
impl Primitive3d for Sphere {}
impl Default for Sphere {
fn default() -> Self {
Self { radius: 0.5 }
}
}
impl Sphere {
#[inline]
pub const fn new(radius: f32) -> Self {
Self { radius }
}
#[inline]
pub const fn diameter(&self) -> f32 {
2.0 * self.radius
}
#[inline]
pub fn closest_point(&self, point: Vec3) -> Vec3 {
let distance_squared = point.length_squared();
if distance_squared <= self.radius.squared() {
point
} else {
let dir_to_point = point / ops::sqrt(distance_squared);
self.radius * dir_to_point
}
}
}
impl Measured3d for Sphere {
#[inline]
fn area(&self) -> f32 {
4.0 * PI * self.radius.squared()
}
#[inline]
fn volume(&self) -> f32 {
4.0 * FRAC_PI_3 * self.radius.cubed()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Plane3d {
pub normal: Dir3,
pub half_size: Vec2,
}
impl Primitive3d for Plane3d {}
impl Default for Plane3d {
fn default() -> Self {
Self {
normal: Dir3::Y,
half_size: Vec2::splat(0.5),
}
}
}
impl Plane3d {
#[inline]
pub fn new(normal: Vec3, half_size: Vec2) -> Self {
Self {
normal: Dir3::new(normal).expect("normal must be nonzero and finite"),
half_size,
}
}
#[inline]
pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
let normal = Dir3::new((b - a).cross(c - a)).expect(
"finite plane must be defined by three finite points that don't lie on the same line",
);
let translation = (a + b + c) / 3.0;
(
Self {
normal,
..Default::default()
},
translation,
)
}
}
impl Measured2d for Plane3d {
#[inline]
fn area(&self) -> f32 {
self.half_size.element_product() * 4.0
}
#[inline]
fn perimeter(&self) -> f32 {
self.half_size.element_sum() * 4.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct InfinitePlane3d {
pub normal: Dir3,
}
impl Primitive3d for InfinitePlane3d {}
impl Default for InfinitePlane3d {
fn default() -> Self {
Self { normal: Dir3::Y }
}
}
impl InfinitePlane3d {
#[inline]
pub fn new<T: TryInto<Dir3>>(normal: T) -> Self
where
<T as TryInto<Dir3>>::Error: core::fmt::Debug,
{
Self {
normal: normal
.try_into()
.expect("normal must be nonzero and finite"),
}
}
#[inline]
pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
let normal = Dir3::new((b - a).cross(c - a)).expect(
"infinite plane must be defined by three finite points that don't lie on the same line",
);
let translation = (a + b + c) / 3.0;
(Self { normal }, translation)
}
#[inline]
pub fn signed_distance(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> f32 {
let isometry = isometry.into();
self.normal.dot(isometry.inverse() * point)
}
#[inline]
pub fn project_point(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> Vec3 {
point - self.normal * self.signed_distance(isometry, point)
}
#[inline]
pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d {
let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z);
let transformed_origin = rotation * origin;
Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation)
}
#[inline]
pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d {
self.isometry_into_xy(origin).inverse()
}
#[inline]
pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) {
let projection = self.isometry_into_xy(origin);
(projection, projection.inverse())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Line3d {
pub direction: Dir3,
}
impl Primitive3d for Line3d {}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
#[doc(alias = "LineSegment3d")]
pub struct Segment3d {
pub vertices: [Vec3; 2],
}
impl Primitive3d for Segment3d {}
impl Default for Segment3d {
fn default() -> Self {
Self {
vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)],
}
}
}
impl Segment3d {
#[inline]
pub const fn new(point1: Vec3, point2: Vec3) -> Self {
Self {
vertices: [point1, point2],
}
}
#[inline]
pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {
let endpoint = 0.5 * length * direction;
Self {
vertices: [-endpoint, endpoint],
}
}
#[inline]
pub fn from_scaled_direction(scaled_direction: Vec3) -> Self {
let endpoint = 0.5 * scaled_direction;
Self {
vertices: [-endpoint, endpoint],
}
}
#[inline]
pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self {
Self {
vertices: [ray.origin, ray.get_point(length)],
}
}
#[inline]
pub const fn point1(&self) -> Vec3 {
self.vertices[0]
}
#[inline]
pub const fn point2(&self) -> Vec3 {
self.vertices[1]
}
#[inline]
#[doc(alias = "midpoint")]
pub fn center(&self) -> Vec3 {
self.point1().midpoint(self.point2())
}
#[inline]
pub fn length(&self) -> f32 {
self.point1().distance(self.point2())
}
#[inline]
pub fn length_squared(&self) -> f32 {
self.point1().distance_squared(self.point2())
}
#[inline]
pub fn direction(&self) -> Dir3 {
self.try_direction().unwrap_or_else(|err| {
panic!("Failed to compute the direction of a line segment: {err}")
})
}
#[inline]
pub fn try_direction(&self) -> Result<Dir3, InvalidDirectionError> {
Dir3::new(self.scaled_direction())
}
#[inline]
pub fn scaled_direction(&self) -> Vec3 {
self.point2() - self.point1()
}
#[inline]
pub fn transformed(&self, isometry: impl Into<Isometry3d>) -> Self {
let isometry: Isometry3d = isometry.into();
Self::new(
isometry.transform_point(self.point1()).into(),
isometry.transform_point(self.point2()).into(),
)
}
#[inline]
pub fn translated(&self, translation: Vec3) -> Segment3d {
Self::new(self.point1() + translation, self.point2() + translation)
}
#[inline]
pub fn rotated(&self, rotation: Quat) -> Segment3d {
Segment3d::new(rotation * self.point1(), rotation * self.point2())
}
#[inline]
pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {
let offset = self.translated(-point);
let rotated = offset.rotated(rotation);
rotated.translated(point)
}
#[inline]
pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {
self.rotated_around(rotation, self.center())
}
#[inline]
pub fn centered(&self) -> Segment3d {
let center = self.center();
self.translated(-center)
}
#[inline]
pub fn resized(&self, length: f32) -> Segment3d {
let offset_from_origin = self.center();
let centered = self.translated(-offset_from_origin);
let ratio = length / self.length();
let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);
segment.translated(offset_from_origin)
}
#[inline]
pub fn reverse(&mut self) {
let [point1, point2] = &mut self.vertices;
core::mem::swap(point1, point2);
}
#[inline]
#[must_use]
pub fn reversed(mut self) -> Self {
self.reverse();
self
}
#[inline]
pub fn closest_point(&self, point: Vec3) -> Vec3 {
let segment_vector = self.vertices[1] - self.vertices[0];
let offset = point - self.vertices[0];
let projection_scaled = segment_vector.dot(offset);
if projection_scaled <= 0.0 {
return self.vertices[0];
}
let length_squared = segment_vector.length_squared();
if projection_scaled >= length_squared {
return self.vertices[1];
}
let t = projection_scaled / length_squared;
self.vertices[0] + t * segment_vector
}
}
impl From<[Vec3; 2]> for Segment3d {
#[inline]
fn from(vertices: [Vec3; 2]) -> Self {
Self { vertices }
}
}
impl From<(Vec3, Vec3)> for Segment3d {
#[inline]
fn from((point1, point2): (Vec3, Vec3)) -> Self {
Self::new(point1, point2)
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Polyline3d {
pub vertices: Vec<Vec3>,
}
#[cfg(feature = "alloc")]
impl Primitive3d for Polyline3d {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec3> for Polyline3d {
fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
Self {
vertices: iter.into_iter().collect(),
}
}
}
#[cfg(feature = "alloc")]
impl Default for Polyline3d {
fn default() -> Self {
Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)])
}
}
#[cfg(feature = "alloc")]
impl Polyline3d {
pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
Self::from_iter(vertices)
}
pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self {
let total_vertices = subdivisions + 2;
let mut vertices = Vec::with_capacity(total_vertices);
let step = (end - start) / (subdivisions + 1) as f32;
for i in 0..total_vertices {
vertices.push(start + step * i as f32);
}
Self { vertices }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Cuboid {
pub half_size: Vec3,
}
impl Primitive3d for Cuboid {}
impl Default for Cuboid {
fn default() -> Self {
Self {
half_size: Vec3::splat(0.5),
}
}
}
impl Cuboid {
#[inline]
pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {
Self::from_size(Vec3::new(x_length, y_length, z_length))
}
#[inline]
pub const fn from_size(size: Vec3) -> Self {
Self {
half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0),
}
}
#[inline]
pub fn from_corners(point1: Vec3, point2: Vec3) -> Self {
Self {
half_size: (point2 - point1).abs() / 2.0,
}
}
#[inline]
pub const fn from_length(length: f32) -> Self {
Self {
half_size: Vec3::splat(length / 2.0),
}
}
#[inline]
pub fn size(&self) -> Vec3 {
2.0 * self.half_size
}
#[inline]
pub fn closest_point(&self, point: Vec3) -> Vec3 {
point.clamp(-self.half_size, self.half_size)
}
}
impl Measured3d for Cuboid {
#[inline]
fn area(&self) -> f32 {
8.0 * (self.half_size.x * self.half_size.y
+ self.half_size.y * self.half_size.z
+ self.half_size.x * self.half_size.z)
}
#[inline]
fn volume(&self) -> f32 {
8.0 * self.half_size.x * self.half_size.y * self.half_size.z
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Cylinder {
pub radius: f32,
pub half_height: f32,
}
impl Primitive3d for Cylinder {}
impl Default for Cylinder {
fn default() -> Self {
Self {
radius: 0.5,
half_height: 0.5,
}
}
}
impl Cylinder {
#[inline]
pub const fn new(radius: f32, height: f32) -> Self {
Self {
radius,
half_height: height / 2.0,
}
}
#[inline]
pub const fn base(&self) -> Circle {
Circle {
radius: self.radius,
}
}
#[inline]
#[doc(alias = "side_area")]
pub const fn lateral_area(&self) -> f32 {
4.0 * PI * self.radius * self.half_height
}
#[inline]
pub fn base_area(&self) -> f32 {
PI * self.radius.squared()
}
}
impl Measured3d for Cylinder {
#[inline]
fn area(&self) -> f32 {
2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)
}
#[inline]
fn volume(&self) -> f32 {
self.base_area() * 2.0 * self.half_height
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Capsule3d {
pub radius: f32,
pub half_length: f32,
}
impl Primitive3d for Capsule3d {}
impl Default for Capsule3d {
fn default() -> Self {
Self {
radius: 0.5,
half_length: 0.5,
}
}
}
impl Capsule3d {
pub const fn new(radius: f32, length: f32) -> Self {
Self {
radius,
half_length: length / 2.0,
}
}
#[inline]
pub const fn to_cylinder(&self) -> Cylinder {
Cylinder {
radius: self.radius,
half_height: self.half_length,
}
}
}
impl Measured3d for Capsule3d {
#[inline]
fn area(&self) -> f32 {
4.0 * PI * self.radius * (self.radius + self.half_length)
}
#[inline]
fn volume(&self) -> f32 {
let diameter = self.radius * 2.0;
PI * self.radius * diameter * (diameter / 3.0 + self.half_length)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Cone {
pub radius: f32,
pub height: f32,
}
impl Primitive3d for Cone {}
impl Default for Cone {
fn default() -> Self {
Self {
radius: 0.5,
height: 1.0,
}
}
}
impl Cone {
pub const fn new(radius: f32, height: f32) -> Self {
Self { radius, height }
}
#[inline]
pub const fn base(&self) -> Circle {
Circle {
radius: self.radius,
}
}
#[inline]
#[doc(alias = "side_length")]
pub fn slant_height(&self) -> f32 {
ops::hypot(self.radius, self.height)
}
#[inline]
#[doc(alias = "side_area")]
pub fn lateral_area(&self) -> f32 {
PI * self.radius * self.slant_height()
}
#[inline]
pub fn base_area(&self) -> f32 {
PI * self.radius.squared()
}
}
impl Measured3d for Cone {
#[inline]
fn area(&self) -> f32 {
self.base_area() + self.lateral_area()
}
#[inline]
fn volume(&self) -> f32 {
(self.base_area() * self.height) / 3.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct ConicalFrustum {
pub radius_top: f32,
pub radius_bottom: f32,
pub height: f32,
}
impl Primitive3d for ConicalFrustum {}
impl Default for ConicalFrustum {
fn default() -> Self {
Self {
radius_top: 0.25,
radius_bottom: 0.5,
height: 0.5,
}
}
}
impl ConicalFrustum {
#[inline]
pub const fn bottom_base(&self) -> Circle {
Circle {
radius: self.radius_bottom,
}
}
#[inline]
pub const fn top_base(&self) -> Circle {
Circle {
radius: self.radius_top,
}
}
#[inline]
#[doc(alias = "side_length")]
pub fn slant_height(&self) -> f32 {
ops::hypot(self.radius_bottom - self.radius_top, self.height)
}
#[inline]
#[doc(alias = "side_area")]
pub fn lateral_area(&self) -> f32 {
PI * (self.radius_bottom + self.radius_top) * self.slant_height()
}
#[inline]
pub fn bottom_base_area(&self) -> f32 {
PI * self.radius_bottom.squared()
}
#[inline]
pub fn top_base_area(&self) -> f32 {
PI * self.radius_top.squared()
}
}
impl Measured3d for ConicalFrustum {
#[inline]
fn volume(&self) -> f32 {
FRAC_PI_3
* self.height
* (self.radius_bottom * self.radius_bottom
+ self.radius_top * self.radius_top
+ self.radius_top * self.radius_bottom)
}
#[inline]
fn area(&self) -> f32 {
self.bottom_base_area() + self.top_base_area() + self.lateral_area()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TorusKind {
Ring,
Horn,
Spindle,
Invalid,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Torus {
#[doc(
alias = "ring_radius",
alias = "tube_radius",
alias = "cross_section_radius"
)]
pub minor_radius: f32,
#[doc(alias = "radius_of_revolution")]
pub major_radius: f32,
}
impl Primitive3d for Torus {}
impl Default for Torus {
fn default() -> Self {
Self {
minor_radius: 0.25,
major_radius: 0.75,
}
}
}
impl Torus {
#[inline]
pub const fn new(inner_radius: f32, outer_radius: f32) -> Self {
let minor_radius = (outer_radius - inner_radius) / 2.0;
let major_radius = outer_radius - minor_radius;
Self {
minor_radius,
major_radius,
}
}
#[inline]
pub const fn inner_radius(&self) -> f32 {
self.major_radius - self.minor_radius
}
#[inline]
pub const fn outer_radius(&self) -> f32 {
self.major_radius + self.minor_radius
}
#[inline]
pub fn kind(&self) -> TorusKind {
if self.minor_radius <= 0.0
|| !self.minor_radius.is_finite()
|| self.major_radius <= 0.0
|| !self.major_radius.is_finite()
{
return TorusKind::Invalid;
}
match self.major_radius.partial_cmp(&self.minor_radius).unwrap() {
core::cmp::Ordering::Greater => TorusKind::Ring,
core::cmp::Ordering::Equal => TorusKind::Horn,
core::cmp::Ordering::Less => TorusKind::Spindle,
}
}
}
impl Measured3d for Torus {
#[inline]
fn area(&self) -> f32 {
4.0 * PI.squared() * self.major_radius * self.minor_radius
}
#[inline]
fn volume(&self) -> f32 {
2.0 * PI.squared() * self.major_radius * self.minor_radius.squared()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Triangle3d {
pub vertices: [Vec3; 3],
}
impl Primitive3d for Triangle3d {}
impl Default for Triangle3d {
fn default() -> Self {
Self {
vertices: [
Vec3::new(0.0, 0.5, 0.0),
Vec3::new(-0.5, -0.5, 0.0),
Vec3::new(0.5, -0.5, 0.0),
],
}
}
}
impl Triangle3d {
#[inline]
pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {
Self {
vertices: [a, b, c],
}
}
#[inline]
pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
Dir3::new(ab.cross(ac))
}
#[inline]
pub fn is_degenerate(&self) -> bool {
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
ab.cross(ac).length() < 10e-7
}
#[inline]
pub fn is_acute(&self) -> bool {
let [a, b, c] = self.vertices;
let ab = b - a;
let bc = c - b;
let ca = a - c;
let side_lengths = [
ab.length_squared(),
bc.length_squared(),
ca.length_squared(),
];
let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];
let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);
sum - max > max
}
#[inline]
pub fn is_obtuse(&self) -> bool {
let [a, b, c] = self.vertices;
let ab = b - a;
let bc = c - b;
let ca = a - c;
let side_lengths = [
ab.length_squared(),
bc.length_squared(),
ca.length_squared(),
];
let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];
let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);
sum - max < max
}
#[inline]
pub fn reverse(&mut self) {
self.vertices.swap(0, 2);
}
#[inline]
#[must_use]
pub fn reversed(mut self) -> Triangle3d {
self.reverse();
self
}
#[doc(alias("center", "barycenter", "baricenter"))]
#[inline]
pub fn centroid(&self) -> Vec3 {
(self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0
}
#[inline]
pub fn largest_side(&self) -> (Vec3, Vec3) {
let [a, b, c] = self.vertices;
let ab = b - a;
let bc = c - b;
let ca = a - c;
let mut largest_side_points = (a, b);
let mut largest_side_length = ab.length_squared();
let bc_length = bc.length_squared();
if bc_length > largest_side_length {
largest_side_points = (b, c);
largest_side_length = bc_length;
}
let ca_length = ca.length_squared();
if ca_length > largest_side_length {
largest_side_points = (a, c);
}
largest_side_points
}
#[inline]
pub fn circumcenter(&self) -> Vec3 {
if self.is_degenerate() {
let (p1, p2) = self.largest_side();
return (p1 + p2) / 2.0;
}
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
let n = ab.cross(ac);
a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))
/ (2.0 * n.length_squared()))
}
}
impl Measured2d for Triangle3d {
#[inline]
fn area(&self) -> f32 {
let [a, b, c] = self.vertices;
let ab = b - a;
let ac = c - a;
ab.cross(ac).length() / 2.0
}
#[inline]
fn perimeter(&self) -> f32 {
let [a, b, c] = self.vertices;
a.distance(b) + b.distance(c) + c.distance(a)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Tetrahedron {
pub vertices: [Vec3; 4],
}
impl Primitive3d for Tetrahedron {}
impl Default for Tetrahedron {
fn default() -> Self {
Self {
vertices: [
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-0.5, 0.5, -0.5),
Vec3::new(-0.5, -0.5, 0.5),
Vec3::new(0.5, -0.5, -0.5),
],
}
}
}
impl Tetrahedron {
#[inline]
pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self {
Self {
vertices: [a, b, c, d],
}
}
#[inline]
pub fn signed_volume(&self) -> f32 {
let [a, b, c, d] = self.vertices;
let ab = b - a;
let ac = c - a;
let ad = d - a;
Mat3::from_cols(ab, ac, ad).determinant() / 6.0
}
#[doc(alias("center", "barycenter", "baricenter"))]
#[inline]
pub fn centroid(&self) -> Vec3 {
(self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0
}
#[inline]
pub fn faces(&self) -> [Triangle3d; 4] {
let [a, b, c, d] = self.vertices;
[
Triangle3d::new(b, c, d),
Triangle3d::new(a, c, d).reversed(),
Triangle3d::new(a, b, d),
Triangle3d::new(a, b, c).reversed(),
]
}
}
impl Measured3d for Tetrahedron {
#[inline]
fn area(&self) -> f32 {
let [a, b, c, d] = self.vertices;
let ab = b - a;
let ac = c - a;
let ad = d - a;
let bc = c - b;
let bd = d - b;
(ab.cross(ac).length()
+ ab.cross(ad).length()
+ ac.cross(ad).length()
+ bc.cross(bd).length())
/ 2.0
}
#[inline]
fn volume(&self) -> f32 {
ops::abs(self.signed_volume())
}
}
#[doc(alias = "Prism")]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Extrusion<T: Primitive2d> {
pub base_shape: T,
pub half_depth: f32,
}
impl<T: Primitive2d> Primitive3d for Extrusion<T> {}
impl<T: Primitive2d> Extrusion<T> {
pub fn new(base_shape: T, depth: f32) -> Self {
Self {
base_shape,
half_depth: depth / 2.,
}
}
}
impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {
fn area(&self) -> f32 {
2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())
}
fn volume(&self) -> f32 {
2. * self.base_shape.area() * self.half_depth
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{InvalidDirectionError, Quat};
use approx::assert_relative_eq;
#[test]
fn direction_creation() {
assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));
assert_eq!(
Dir3::new(Vec3::new(0.0, 0.0, 0.0)),
Err(InvalidDirectionError::Zero)
);
assert_eq!(
Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),
Err(InvalidDirectionError::Infinite)
);
assert_eq!(
Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),
Err(InvalidDirectionError::Infinite)
);
assert_eq!(
Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),
Err(InvalidDirectionError::NaN)
);
assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));
assert!(
(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X)
.abs_diff_eq(Vec3::Y, 10e-6)
);
}
#[test]
fn cuboid_closest_point() {
let cuboid = Cuboid::new(2.0, 2.0, 2.0);
assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X);
assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE);
assert_eq!(
cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)),
Vec3::new(0.25, 0.1, 0.3)
);
}
#[test]
fn sphere_closest_point() {
let sphere = Sphere { radius: 1.0 };
assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X);
assert_eq!(
sphere.closest_point(Vec3::NEG_ONE * 10.0),
Vec3::NEG_ONE.normalize()
);
assert_eq!(
sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),
Vec3::new(0.25, 0.1, 0.3)
);
}
#[test]
fn segment_closest_point() {
assert_eq!(
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0))
.closest_point(Vec3::new(1.0, 6.0, -2.0)),
Vec3::new(1.0, 0.0, 0.0)
);
let segments = [
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)),
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)),
Segment3d::new(
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0),
),
];
let points = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-1.0, 1.0, 2.0),
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(5.0, -1.0, 0.5),
Vec3::new(1.0, f32::EPSILON, 0.0),
];
for point in points.iter() {
for segment in segments.iter() {
let closest = segment.closest_point(*point);
assert!(
point.distance_squared(closest) <= point.distance_squared(segment.point1()),
"Closest point must always be at least as close as either vertex."
);
assert!(
point.distance_squared(closest) <= point.distance_squared(segment.point2()),
"Closest point must always be at least as close as either vertex."
);
assert!(
point.distance_squared(closest) <= point.distance_squared(segment.center()),
"Closest point must always be at least as close as the center."
);
let closest_to_closest = segment.closest_point(closest);
assert_relative_eq!(closest_to_closest, closest);
}
}
}
#[test]
fn sphere_math() {
let sphere = Sphere { radius: 4.0 };
assert_eq!(sphere.diameter(), 8.0, "incorrect diameter");
assert_eq!(sphere.area(), 201.06193, "incorrect area");
assert_eq!(sphere.volume(), 268.08257, "incorrect volume");
}
#[test]
fn plane_from_points() {
let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size");
assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation");
}
#[test]
fn infinite_plane_math() {
let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation");
let point_in_plane = Vec3::X + Vec3::Z;
assert_eq!(
plane.signed_distance(origin, point_in_plane),
0.0,
"incorrect distance"
);
assert_eq!(
plane.project_point(origin, point_in_plane),
point_in_plane,
"incorrect point"
);
let point_outside = Vec3::Y;
assert_eq!(
plane.signed_distance(origin, point_outside),
-1.0,
"incorrect distance"
);
assert_eq!(
plane.project_point(origin, point_outside),
Vec3::ZERO,
"incorrect point"
);
let point_outside = Vec3::NEG_Y;
assert_eq!(
plane.signed_distance(origin, point_outside),
1.0,
"incorrect distance"
);
assert_eq!(
plane.project_point(origin, point_outside),
Vec3::ZERO,
"incorrect point"
);
let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5;
let (proj, inj) = plane.isometries_xy(origin);
let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO];
assert_eq!(area_f(triangle), 0.5, "incorrect area");
let triangle_proj = triangle.map(|vec3| proj * vec3);
assert_relative_eq!(area_f(triangle_proj), 0.5);
let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3);
assert_relative_eq!(area_f(triangle_proj_inj), 0.5);
}
#[test]
fn cuboid_math() {
let cuboid = Cuboid::new(3.0, 7.0, 2.0);
assert_eq!(
cuboid,
Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)),
"incorrect dimensions when created from corners"
);
assert_eq!(cuboid.area(), 82.0, "incorrect area");
assert_eq!(cuboid.volume(), 42.0, "incorrect volume");
}
#[test]
fn cylinder_math() {
let cylinder = Cylinder::new(2.0, 9.0);
assert_eq!(
cylinder.base(),
Circle { radius: 2.0 },
"base produces incorrect circle"
);
assert_eq!(
cylinder.lateral_area(),
113.097336,
"incorrect lateral area"
);
assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area");
assert_relative_eq!(cylinder.area(), 138.23007);
assert_eq!(cylinder.volume(), 113.097336, "incorrect volume");
}
#[test]
fn capsule_math() {
let capsule = Capsule3d::new(2.0, 9.0);
assert_eq!(
capsule.to_cylinder(),
Cylinder::new(2.0, 9.0),
"cylinder wasn't created correctly from a capsule"
);
assert_eq!(capsule.area(), 163.36282, "incorrect area");
assert_relative_eq!(capsule.volume(), 146.60765);
}
#[test]
fn cone_math() {
let cone = Cone {
radius: 2.0,
height: 9.0,
};
assert_eq!(
cone.base(),
Circle { radius: 2.0 },
"base produces incorrect circle"
);
assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height");
assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area");
assert_eq!(cone.base_area(), 12.566371, "incorrect base area");
assert_relative_eq!(cone.area(), 70.49447);
assert_eq!(cone.volume(), 37.699111, "incorrect volume");
}
#[test]
fn conical_frustum_math() {
let frustum = ConicalFrustum {
height: 9.0,
radius_top: 1.0,
radius_bottom: 2.0,
};
assert_eq!(
frustum.bottom_base(),
Circle { radius: 2.0 },
"bottom base produces incorrect circle"
);
assert_eq!(
frustum.top_base(),
Circle { radius: 1.0 },
"top base produces incorrect circle"
);
assert_eq!(frustum.slant_height(), 9.055386, "incorrect slant height");
assert_eq!(frustum.lateral_area(), 85.345, "incorrect lateral area");
assert_eq!(
frustum.bottom_base_area(),
12.566371,
"incorrect bottom base area"
);
assert_eq!(frustum.top_base_area(), PI, "incorrect top base area");
assert_eq!(frustum.area(), 101.05296, "incorrect surface area");
assert_eq!(frustum.volume(), 65.97345, "incorrect volume");
}
#[test]
fn torus_math() {
let torus = Torus {
minor_radius: 0.3,
major_radius: 2.8,
};
assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius");
assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius");
assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind");
assert_eq!(
Torus::new(0.0, 1.0).kind(),
TorusKind::Horn,
"incorrect torus kind"
);
assert_eq!(
Torus::new(-0.5, 1.0).kind(),
TorusKind::Spindle,
"incorrect torus kind"
);
assert_eq!(
Torus::new(1.5, 1.0).kind(),
TorusKind::Invalid,
"torus should be invalid"
);
assert_relative_eq!(torus.area(), 33.16187);
assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001);
}
#[test]
fn tetrahedron_math() {
let tetrahedron = Tetrahedron {
vertices: [
Vec3::new(0.3, 1.0, 1.7),
Vec3::new(-2.0, -1.0, 0.0),
Vec3::new(1.8, 0.5, 1.0),
Vec3::new(-1.0, -2.0, 3.5),
],
};
assert_eq!(tetrahedron.area(), 19.251068, "incorrect area");
assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume");
assert_eq!(
tetrahedron.signed_volume(),
3.2058334,
"incorrect signed volume"
);
assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55));
assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area");
assert_eq!(
Tetrahedron::default().volume(),
0.33333334,
"incorrect volume"
);
assert_eq!(
Tetrahedron::default().signed_volume(),
-0.33333334,
"incorrect signed volume"
);
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() {
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 * ops::sqrt(1.25_f32),
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"
);
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());
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 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"
);
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 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"
);
}
}