Path: blob/main/crates/bevy_math/src/primitives/dim3.rs
6596 views
use core::f32::consts::{FRAC_PI_3, PI};12use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};3use crate::{4ops::{self, FloatPow},5Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3,6};78#[cfg(feature = "bevy_reflect")]9use bevy_reflect::{std_traits::ReflectDefault, Reflect};10#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]11use bevy_reflect::{ReflectDeserialize, ReflectSerialize};12use glam::Quat;1314#[cfg(feature = "alloc")]15use alloc::vec::Vec;1617/// A sphere primitive, representing the set of all points some distance from the origin18#[derive(Clone, Copy, Debug, PartialEq)]19#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]20#[cfg_attr(21feature = "bevy_reflect",22derive(Reflect),23reflect(Debug, PartialEq, Default, Clone)24)]25#[cfg_attr(26all(feature = "serialize", feature = "bevy_reflect"),27reflect(Serialize, Deserialize)28)]29pub struct Sphere {30/// The radius of the sphere31pub radius: f32,32}3334impl Primitive3d for Sphere {}3536impl Default for Sphere {37/// Returns the default [`Sphere`] with a radius of `0.5`.38fn default() -> Self {39Self { radius: 0.5 }40}41}4243impl Sphere {44/// Create a new [`Sphere`] from a `radius`45#[inline(always)]46pub const fn new(radius: f32) -> Self {47Self { radius }48}4950/// Get the diameter of the sphere51#[inline(always)]52pub const fn diameter(&self) -> f32 {532.0 * self.radius54}5556/// Finds the point on the sphere that is closest to the given `point`.57///58/// If the point is outside the sphere, the returned point will be on the surface of the sphere.59/// Otherwise, it will be inside the sphere and returned as is.60#[inline(always)]61pub fn closest_point(&self, point: Vec3) -> Vec3 {62let distance_squared = point.length_squared();6364if distance_squared <= self.radius.squared() {65// The point is inside the sphere.66point67} else {68// The point is outside the sphere.69// Find the closest point on the surface of the sphere.70let dir_to_point = point / ops::sqrt(distance_squared);71self.radius * dir_to_point72}73}74}7576impl Measured3d for Sphere {77/// Get the surface area of the sphere78#[inline(always)]79fn area(&self) -> f32 {804.0 * PI * self.radius.squared()81}8283/// Get the volume of the sphere84#[inline(always)]85fn volume(&self) -> f32 {864.0 * FRAC_PI_3 * self.radius.cubed()87}88}8990/// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width.91#[derive(Clone, Copy, Debug, PartialEq)]92#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]93#[cfg_attr(94feature = "bevy_reflect",95derive(Reflect),96reflect(Debug, PartialEq, Default, Clone)97)]98#[cfg_attr(99all(feature = "serialize", feature = "bevy_reflect"),100reflect(Serialize, Deserialize)101)]102pub struct Plane3d {103/// The normal of the plane. The plane will be placed perpendicular to this direction104pub normal: Dir3,105/// Half of the width and height of the plane106pub half_size: Vec2,107}108109impl Primitive3d for Plane3d {}110111impl Default for Plane3d {112/// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`.113fn default() -> Self {114Self {115normal: Dir3::Y,116half_size: Vec2::splat(0.5),117}118}119}120121impl Plane3d {122/// Create a new `Plane3d` from a normal and a half size123///124/// # Panics125///126/// Panics if the given `normal` is zero (or very close to zero), or non-finite.127#[inline(always)]128pub fn new(normal: Vec3, half_size: Vec2) -> Self {129Self {130normal: Dir3::new(normal).expect("normal must be nonzero and finite"),131half_size,132}133}134135/// Create a new `Plane3d` based on three points and compute the geometric center136/// of those points.137///138/// The direction of the plane normal is determined by the winding order139/// of the triangular shape formed by the points.140///141/// # Panics142///143/// Panics if a valid normal can not be computed, for example when the points144/// are *collinear* and lie on the same line.145#[inline(always)]146pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {147let normal = Dir3::new((b - a).cross(c - a)).expect(148"finite plane must be defined by three finite points that don't lie on the same line",149);150let translation = (a + b + c) / 3.0;151152(153Self {154normal,155..Default::default()156},157translation,158)159}160}161162/// An unbounded plane in 3D space. It forms a separating surface through the origin,163/// stretching infinitely far164#[derive(Clone, Copy, Debug, PartialEq)]165#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]166#[cfg_attr(167feature = "bevy_reflect",168derive(Reflect),169reflect(Debug, PartialEq, Default, Clone)170)]171#[cfg_attr(172all(feature = "serialize", feature = "bevy_reflect"),173reflect(Serialize, Deserialize)174)]175pub struct InfinitePlane3d {176/// The normal of the plane. The plane will be placed perpendicular to this direction177pub normal: Dir3,178}179180impl Primitive3d for InfinitePlane3d {}181182impl Default for InfinitePlane3d {183/// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction.184fn default() -> Self {185Self { normal: Dir3::Y }186}187}188189impl InfinitePlane3d {190/// Create a new `InfinitePlane3d` from a normal191///192/// # Panics193///194/// Panics if the given `normal` is zero (or very close to zero), or non-finite.195#[inline(always)]196pub fn new<T: TryInto<Dir3>>(normal: T) -> Self197where198<T as TryInto<Dir3>>::Error: core::fmt::Debug,199{200Self {201normal: normal202.try_into()203.expect("normal must be nonzero and finite"),204}205}206207/// Create a new `InfinitePlane3d` based on three points and compute the geometric center208/// of those points.209///210/// The direction of the plane normal is determined by the winding order211/// of the triangular shape formed by the points.212///213/// # Panics214///215/// Panics if a valid normal can not be computed, for example when the points216/// are *collinear* and lie on the same line.217#[inline(always)]218pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {219let normal = Dir3::new((b - a).cross(c - a)).expect(220"infinite plane must be defined by three finite points that don't lie on the same line",221);222let translation = (a + b + c) / 3.0;223224(Self { normal }, translation)225}226227/// Computes the shortest distance between a plane transformed with the given `isometry` and a228/// `point`. The result is a signed value; it's positive if the point lies in the half-space229/// that the plane's normal vector points towards.230#[inline]231pub fn signed_distance(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> f32 {232let isometry = isometry.into();233self.normal.dot(isometry.inverse() * point)234}235236/// Injects the `point` into this plane transformed with the given `isometry`.237///238/// This projects the point orthogonally along the shortest path onto the plane.239#[inline]240pub fn project_point(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> Vec3 {241point - self.normal * self.signed_distance(isometry, point)242}243244/// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given245/// `origin` to the XY-plane.246///247/// ## Guarantees248///249/// * the transformation is a [congruence] meaning it will preserve all distances and angles of250/// the transformed geometry251/// * uses the least rotation possible to transform the geometry252/// * if two geometries are transformed with the same isometry, then the relations between253/// them, like distances, are also preserved254/// * compared to projections, the transformation is lossless (up to floating point errors)255/// reversible256///257/// ## Non-Guarantees258///259/// * the rotation used is generally not unique260/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to261/// enforce some kind of alignment the user has to use an extra transformation ontop of this262/// one263///264/// See [`isometries_xy`] for example usescases.265///266/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)267/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`268#[inline]269pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d {270let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z);271let transformed_origin = rotation * origin;272Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation)273}274275/// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the276/// given `origin`.277///278/// ## Guarantees279///280/// * the transformation is a [congruence] meaning it will preserve all distances and angles of281/// the transformed geometry282/// * uses the least rotation possible to transform the geometry283/// * if two geometries are transformed with the same isometry, then the relations between284/// them, like distances, are also preserved285/// * compared to projections, the transformation is lossless (up to floating point errors)286/// reversible287///288/// ## Non-Guarantees289///290/// * the rotation used is generally not unique291/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to292/// enforce some kind of alignment the user has to use an extra transformation ontop of this293/// one294///295/// See [`isometries_xy`] for example usescases.296///297/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)298/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`299#[inline]300pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d {301self.isometry_into_xy(origin).inverse()302}303304/// Computes both [isometries] which transforms points from the plane in 3D space with the305/// given `origin` to the XY-plane and back.306///307/// [isometries]: `Isometry3d`308///309/// # Example310///311/// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The312/// workflow would usually look like this:313///314/// ```315/// # use bevy_math::{Vec3, Dir3};316/// # use bevy_math::primitives::InfinitePlane3d;317///318/// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z];319/// let center = (a + b + c) / 3.0;320///321/// let plane = InfinitePlane3d::new(Vec3::ONE);322///323/// let (to_xy, from_xy) = plane.isometries_xy(center);324///325/// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate());326///327/// // apply some algorithm to `triangle_2d`328///329/// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3);330/// ```331#[inline]332pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) {333let projection = self.isometry_into_xy(origin);334(projection, projection.inverse())335}336}337338/// An infinite line going through the origin along a direction in 3D space.339///340/// For a finite line: [`Segment3d`]341#[derive(Clone, Copy, Debug, PartialEq)]342#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]343#[cfg_attr(344feature = "bevy_reflect",345derive(Reflect),346reflect(Debug, PartialEq, Clone)347)]348#[cfg_attr(349all(feature = "serialize", feature = "bevy_reflect"),350reflect(Serialize, Deserialize)351)]352pub struct Line3d {353/// The direction of the line354pub direction: Dir3,355}356357impl Primitive3d for Line3d {}358359/// A line segment defined by two endpoints in 3D space.360#[derive(Clone, Copy, Debug, PartialEq)]361#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]362#[cfg_attr(363feature = "bevy_reflect",364derive(Reflect),365reflect(Debug, PartialEq, Clone)366)]367#[cfg_attr(368all(feature = "serialize", feature = "bevy_reflect"),369reflect(Serialize, Deserialize)370)]371#[doc(alias = "LineSegment3d")]372pub struct Segment3d {373/// The endpoints of the line segment.374pub vertices: [Vec3; 2],375}376377impl Primitive3d for Segment3d {}378379impl Default for Segment3d {380fn default() -> Self {381Self {382vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)],383}384}385}386387impl Segment3d {388/// Create a new `Segment3d` from its endpoints.389#[inline(always)]390pub const fn new(point1: Vec3, point2: Vec3) -> Self {391Self {392vertices: [point1, point2],393}394}395396/// Create a new `Segment3d` centered at the origin with the given direction and length.397///398/// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`.399#[inline(always)]400pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {401let endpoint = 0.5 * length * direction;402Self {403vertices: [-endpoint, endpoint],404}405}406407/// Create a new `Segment3d` centered at the origin from a vector representing408/// the direction and length of the line segment.409///410/// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`.411#[inline(always)]412pub fn from_scaled_direction(scaled_direction: Vec3) -> Self {413let endpoint = 0.5 * scaled_direction;414Self {415vertices: [-endpoint, endpoint],416}417}418419/// Create a new `Segment3d` starting from the origin of the given `ray`,420/// going in the direction of the ray for the given `length`.421///422/// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`.423#[inline(always)]424pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self {425Self {426vertices: [ray.origin, ray.get_point(length)],427}428}429430/// Get the position of the first endpoint of the line segment.431#[inline(always)]432pub const fn point1(&self) -> Vec3 {433self.vertices[0]434}435436/// Get the position of the second endpoint of the line segment.437#[inline(always)]438pub const fn point2(&self) -> Vec3 {439self.vertices[1]440}441442/// Compute the midpoint between the two endpoints of the line segment.443#[inline(always)]444#[doc(alias = "midpoint")]445pub fn center(&self) -> Vec3 {446self.point1().midpoint(self.point2())447}448449/// Compute the length of the line segment.450#[inline(always)]451pub fn length(&self) -> f32 {452self.point1().distance(self.point2())453}454455/// Compute the squared length of the line segment.456#[inline(always)]457pub fn length_squared(&self) -> f32 {458self.point1().distance_squared(self.point2())459}460461/// Compute the normalized direction pointing from the first endpoint to the second endpoint.462///463/// For the non-panicking version, see [`Segment3d::try_direction`].464///465/// # Panics466///467/// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.468#[inline(always)]469pub fn direction(&self) -> Dir3 {470self.try_direction().unwrap_or_else(|err| {471panic!("Failed to compute the direction of a line segment: {err}")472})473}474475/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.476///477/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,478/// for example when the endpoints are coincident, NaN, or infinite.479#[inline(always)]480pub fn try_direction(&self) -> Result<Dir3, InvalidDirectionError> {481Dir3::new(self.scaled_direction())482}483484/// Compute the vector from the first endpoint to the second endpoint.485#[inline(always)]486pub fn scaled_direction(&self) -> Vec3 {487self.point2() - self.point1()488}489490/// Compute the segment transformed by the given [`Isometry3d`].491#[inline(always)]492pub fn transformed(&self, isometry: impl Into<Isometry3d>) -> Self {493let isometry: Isometry3d = isometry.into();494Self::new(495isometry.transform_point(self.point1()).into(),496isometry.transform_point(self.point2()).into(),497)498}499500/// Compute the segment translated by the given vector.501#[inline(always)]502pub fn translated(&self, translation: Vec3) -> Segment3d {503Self::new(self.point1() + translation, self.point2() + translation)504}505506/// Compute the segment rotated around the origin by the given rotation.507#[inline(always)]508pub fn rotated(&self, rotation: Quat) -> Segment3d {509Segment3d::new(rotation * self.point1(), rotation * self.point2())510}511512/// Compute the segment rotated around the given point by the given rotation.513#[inline(always)]514pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {515// We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back516let offset = self.translated(-point);517let rotated = offset.rotated(rotation);518rotated.translated(point)519}520521/// Compute the segment rotated around its own center.522#[inline(always)]523pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {524self.rotated_around(rotation, self.center())525}526527/// Compute the segment with its center at the origin, keeping the same direction and length.528#[inline(always)]529pub fn centered(&self) -> Segment3d {530let center = self.center();531self.translated(-center)532}533534/// Compute the segment with a new length, keeping the same direction and center.535#[inline(always)]536pub fn resized(&self, length: f32) -> Segment3d {537let offset_from_origin = self.center();538let centered = self.translated(-offset_from_origin);539let ratio = length / self.length();540let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);541segment.translated(offset_from_origin)542}543544/// Reverses the direction of the line segment by swapping the endpoints.545#[inline(always)]546pub fn reverse(&mut self) {547let [point1, point2] = &mut self.vertices;548core::mem::swap(point1, point2);549}550551/// Returns the line segment with its direction reversed by swapping the endpoints.552#[inline(always)]553#[must_use]554pub fn reversed(mut self) -> Self {555self.reverse();556self557}558559/// Returns the point on the [`Segment3d`] that is closest to the specified `point`.560#[inline(always)]561pub fn closest_point(&self, point: Vec3) -> Vec3 {562// `point`563// x564// ^|565// / |566//`offset`/ |567// / | `segment_vector`568// x----.-------------->x569// 0 t 1570let segment_vector = self.vertices[1] - self.vertices[0];571let offset = point - self.vertices[0];572// The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment.573let projection_scaled = segment_vector.dot(offset);574575// `point` is too far "left" in the picture576if projection_scaled <= 0.0 {577return self.vertices[0];578}579580let length_squared = segment_vector.length_squared();581// `point` is too far "right" in the picture582if projection_scaled >= length_squared {583return self.vertices[1];584}585586// Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line.587let t = projection_scaled / length_squared;588self.vertices[0] + t * segment_vector589}590}591592impl From<[Vec3; 2]> for Segment3d {593#[inline(always)]594fn from(vertices: [Vec3; 2]) -> Self {595Self { vertices }596}597}598599impl From<(Vec3, Vec3)> for Segment3d {600#[inline(always)]601fn from((point1, point2): (Vec3, Vec3)) -> Self {602Self::new(point1, point2)603}604}605606/// A series of connected line segments in 3D space.607#[cfg(feature = "alloc")]608#[derive(Clone, Debug, PartialEq)]609#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]610#[cfg_attr(611feature = "bevy_reflect",612derive(Reflect),613reflect(Debug, PartialEq, Clone)614)]615#[cfg_attr(616all(feature = "serialize", feature = "bevy_reflect"),617reflect(Serialize, Deserialize)618)]619pub struct Polyline3d {620/// The vertices of the polyline621pub vertices: Vec<Vec3>,622}623624#[cfg(feature = "alloc")]625impl Primitive3d for Polyline3d {}626627#[cfg(feature = "alloc")]628impl FromIterator<Vec3> for Polyline3d {629fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {630Self {631vertices: iter.into_iter().collect(),632}633}634}635636#[cfg(feature = "alloc")]637impl Default for Polyline3d {638fn default() -> Self {639Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)])640}641}642643#[cfg(feature = "alloc")]644impl Polyline3d {645/// Create a new `Polyline3d` from its vertices646pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {647Self::from_iter(vertices)648}649650/// Create a new `Polyline3d` from two endpoints with subdivision points.651/// `subdivisions = 0` creates a simple line with just start and end points.652/// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc.653pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self {654let total_vertices = subdivisions + 2;655let mut vertices = Vec::with_capacity(total_vertices);656657let step = (end - start) / (subdivisions + 1) as f32;658for i in 0..total_vertices {659vertices.push(start + step * i as f32);660}661662Self { vertices }663}664}665666/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not667/// required to be the same.668#[derive(Clone, Copy, Debug, PartialEq)]669#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]670#[cfg_attr(671feature = "bevy_reflect",672derive(Reflect),673reflect(Debug, PartialEq, Default, Clone)674)]675#[cfg_attr(676all(feature = "serialize", feature = "bevy_reflect"),677reflect(Serialize, Deserialize)678)]679pub struct Cuboid {680/// Half of the width, height and depth of the cuboid681pub half_size: Vec3,682}683684impl Primitive3d for Cuboid {}685686impl Default for Cuboid {687/// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`.688fn default() -> Self {689Self {690half_size: Vec3::splat(0.5),691}692}693}694695impl Cuboid {696/// Create a new `Cuboid` from a full x, y, and z length697#[inline(always)]698pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {699Self::from_size(Vec3::new(x_length, y_length, z_length))700}701702/// Create a new `Cuboid` from a given full size703#[inline(always)]704pub const fn from_size(size: Vec3) -> Self {705Self {706half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0),707}708}709710/// Create a new `Cuboid` from two corner points711#[inline(always)]712pub fn from_corners(point1: Vec3, point2: Vec3) -> Self {713Self {714half_size: (point2 - point1).abs() / 2.0,715}716}717718/// Create a `Cuboid` from a single length.719/// The resulting `Cuboid` will be the same size in every direction.720#[inline(always)]721pub const fn from_length(length: f32) -> Self {722Self {723half_size: Vec3::splat(length / 2.0),724}725}726727/// Get the size of the cuboid728#[inline(always)]729pub fn size(&self) -> Vec3 {7302.0 * self.half_size731}732733/// Finds the point on the cuboid that is closest to the given `point`.734///735/// If the point is outside the cuboid, the returned point will be on the surface of the cuboid.736/// Otherwise, it will be inside the cuboid and returned as is.737#[inline(always)]738pub fn closest_point(&self, point: Vec3) -> Vec3 {739// Clamp point coordinates to the cuboid740point.clamp(-self.half_size, self.half_size)741}742}743744impl Measured3d for Cuboid {745/// Get the surface area of the cuboid746#[inline(always)]747fn area(&self) -> f32 {7488.0 * (self.half_size.x * self.half_size.y749+ self.half_size.y * self.half_size.z750+ self.half_size.x * self.half_size.z)751}752753/// Get the volume of the cuboid754#[inline(always)]755fn volume(&self) -> f32 {7568.0 * self.half_size.x * self.half_size.y * self.half_size.z757}758}759760/// A cylinder primitive centered on the origin761#[derive(Clone, Copy, Debug, PartialEq)]762#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]763#[cfg_attr(764feature = "bevy_reflect",765derive(Reflect),766reflect(Debug, PartialEq, Default, Clone)767)]768#[cfg_attr(769all(feature = "serialize", feature = "bevy_reflect"),770reflect(Serialize, Deserialize)771)]772pub struct Cylinder {773/// The radius of the cylinder774pub radius: f32,775/// The half height of the cylinder776pub half_height: f32,777}778779impl Primitive3d for Cylinder {}780781impl Default for Cylinder {782/// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`.783fn default() -> Self {784Self {785radius: 0.5,786half_height: 0.5,787}788}789}790791impl Cylinder {792/// Create a new `Cylinder` from a radius and full height793#[inline(always)]794pub const fn new(radius: f32, height: f32) -> Self {795Self {796radius,797half_height: height / 2.0,798}799}800801/// Get the base of the cylinder as a [`Circle`]802#[inline(always)]803pub const fn base(&self) -> Circle {804Circle {805radius: self.radius,806}807}808809/// Get the surface area of the side of the cylinder,810/// also known as the lateral area811#[inline(always)]812#[doc(alias = "side_area")]813pub const fn lateral_area(&self) -> f32 {8144.0 * PI * self.radius * self.half_height815}816817/// Get the surface area of one base of the cylinder818#[inline(always)]819pub fn base_area(&self) -> f32 {820PI * self.radius.squared()821}822}823824impl Measured3d for Cylinder {825/// Get the total surface area of the cylinder826#[inline(always)]827fn area(&self) -> f32 {8282.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)829}830831/// Get the volume of the cylinder832#[inline(always)]833fn volume(&self) -> f32 {834self.base_area() * 2.0 * self.half_height835}836}837838/// A 3D capsule primitive centered on the origin839/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line840#[derive(Clone, Copy, Debug, PartialEq)]841#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]842#[cfg_attr(843feature = "bevy_reflect",844derive(Reflect),845reflect(Debug, PartialEq, Default, Clone)846)]847#[cfg_attr(848all(feature = "serialize", feature = "bevy_reflect"),849reflect(Serialize, Deserialize)850)]851pub struct Capsule3d {852/// The radius of the capsule853pub radius: f32,854/// Half the height of the capsule, excluding the hemispheres855pub half_length: f32,856}857858impl Primitive3d for Capsule3d {}859860impl Default for Capsule3d {861/// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`.862/// The total height is `2.0`.863fn default() -> Self {864Self {865radius: 0.5,866half_length: 0.5,867}868}869}870871impl Capsule3d {872/// Create a new `Capsule3d` from a radius and length873pub const fn new(radius: f32, length: f32) -> Self {874Self {875radius,876half_length: length / 2.0,877}878}879880/// Get the part connecting the hemispherical ends881/// of the capsule as a [`Cylinder`]882#[inline(always)]883pub const fn to_cylinder(&self) -> Cylinder {884Cylinder {885radius: self.radius,886half_height: self.half_length,887}888}889}890891impl Measured3d for Capsule3d {892/// Get the surface area of the capsule893#[inline(always)]894fn area(&self) -> f32 {895// Modified version of 2pi * r * (2r + h)8964.0 * PI * self.radius * (self.radius + self.half_length)897}898899/// Get the volume of the capsule900#[inline(always)]901fn volume(&self) -> f32 {902// Modified version of pi * r^2 * (4/3 * r + a)903let diameter = self.radius * 2.0;904PI * self.radius * diameter * (diameter / 3.0 + self.half_length)905}906}907908/// A cone primitive centered on the midpoint between the tip of the cone and the center of its base.909///910/// The cone is oriented with its tip pointing towards the Y axis.911#[derive(Clone, Copy, Debug, PartialEq)]912#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]913#[cfg_attr(914feature = "bevy_reflect",915derive(Reflect),916reflect(Debug, PartialEq, Default, Clone)917)]918#[cfg_attr(919all(feature = "serialize", feature = "bevy_reflect"),920reflect(Serialize, Deserialize)921)]922pub struct Cone {923/// The radius of the base924pub radius: f32,925/// The height of the cone926pub height: f32,927}928929impl Primitive3d for Cone {}930931impl Default for Cone {932/// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`.933fn default() -> Self {934Self {935radius: 0.5,936height: 1.0,937}938}939}940941impl Cone {942/// Create a new [`Cone`] from a radius and height.943pub const fn new(radius: f32, height: f32) -> Self {944Self { radius, height }945}946/// Get the base of the cone as a [`Circle`]947#[inline(always)]948pub const fn base(&self) -> Circle {949Circle {950radius: self.radius,951}952}953954/// Get the slant height of the cone, the length of the line segment955/// connecting a point on the base to the apex956#[inline(always)]957#[doc(alias = "side_length")]958pub fn slant_height(&self) -> f32 {959ops::hypot(self.radius, self.height)960}961962/// Get the surface area of the side of the cone,963/// also known as the lateral area964#[inline(always)]965#[doc(alias = "side_area")]966pub fn lateral_area(&self) -> f32 {967PI * self.radius * self.slant_height()968}969970/// Get the surface area of the base of the cone971#[inline(always)]972pub fn base_area(&self) -> f32 {973PI * self.radius.squared()974}975}976977impl Measured3d for Cone {978/// Get the total surface area of the cone979#[inline(always)]980fn area(&self) -> f32 {981self.base_area() + self.lateral_area()982}983984/// Get the volume of the cone985#[inline(always)]986fn volume(&self) -> f32 {987(self.base_area() * self.height) / 3.0988}989}990991/// A conical frustum primitive.992/// A conical frustum can be created993/// by slicing off a section of a cone.994#[derive(Clone, Copy, Debug, PartialEq)]995#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]996#[cfg_attr(997feature = "bevy_reflect",998derive(Reflect),999reflect(Debug, PartialEq, Default, Clone)1000)]1001#[cfg_attr(1002all(feature = "serialize", feature = "bevy_reflect"),1003reflect(Serialize, Deserialize)1004)]1005pub struct ConicalFrustum {1006/// The radius of the top of the frustum1007pub radius_top: f32,1008/// The radius of the base of the frustum1009pub radius_bottom: f32,1010/// The height of the frustum1011pub height: f32,1012}10131014impl Primitive3d for ConicalFrustum {}10151016impl Default for ConicalFrustum {1017/// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`.1018fn default() -> Self {1019Self {1020radius_top: 0.25,1021radius_bottom: 0.5,1022height: 0.5,1023}1024}1025}10261027/// The type of torus determined by the minor and major radii1028#[derive(Clone, Copy, Debug, PartialEq, Eq)]1029pub enum TorusKind {1030/// A torus that has a ring.1031/// The major radius is greater than the minor radius1032Ring,1033/// A torus that has no hole but also doesn't intersect itself.1034/// The major radius is equal to the minor radius1035Horn,1036/// A self-intersecting torus.1037/// The major radius is less than the minor radius1038Spindle,1039/// A torus with non-geometric properties like1040/// a minor or major radius that is non-positive,1041/// infinite, or `NaN`1042Invalid,1043}10441045/// A torus primitive, often representing a ring or donut shape1046/// The set of points some distance from a circle centered at the origin1047#[derive(Clone, Copy, Debug, PartialEq)]1048#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1049#[cfg_attr(1050feature = "bevy_reflect",1051derive(Reflect),1052reflect(Debug, PartialEq, Default, Clone)1053)]1054#[cfg_attr(1055all(feature = "serialize", feature = "bevy_reflect"),1056reflect(Serialize, Deserialize)1057)]1058pub struct Torus {1059/// The radius of the tube of the torus1060#[doc(1061alias = "ring_radius",1062alias = "tube_radius",1063alias = "cross_section_radius"1064)]1065pub minor_radius: f32,1066/// The distance from the center of the torus to the center of the tube1067#[doc(alias = "radius_of_revolution")]1068pub major_radius: f32,1069}10701071impl Primitive3d for Torus {}10721073impl Default for Torus {1074/// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`.1075fn default() -> Self {1076Self {1077minor_radius: 0.25,1078major_radius: 0.75,1079}1080}1081}10821083impl Torus {1084/// Create a new `Torus` from an inner and outer radius.1085///1086/// The inner radius is the radius of the hole, and the outer radius1087/// is the radius of the entire object1088#[inline(always)]1089pub const fn new(inner_radius: f32, outer_radius: f32) -> Self {1090let minor_radius = (outer_radius - inner_radius) / 2.0;1091let major_radius = outer_radius - minor_radius;10921093Self {1094minor_radius,1095major_radius,1096}1097}10981099/// Get the inner radius of the torus.1100/// For a ring torus, this corresponds to the radius of the hole,1101/// or `major_radius - minor_radius`1102#[inline(always)]1103pub const fn inner_radius(&self) -> f32 {1104self.major_radius - self.minor_radius1105}11061107/// Get the outer radius of the torus.1108/// This corresponds to the overall radius of the entire object,1109/// or `major_radius + minor_radius`1110#[inline(always)]1111pub const fn outer_radius(&self) -> f32 {1112self.major_radius + self.minor_radius1113}11141115/// Get the [`TorusKind`] determined by the minor and major radii.1116///1117/// The torus can either be a *ring torus* that has a hole,1118/// a *horn torus* that doesn't have a hole but also isn't self-intersecting,1119/// or a *spindle torus* that is self-intersecting.1120///1121/// If the minor or major radius is non-positive, infinite, or `NaN`,1122/// [`TorusKind::Invalid`] is returned1123#[inline(always)]1124pub fn kind(&self) -> TorusKind {1125// Invalid if minor or major radius is non-positive, infinite, or NaN1126if self.minor_radius <= 0.01127|| !self.minor_radius.is_finite()1128|| self.major_radius <= 0.01129|| !self.major_radius.is_finite()1130{1131return TorusKind::Invalid;1132}11331134match self.major_radius.partial_cmp(&self.minor_radius).unwrap() {1135core::cmp::Ordering::Greater => TorusKind::Ring,1136core::cmp::Ordering::Equal => TorusKind::Horn,1137core::cmp::Ordering::Less => TorusKind::Spindle,1138}1139}1140}11411142impl Measured3d for Torus {1143/// Get the surface area of the torus. Note that this only produces1144/// the expected result when the torus has a ring and isn't self-intersecting1145#[inline(always)]1146fn area(&self) -> f32 {11474.0 * PI.squared() * self.major_radius * self.minor_radius1148}11491150/// Get the volume of the torus. Note that this only produces1151/// the expected result when the torus has a ring and isn't self-intersecting1152#[inline(always)]1153fn volume(&self) -> f32 {11542.0 * PI.squared() * self.major_radius * self.minor_radius.squared()1155}1156}11571158/// A 3D triangle primitive.1159#[derive(Clone, Copy, Debug, PartialEq)]1160#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1161#[cfg_attr(1162feature = "bevy_reflect",1163derive(Reflect),1164reflect(Debug, PartialEq, Default, Clone)1165)]1166#[cfg_attr(1167all(feature = "serialize", feature = "bevy_reflect"),1168reflect(Serialize, Deserialize)1169)]1170pub struct Triangle3d {1171/// The vertices of the triangle.1172pub vertices: [Vec3; 3],1173}11741175impl Primitive3d for Triangle3d {}11761177impl Default for Triangle3d {1178/// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`.1179fn default() -> Self {1180Self {1181vertices: [1182Vec3::new(0.0, 0.5, 0.0),1183Vec3::new(-0.5, -0.5, 0.0),1184Vec3::new(0.5, -0.5, 0.0),1185],1186}1187}1188}11891190impl Triangle3d {1191/// Create a new [`Triangle3d`] from points `a`, `b`, and `c`.1192#[inline(always)]1193pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {1194Self {1195vertices: [a, b, c],1196}1197}11981199/// Get the normal of the triangle in the direction of the right-hand rule, assuming1200/// the vertices are ordered in a counter-clockwise direction.1201///1202/// The normal is computed as the cross product of the vectors `ab` and `ac`.1203///1204/// # Errors1205///1206/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length1207/// of the given vector is zero (or very close to zero), infinite, or `NaN`.1208#[inline(always)]1209pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {1210let [a, b, c] = self.vertices;1211let ab = b - a;1212let ac = c - a;1213Dir3::new(ab.cross(ac))1214}12151216/// Checks if the triangle is degenerate, meaning it has zero area.1217///1218/// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`.1219/// This indicates that the three vertices are collinear or nearly collinear.1220#[inline(always)]1221pub fn is_degenerate(&self) -> bool {1222let [a, b, c] = self.vertices;1223let ab = b - a;1224let ac = c - a;1225ab.cross(ac).length() < 10e-71226}12271228/// Checks if the triangle is acute, meaning all angles are less than 90 degrees1229#[inline(always)]1230pub fn is_acute(&self) -> bool {1231let [a, b, c] = self.vertices;1232let ab = b - a;1233let bc = c - b;1234let ca = a - c;12351236// a^2 + b^2 < c^2 for an acute triangle1237let mut side_lengths = [1238ab.length_squared(),1239bc.length_squared(),1240ca.length_squared(),1241];1242side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());1243side_lengths[0] + side_lengths[1] > side_lengths[2]1244}12451246/// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees1247#[inline(always)]1248pub fn is_obtuse(&self) -> bool {1249let [a, b, c] = self.vertices;1250let ab = b - a;1251let bc = c - b;1252let ca = a - c;12531254// a^2 + b^2 > c^2 for an obtuse triangle1255let mut side_lengths = [1256ab.length_squared(),1257bc.length_squared(),1258ca.length_squared(),1259];1260side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());1261side_lengths[0] + side_lengths[1] < side_lengths[2]1262}12631264/// Reverse the triangle by swapping the first and last vertices.1265#[inline(always)]1266pub fn reverse(&mut self) {1267self.vertices.swap(0, 2);1268}12691270/// This triangle but reversed.1271#[inline(always)]1272#[must_use]1273pub fn reversed(mut self) -> Triangle3d {1274self.reverse();1275self1276}12771278/// Get the centroid of the triangle.1279///1280/// This function finds the geometric center of the triangle by averaging the vertices:1281/// `centroid = (a + b + c) / 3`.1282#[doc(alias("center", "barycenter", "baricenter"))]1283#[inline(always)]1284pub fn centroid(&self) -> Vec3 {1285(self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.01286}12871288/// Get the largest side of the triangle.1289///1290/// Returns the two points that form the largest side of the triangle.1291#[inline(always)]1292pub fn largest_side(&self) -> (Vec3, Vec3) {1293let [a, b, c] = self.vertices;1294let ab = b - a;1295let bc = c - b;1296let ca = a - c;12971298let mut largest_side_points = (a, b);1299let mut largest_side_length = ab.length_squared();13001301let bc_length = bc.length_squared();1302if bc_length > largest_side_length {1303largest_side_points = (b, c);1304largest_side_length = bc_length;1305}13061307let ca_length = ca.length_squared();1308if ca_length > largest_side_length {1309largest_side_points = (a, c);1310}13111312largest_side_points1313}13141315/// Get the circumcenter of the triangle.1316#[inline(always)]1317pub fn circumcenter(&self) -> Vec3 {1318if self.is_degenerate() {1319// If the triangle is degenerate, the circumcenter is the midpoint of the largest side.1320let (p1, p2) = self.largest_side();1321return (p1 + p2) / 2.0;1322}13231324let [a, b, c] = self.vertices;1325let ab = b - a;1326let ac = c - a;1327let n = ab.cross(ac);13281329// Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d1330a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))1331/ (2.0 * n.length_squared()))1332}1333}13341335impl Measured2d for Triangle3d {1336/// Get the area of the triangle.1337#[inline(always)]1338fn area(&self) -> f32 {1339let [a, b, c] = self.vertices;1340let ab = b - a;1341let ac = c - a;1342ab.cross(ac).length() / 2.01343}13441345/// Get the perimeter of the triangle.1346#[inline(always)]1347fn perimeter(&self) -> f32 {1348let [a, b, c] = self.vertices;1349a.distance(b) + b.distance(c) + c.distance(a)1350}1351}13521353/// A tetrahedron primitive.1354#[derive(Clone, Copy, Debug, PartialEq)]1355#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1356#[cfg_attr(1357feature = "bevy_reflect",1358derive(Reflect),1359reflect(Debug, PartialEq, Default, Clone)1360)]1361#[cfg_attr(1362all(feature = "serialize", feature = "bevy_reflect"),1363reflect(Serialize, Deserialize)1364)]1365pub struct Tetrahedron {1366/// The vertices of the tetrahedron.1367pub vertices: [Vec3; 4],1368}13691370impl Primitive3d for Tetrahedron {}13711372impl Default for Tetrahedron {1373/// Returns the default [`Tetrahedron`] with the vertices1374/// `[0.5, 0.5, 0.5]`, `[-0.5, 0.5, -0.5]`, `[-0.5, -0.5, 0.5]` and `[0.5, -0.5, -0.5]`.1375fn default() -> Self {1376Self {1377vertices: [1378Vec3::new(0.5, 0.5, 0.5),1379Vec3::new(-0.5, 0.5, -0.5),1380Vec3::new(-0.5, -0.5, 0.5),1381Vec3::new(0.5, -0.5, -0.5),1382],1383}1384}1385}13861387impl Tetrahedron {1388/// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`.1389#[inline(always)]1390pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self {1391Self {1392vertices: [a, b, c, d],1393}1394}13951396/// Get the signed volume of the tetrahedron.1397///1398/// If it's negative, the normal vector of the face defined by1399/// the first three points using the right-hand rule points1400/// away from the fourth vertex.1401#[inline(always)]1402pub fn signed_volume(&self) -> f32 {1403let [a, b, c, d] = self.vertices;1404let ab = b - a;1405let ac = c - a;1406let ad = d - a;1407Mat3::from_cols(ab, ac, ad).determinant() / 6.01408}14091410/// Get the centroid of the tetrahedron.1411///1412/// This function finds the geometric center of the tetrahedron1413/// by averaging the vertices: `centroid = (a + b + c + d) / 4`.1414#[doc(alias("center", "barycenter", "baricenter"))]1415#[inline(always)]1416pub fn centroid(&self) -> Vec3 {1417(self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.01418}14191420/// Get the triangles that form the faces of this tetrahedron.1421///1422/// Note that the orientations of the faces are determined by that of the tetrahedron; if the1423/// signed volume of this tetrahedron is positive, then the triangles' normals will point1424/// outward, and if the signed volume is negative they will point inward.1425#[inline(always)]1426pub fn faces(&self) -> [Triangle3d; 4] {1427let [a, b, c, d] = self.vertices;1428[1429Triangle3d::new(b, c, d),1430Triangle3d::new(a, c, d).reversed(),1431Triangle3d::new(a, b, d),1432Triangle3d::new(a, b, c).reversed(),1433]1434}1435}14361437impl Measured3d for Tetrahedron {1438/// Get the surface area of the tetrahedron.1439#[inline(always)]1440fn area(&self) -> f32 {1441let [a, b, c, d] = self.vertices;1442let ab = b - a;1443let ac = c - a;1444let ad = d - a;1445let bc = c - b;1446let bd = d - b;1447(ab.cross(ac).length()1448+ ab.cross(ad).length()1449+ ac.cross(ad).length()1450+ bc.cross(bd).length())1451/ 2.01452}14531454/// Get the volume of the tetrahedron.1455#[inline(always)]1456fn volume(&self) -> f32 {1457ops::abs(self.signed_volume())1458}1459}14601461/// A 3D shape representing an extruded 2D `base_shape`.1462///1463/// Extruding a shape effectively "thickens" a 2D shapes,1464/// creating a shape with the same cross-section over the entire depth.1465///1466/// The resulting volumes are prisms.1467/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder.1468#[doc(alias = "Prism")]1469#[derive(Clone, Copy, Debug, PartialEq)]1470#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1471pub struct Extrusion<T: Primitive2d> {1472/// The base shape of the extrusion1473pub base_shape: T,1474/// Half of the depth of the extrusion1475pub half_depth: f32,1476}14771478impl<T: Primitive2d> Primitive3d for Extrusion<T> {}14791480impl<T: Primitive2d> Extrusion<T> {1481/// Create a new `Extrusion<T>` from a given `base_shape` and `depth`1482pub fn new(base_shape: T, depth: f32) -> Self {1483Self {1484base_shape,1485half_depth: depth / 2.,1486}1487}1488}14891490impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {1491/// Get the surface area of the extrusion1492fn area(&self) -> f32 {14932. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())1494}14951496/// Get the volume of the extrusion1497fn volume(&self) -> f32 {14982. * self.base_shape.area() * self.half_depth1499}1500}15011502#[cfg(test)]1503mod tests {1504// Reference values were computed by hand and/or with external tools15051506use super::*;1507use crate::{InvalidDirectionError, Quat};1508use approx::assert_relative_eq;15091510#[test]1511fn direction_creation() {1512assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));1513assert_eq!(1514Dir3::new(Vec3::new(0.0, 0.0, 0.0)),1515Err(InvalidDirectionError::Zero)1516);1517assert_eq!(1518Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),1519Err(InvalidDirectionError::Infinite)1520);1521assert_eq!(1522Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),1523Err(InvalidDirectionError::Infinite)1524);1525assert_eq!(1526Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),1527Err(InvalidDirectionError::NaN)1528);1529assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));15301531// Test rotation1532assert!(1533(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X)1534.abs_diff_eq(Vec3::Y, 10e-6)1535);1536}15371538#[test]1539fn cuboid_closest_point() {1540let cuboid = Cuboid::new(2.0, 2.0, 2.0);1541assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X);1542assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE);1543assert_eq!(1544cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)),1545Vec3::new(0.25, 0.1, 0.3)1546);1547}15481549#[test]1550fn sphere_closest_point() {1551let sphere = Sphere { radius: 1.0 };1552assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X);1553assert_eq!(1554sphere.closest_point(Vec3::NEG_ONE * 10.0),1555Vec3::NEG_ONE.normalize()1556);1557assert_eq!(1558sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),1559Vec3::new(0.25, 0.1, 0.3)1560);1561}15621563#[test]1564fn segment_closest_point() {1565assert_eq!(1566Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0))1567.closest_point(Vec3::new(1.0, 6.0, -2.0)),1568Vec3::new(1.0, 0.0, 0.0)1569);15701571let segments = [1572Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)),1573Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),1574Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)),1575Segment3d::new(1576Vec3::new(1.0, 0.0, 0.0),1577Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0),1578),1579];1580let points = [1581Vec3::new(0.0, 0.0, 0.0),1582Vec3::new(1.0, 0.0, 0.0),1583Vec3::new(-1.0, 1.0, 2.0),1584Vec3::new(1.0, 1.0, 1.0),1585Vec3::new(-1.0, 0.0, 0.0),1586Vec3::new(5.0, -1.0, 0.5),1587Vec3::new(1.0, f32::EPSILON, 0.0),1588];15891590for point in points.iter() {1591for segment in segments.iter() {1592let closest = segment.closest_point(*point);1593assert!(1594point.distance_squared(closest) <= point.distance_squared(segment.point1()),1595"Closest point must always be at least as close as either vertex."1596);1597assert!(1598point.distance_squared(closest) <= point.distance_squared(segment.point2()),1599"Closest point must always be at least as close as either vertex."1600);1601assert!(1602point.distance_squared(closest) <= point.distance_squared(segment.center()),1603"Closest point must always be at least as close as the center."1604);1605let closest_to_closest = segment.closest_point(closest);1606// Closest point must already be on the segment1607assert_relative_eq!(closest_to_closest, closest);1608}1609}1610}16111612#[test]1613fn sphere_math() {1614let sphere = Sphere { radius: 4.0 };1615assert_eq!(sphere.diameter(), 8.0, "incorrect diameter");1616assert_eq!(sphere.area(), 201.06193, "incorrect area");1617assert_eq!(sphere.volume(), 268.08257, "incorrect volume");1618}16191620#[test]1621fn plane_from_points() {1622let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);1623assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");1624assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size");1625assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation");1626}16271628#[test]1629fn infinite_plane_math() {1630let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);1631assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");1632assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation");16331634let point_in_plane = Vec3::X + Vec3::Z;1635assert_eq!(1636plane.signed_distance(origin, point_in_plane),16370.0,1638"incorrect distance"1639);1640assert_eq!(1641plane.project_point(origin, point_in_plane),1642point_in_plane,1643"incorrect point"1644);16451646let point_outside = Vec3::Y;1647assert_eq!(1648plane.signed_distance(origin, point_outside),1649-1.0,1650"incorrect distance"1651);1652assert_eq!(1653plane.project_point(origin, point_outside),1654Vec3::ZERO,1655"incorrect point"1656);16571658let point_outside = Vec3::NEG_Y;1659assert_eq!(1660plane.signed_distance(origin, point_outside),16611.0,1662"incorrect distance"1663);1664assert_eq!(1665plane.project_point(origin, point_outside),1666Vec3::ZERO,1667"incorrect point"1668);16691670let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5;1671let (proj, inj) = plane.isometries_xy(origin);16721673let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO];1674assert_eq!(area_f(triangle), 0.5, "incorrect area");16751676let triangle_proj = triangle.map(|vec3| proj * vec3);1677assert_relative_eq!(area_f(triangle_proj), 0.5);16781679let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3);1680assert_relative_eq!(area_f(triangle_proj_inj), 0.5);1681}16821683#[test]1684fn cuboid_math() {1685let cuboid = Cuboid::new(3.0, 7.0, 2.0);1686assert_eq!(1687cuboid,1688Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)),1689"incorrect dimensions when created from corners"1690);1691assert_eq!(cuboid.area(), 82.0, "incorrect area");1692assert_eq!(cuboid.volume(), 42.0, "incorrect volume");1693}16941695#[test]1696fn cylinder_math() {1697let cylinder = Cylinder::new(2.0, 9.0);1698assert_eq!(1699cylinder.base(),1700Circle { radius: 2.0 },1701"base produces incorrect circle"1702);1703assert_eq!(1704cylinder.lateral_area(),1705113.097336,1706"incorrect lateral area"1707);1708assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area");1709assert_relative_eq!(cylinder.area(), 138.23007);1710assert_eq!(cylinder.volume(), 113.097336, "incorrect volume");1711}17121713#[test]1714fn capsule_math() {1715let capsule = Capsule3d::new(2.0, 9.0);1716assert_eq!(1717capsule.to_cylinder(),1718Cylinder::new(2.0, 9.0),1719"cylinder wasn't created correctly from a capsule"1720);1721assert_eq!(capsule.area(), 163.36282, "incorrect area");1722assert_relative_eq!(capsule.volume(), 146.60765);1723}17241725#[test]1726fn cone_math() {1727let cone = Cone {1728radius: 2.0,1729height: 9.0,1730};1731assert_eq!(1732cone.base(),1733Circle { radius: 2.0 },1734"base produces incorrect circle"1735);1736assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height");1737assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area");1738assert_eq!(cone.base_area(), 12.566371, "incorrect base area");1739assert_relative_eq!(cone.area(), 70.49447);1740assert_eq!(cone.volume(), 37.699111, "incorrect volume");1741}17421743#[test]1744fn torus_math() {1745let torus = Torus {1746minor_radius: 0.3,1747major_radius: 2.8,1748};1749assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius");1750assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius");1751assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind");1752assert_eq!(1753Torus::new(0.0, 1.0).kind(),1754TorusKind::Horn,1755"incorrect torus kind"1756);1757assert_eq!(1758Torus::new(-0.5, 1.0).kind(),1759TorusKind::Spindle,1760"incorrect torus kind"1761);1762assert_eq!(1763Torus::new(1.5, 1.0).kind(),1764TorusKind::Invalid,1765"torus should be invalid"1766);1767assert_relative_eq!(torus.area(), 33.16187);1768assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001);1769}17701771#[test]1772fn tetrahedron_math() {1773let tetrahedron = Tetrahedron {1774vertices: [1775Vec3::new(0.3, 1.0, 1.7),1776Vec3::new(-2.0, -1.0, 0.0),1777Vec3::new(1.8, 0.5, 1.0),1778Vec3::new(-1.0, -2.0, 3.5),1779],1780};1781assert_eq!(tetrahedron.area(), 19.251068, "incorrect area");1782assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume");1783assert_eq!(1784tetrahedron.signed_volume(),17853.2058334,1786"incorrect signed volume"1787);1788assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55));17891790assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area");1791assert_eq!(1792Tetrahedron::default().volume(),17930.33333334,1794"incorrect volume"1795);1796assert_eq!(1797Tetrahedron::default().signed_volume(),1798-0.33333334,1799"incorrect signed volume"1800);1801assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);1802}18031804#[test]1805fn extrusion_math() {1806let circle = Circle::new(0.75);1807let cylinder = Extrusion::new(circle, 2.5);1808assert_eq!(cylinder.area(), 15.315264, "incorrect surface area");1809assert_eq!(cylinder.volume(), 4.417865, "incorrect volume");18101811let annulus = crate::primitives::Annulus::new(0.25, 1.375);1812let tube = Extrusion::new(annulus, 0.333);1813assert_eq!(tube.area(), 14.886437, "incorrect surface area");1814assert_eq!(tube.volume(), 1.9124937, "incorrect volume");18151816let polygon = crate::primitives::RegularPolygon::new(3.8, 7);1817let regular_prism = Extrusion::new(polygon, 1.25);1818assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area");1819assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume");1820}18211822#[test]1823fn triangle_math() {1824// Default triangle tests1825let mut default_triangle = Triangle3d::default();1826let reverse_default_triangle = Triangle3d::new(1827Vec3::new(0.5, -0.5, 0.0),1828Vec3::new(-0.5, -0.5, 0.0),1829Vec3::new(0.0, 0.5, 0.0),1830);1831assert_eq!(default_triangle.area(), 0.5, "incorrect area");1832assert_relative_eq!(1833default_triangle.perimeter(),18341.0 + 2.0 * ops::sqrt(1.25_f32),1835epsilon = 10e-91836);1837assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");1838assert!(1839!default_triangle.is_degenerate(),1840"incorrect degenerate check"1841);1842assert_eq!(1843default_triangle.centroid(),1844Vec3::new(0.0, -0.16666667, 0.0),1845"incorrect centroid"1846);1847assert_eq!(1848default_triangle.largest_side(),1849(Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0))1850);1851default_triangle.reverse();1852assert_eq!(1853default_triangle, reverse_default_triangle,1854"incorrect reverse"1855);1856assert_eq!(1857default_triangle.circumcenter(),1858Vec3::new(0.0, -0.125, 0.0),1859"incorrect circumcenter"1860);18611862// Custom triangle tests1863let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y);1864let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0));1865let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0));18661867assert_eq!(1868right_triangle.circumcenter(),1869Vec3::new(0.5, 0.5, 0.0),1870"incorrect circumcenter"1871);1872assert_eq!(1873obtuse_triangle.circumcenter(),1874Vec3::new(0.0, -4.95, 0.0),1875"incorrect circumcenter"1876);1877assert_eq!(1878acute_triangle.circumcenter(),1879Vec3::new(0.5, 2.475, 0.0),1880"incorrect circumcenter"1881);18821883assert!(acute_triangle.is_acute());1884assert!(!acute_triangle.is_obtuse());1885assert!(!obtuse_triangle.is_acute());1886assert!(obtuse_triangle.is_obtuse());18871888// Arbitrary triangle tests1889let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)];1890let triangle = Triangle3d::new(a, b, c);18911892assert!(!triangle.is_degenerate(), "incorrectly found degenerate");1893assert_eq!(triangle.area(), 3.0233467, "incorrect area");1894assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter");1895assert_eq!(1896triangle.circumcenter(),1897Vec3::new(-1., 1.75, 0.75),1898"incorrect circumcenter"1899);1900assert_eq!(1901triangle.normal(),1902Ok(Dir3::new_unchecked(Vec3::new(1903-0.04134491,1904-0.4134491,19050.909588041906))),1907"incorrect normal"1908);19091910// Degenerate triangle tests1911let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);1912assert!(1913zero_degenerate_triangle.is_degenerate(),1914"incorrect degenerate check"1915);1916assert_eq!(1917zero_degenerate_triangle.normal(),1918Err(InvalidDirectionError::Zero),1919"incorrect normal"1920);1921assert_eq!(1922zero_degenerate_triangle.largest_side(),1923(Vec3::ZERO, Vec3::ZERO),1924"incorrect largest side"1925);19261927let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);1928assert!(1929dup_degenerate_triangle.is_degenerate(),1930"incorrect degenerate check"1931);1932assert_eq!(1933dup_degenerate_triangle.normal(),1934Err(InvalidDirectionError::Zero),1935"incorrect normal"1936);1937assert_eq!(1938dup_degenerate_triangle.largest_side(),1939(Vec3::ZERO, Vec3::X),1940"incorrect largest side"1941);19421943let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);1944assert!(1945collinear_degenerate_triangle.is_degenerate(),1946"incorrect degenerate check"1947);1948assert_eq!(1949collinear_degenerate_triangle.normal(),1950Err(InvalidDirectionError::Zero),1951"incorrect normal"1952);1953assert_eq!(1954collinear_degenerate_triangle.largest_side(),1955(Vec3::NEG_X, Vec3::X),1956"incorrect largest side"1957);1958}1959}196019611962