Path: blob/main/crates/bevy_math/src/primitives/dim3.rs
9366 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]46pub const fn new(radius: f32) -> Self {47Self { radius }48}4950/// Get the diameter of the sphere51#[inline]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]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]79fn area(&self) -> f32 {804.0 * PI * self.radius.squared()81}8283/// Get the volume of the sphere84#[inline]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]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]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}161impl Measured2d for Plane3d {162#[inline]163fn area(&self) -> f32 {164self.half_size.element_product() * 4.0165}166167#[inline]168fn perimeter(&self) -> f32 {169self.half_size.element_sum() * 4.0170}171}172173/// An unbounded plane in 3D space. It forms a separating surface through the origin,174/// stretching infinitely far175#[derive(Clone, Copy, Debug, PartialEq)]176#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]177#[cfg_attr(178feature = "bevy_reflect",179derive(Reflect),180reflect(Debug, PartialEq, Default, Clone)181)]182#[cfg_attr(183all(feature = "serialize", feature = "bevy_reflect"),184reflect(Serialize, Deserialize)185)]186pub struct InfinitePlane3d {187/// The normal of the plane. The plane will be placed perpendicular to this direction188pub normal: Dir3,189}190191impl Primitive3d for InfinitePlane3d {}192193impl Default for InfinitePlane3d {194/// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction.195fn default() -> Self {196Self { normal: Dir3::Y }197}198}199200impl InfinitePlane3d {201/// Create a new `InfinitePlane3d` from a normal202///203/// # Panics204///205/// Panics if the given `normal` is zero (or very close to zero), or non-finite.206#[inline]207pub fn new<T: TryInto<Dir3>>(normal: T) -> Self208where209<T as TryInto<Dir3>>::Error: core::fmt::Debug,210{211Self {212normal: normal213.try_into()214.expect("normal must be nonzero and finite"),215}216}217218/// Create a new `InfinitePlane3d` based on three points and compute the geometric center219/// of those points.220///221/// The direction of the plane normal is determined by the winding order222/// of the triangular shape formed by the points.223///224/// # Panics225///226/// Panics if a valid normal can not be computed, for example when the points227/// are *collinear* and lie on the same line.228#[inline]229pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {230let normal = Dir3::new((b - a).cross(c - a)).expect(231"infinite plane must be defined by three finite points that don't lie on the same line",232);233let translation = (a + b + c) / 3.0;234235(Self { normal }, translation)236}237238/// Computes the shortest distance between a plane transformed with the given `isometry` and a239/// `point`. The result is a signed value; it's positive if the point lies in the half-space240/// that the plane's normal vector points towards.241#[inline]242pub fn signed_distance(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> f32 {243let isometry = isometry.into();244self.normal.dot(isometry.inverse() * point)245}246247/// Injects the `point` into this plane transformed with the given `isometry`.248///249/// This projects the point orthogonally along the shortest path onto the plane.250#[inline]251pub fn project_point(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> Vec3 {252point - self.normal * self.signed_distance(isometry, point)253}254255/// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given256/// `origin` to the XY-plane.257///258/// ## Guarantees259///260/// * the transformation is a [congruence] meaning it will preserve all distances and angles of261/// the transformed geometry262/// * uses the least rotation possible to transform the geometry263/// * if two geometries are transformed with the same isometry, then the relations between264/// them, like distances, are also preserved265/// * compared to projections, the transformation is lossless (up to floating point errors)266/// reversible267///268/// ## Non-Guarantees269///270/// * the rotation used is generally not unique271/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to272/// enforce some kind of alignment the user has to use an extra transformation ontop of this273/// one274///275/// See [`isometries_xy`] for example usescases.276///277/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)278/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`279#[inline]280pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d {281let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z);282let transformed_origin = rotation * origin;283Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation)284}285286/// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the287/// given `origin`.288///289/// ## Guarantees290///291/// * the transformation is a [congruence] meaning it will preserve all distances and angles of292/// the transformed geometry293/// * uses the least rotation possible to transform the geometry294/// * if two geometries are transformed with the same isometry, then the relations between295/// them, like distances, are also preserved296/// * compared to projections, the transformation is lossless (up to floating point errors)297/// reversible298///299/// ## Non-Guarantees300///301/// * the rotation used is generally not unique302/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to303/// enforce some kind of alignment the user has to use an extra transformation ontop of this304/// one305///306/// See [`isometries_xy`] for example usescases.307///308/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)309/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`310#[inline]311pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d {312self.isometry_into_xy(origin).inverse()313}314315/// Computes both [isometries] which transforms points from the plane in 3D space with the316/// given `origin` to the XY-plane and back.317///318/// [isometries]: `Isometry3d`319///320/// # Example321///322/// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The323/// workflow would usually look like this:324///325/// ```326/// # use bevy_math::{Vec3, Dir3};327/// # use bevy_math::primitives::InfinitePlane3d;328///329/// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z];330/// let center = (a + b + c) / 3.0;331///332/// let plane = InfinitePlane3d::new(Vec3::ONE);333///334/// let (to_xy, from_xy) = plane.isometries_xy(center);335///336/// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate());337///338/// // apply some algorithm to `triangle_2d`339///340/// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3);341/// ```342#[inline]343pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) {344let projection = self.isometry_into_xy(origin);345(projection, projection.inverse())346}347}348349/// An infinite line going through the origin along a direction in 3D space.350///351/// For a finite line: [`Segment3d`]352#[derive(Clone, Copy, Debug, PartialEq)]353#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]354#[cfg_attr(355feature = "bevy_reflect",356derive(Reflect),357reflect(Debug, PartialEq, Clone)358)]359#[cfg_attr(360all(feature = "serialize", feature = "bevy_reflect"),361reflect(Serialize, Deserialize)362)]363pub struct Line3d {364/// The direction of the line365pub direction: Dir3,366}367368impl Primitive3d for Line3d {}369370/// A line segment defined by two endpoints in 3D space.371#[derive(Clone, Copy, Debug, PartialEq)]372#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]373#[cfg_attr(374feature = "bevy_reflect",375derive(Reflect),376reflect(Debug, PartialEq, Clone)377)]378#[cfg_attr(379all(feature = "serialize", feature = "bevy_reflect"),380reflect(Serialize, Deserialize)381)]382#[doc(alias = "LineSegment3d")]383pub struct Segment3d {384/// The endpoints of the line segment.385pub vertices: [Vec3; 2],386}387388impl Primitive3d for Segment3d {}389390impl Default for Segment3d {391fn default() -> Self {392Self {393vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)],394}395}396}397398impl Segment3d {399/// Create a new `Segment3d` from its endpoints.400#[inline]401pub const fn new(point1: Vec3, point2: Vec3) -> Self {402Self {403vertices: [point1, point2],404}405}406407/// Create a new `Segment3d` centered at the origin with the given direction and length.408///409/// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`.410#[inline]411pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {412let endpoint = 0.5 * length * direction;413Self {414vertices: [-endpoint, endpoint],415}416}417418/// Create a new `Segment3d` centered at the origin from a vector representing419/// the direction and length of the line segment.420///421/// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`.422#[inline]423pub fn from_scaled_direction(scaled_direction: Vec3) -> Self {424let endpoint = 0.5 * scaled_direction;425Self {426vertices: [-endpoint, endpoint],427}428}429430/// Create a new `Segment3d` starting from the origin of the given `ray`,431/// going in the direction of the ray for the given `length`.432///433/// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`.434#[inline]435pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self {436Self {437vertices: [ray.origin, ray.get_point(length)],438}439}440441/// Get the position of the first endpoint of the line segment.442#[inline]443pub const fn point1(&self) -> Vec3 {444self.vertices[0]445}446447/// Get the position of the second endpoint of the line segment.448#[inline]449pub const fn point2(&self) -> Vec3 {450self.vertices[1]451}452453/// Compute the midpoint between the two endpoints of the line segment.454#[inline]455#[doc(alias = "midpoint")]456pub fn center(&self) -> Vec3 {457self.point1().midpoint(self.point2())458}459460/// Compute the length of the line segment.461#[inline]462pub fn length(&self) -> f32 {463self.point1().distance(self.point2())464}465466/// Compute the squared length of the line segment.467#[inline]468pub fn length_squared(&self) -> f32 {469self.point1().distance_squared(self.point2())470}471472/// Compute the normalized direction pointing from the first endpoint to the second endpoint.473///474/// For the non-panicking version, see [`Segment3d::try_direction`].475///476/// # Panics477///478/// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.479#[inline]480pub fn direction(&self) -> Dir3 {481self.try_direction().unwrap_or_else(|err| {482panic!("Failed to compute the direction of a line segment: {err}")483})484}485486/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.487///488/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,489/// for example when the endpoints are coincident, NaN, or infinite.490#[inline]491pub fn try_direction(&self) -> Result<Dir3, InvalidDirectionError> {492Dir3::new(self.scaled_direction())493}494495/// Compute the vector from the first endpoint to the second endpoint.496#[inline]497pub fn scaled_direction(&self) -> Vec3 {498self.point2() - self.point1()499}500501/// Compute the segment transformed by the given [`Isometry3d`].502#[inline]503pub fn transformed(&self, isometry: impl Into<Isometry3d>) -> Self {504let isometry: Isometry3d = isometry.into();505Self::new(506isometry.transform_point(self.point1()).into(),507isometry.transform_point(self.point2()).into(),508)509}510511/// Compute the segment translated by the given vector.512#[inline]513pub fn translated(&self, translation: Vec3) -> Segment3d {514Self::new(self.point1() + translation, self.point2() + translation)515}516517/// Compute the segment rotated around the origin by the given rotation.518#[inline]519pub fn rotated(&self, rotation: Quat) -> Segment3d {520Segment3d::new(rotation * self.point1(), rotation * self.point2())521}522523/// Compute the segment rotated around the given point by the given rotation.524#[inline]525pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {526// We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back527let offset = self.translated(-point);528let rotated = offset.rotated(rotation);529rotated.translated(point)530}531532/// Compute the segment rotated around its own center.533#[inline]534pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {535self.rotated_around(rotation, self.center())536}537538/// Compute the segment with its center at the origin, keeping the same direction and length.539#[inline]540pub fn centered(&self) -> Segment3d {541let center = self.center();542self.translated(-center)543}544545/// Compute the segment with a new length, keeping the same direction and center.546#[inline]547pub fn resized(&self, length: f32) -> Segment3d {548let offset_from_origin = self.center();549let centered = self.translated(-offset_from_origin);550let ratio = length / self.length();551let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);552segment.translated(offset_from_origin)553}554555/// Reverses the direction of the line segment by swapping the endpoints.556#[inline]557pub fn reverse(&mut self) {558let [point1, point2] = &mut self.vertices;559core::mem::swap(point1, point2);560}561562/// Returns the line segment with its direction reversed by swapping the endpoints.563#[inline]564#[must_use]565pub fn reversed(mut self) -> Self {566self.reverse();567self568}569570/// Returns the point on the [`Segment3d`] that is closest to the specified `point`.571#[inline]572pub fn closest_point(&self, point: Vec3) -> Vec3 {573// `point`574// x575// ^|576// / |577//`offset`/ |578// / | `segment_vector`579// x----.-------------->x580// 0 t 1581let segment_vector = self.vertices[1] - self.vertices[0];582let offset = point - self.vertices[0];583// The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment.584let projection_scaled = segment_vector.dot(offset);585586// `point` is too far "left" in the picture587if projection_scaled <= 0.0 {588return self.vertices[0];589}590591let length_squared = segment_vector.length_squared();592// `point` is too far "right" in the picture593if projection_scaled >= length_squared {594return self.vertices[1];595}596597// Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line.598let t = projection_scaled / length_squared;599self.vertices[0] + t * segment_vector600}601}602603impl From<[Vec3; 2]> for Segment3d {604#[inline]605fn from(vertices: [Vec3; 2]) -> Self {606Self { vertices }607}608}609610impl From<(Vec3, Vec3)> for Segment3d {611#[inline]612fn from((point1, point2): (Vec3, Vec3)) -> Self {613Self::new(point1, point2)614}615}616617/// A series of connected line segments in 3D space.618#[cfg(feature = "alloc")]619#[derive(Clone, Debug, PartialEq)]620#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]621#[cfg_attr(622feature = "bevy_reflect",623derive(Reflect),624reflect(Debug, PartialEq, Clone)625)]626#[cfg_attr(627all(feature = "serialize", feature = "bevy_reflect"),628reflect(Serialize, Deserialize)629)]630pub struct Polyline3d {631/// The vertices of the polyline632pub vertices: Vec<Vec3>,633}634635#[cfg(feature = "alloc")]636impl Primitive3d for Polyline3d {}637638#[cfg(feature = "alloc")]639impl FromIterator<Vec3> for Polyline3d {640fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {641Self {642vertices: iter.into_iter().collect(),643}644}645}646647#[cfg(feature = "alloc")]648impl Default for Polyline3d {649fn default() -> Self {650Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)])651}652}653654#[cfg(feature = "alloc")]655impl Polyline3d {656/// Create a new `Polyline3d` from its vertices657pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {658Self::from_iter(vertices)659}660661/// Create a new `Polyline3d` from two endpoints with subdivision points.662/// `subdivisions = 0` creates a simple line with just start and end points.663/// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc.664pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self {665let total_vertices = subdivisions + 2;666let mut vertices = Vec::with_capacity(total_vertices);667668let step = (end - start) / (subdivisions + 1) as f32;669for i in 0..total_vertices {670vertices.push(start + step * i as f32);671}672673Self { vertices }674}675}676677/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not678/// required to be the same.679#[derive(Clone, Copy, Debug, PartialEq)]680#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]681#[cfg_attr(682feature = "bevy_reflect",683derive(Reflect),684reflect(Debug, PartialEq, Default, Clone)685)]686#[cfg_attr(687all(feature = "serialize", feature = "bevy_reflect"),688reflect(Serialize, Deserialize)689)]690pub struct Cuboid {691/// Half of the width, height and depth of the cuboid692pub half_size: Vec3,693}694695impl Primitive3d for Cuboid {}696697impl Default for Cuboid {698/// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`.699fn default() -> Self {700Self {701half_size: Vec3::splat(0.5),702}703}704}705706impl Cuboid {707/// Create a new `Cuboid` from a full x, y, and z length708#[inline]709pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {710Self::from_size(Vec3::new(x_length, y_length, z_length))711}712713/// Create a new `Cuboid` from a given full size714#[inline]715pub const fn from_size(size: Vec3) -> Self {716Self {717half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0),718}719}720721/// Create a new `Cuboid` from two corner points722#[inline]723pub fn from_corners(point1: Vec3, point2: Vec3) -> Self {724Self {725half_size: (point2 - point1).abs() / 2.0,726}727}728729/// Create a `Cuboid` from a single length.730/// The resulting `Cuboid` will be the same size in every direction.731#[inline]732pub const fn from_length(length: f32) -> Self {733Self {734half_size: Vec3::splat(length / 2.0),735}736}737738/// Get the size of the cuboid739#[inline]740pub fn size(&self) -> Vec3 {7412.0 * self.half_size742}743744/// Finds the point on the cuboid that is closest to the given `point`.745///746/// If the point is outside the cuboid, the returned point will be on the surface of the cuboid.747/// Otherwise, it will be inside the cuboid and returned as is.748#[inline]749pub fn closest_point(&self, point: Vec3) -> Vec3 {750// Clamp point coordinates to the cuboid751point.clamp(-self.half_size, self.half_size)752}753}754755impl Measured3d for Cuboid {756/// Get the surface area of the cuboid757#[inline]758fn area(&self) -> f32 {7598.0 * (self.half_size.x * self.half_size.y760+ self.half_size.y * self.half_size.z761+ self.half_size.x * self.half_size.z)762}763764/// Get the volume of the cuboid765#[inline]766fn volume(&self) -> f32 {7678.0 * self.half_size.x * self.half_size.y * self.half_size.z768}769}770771/// A cylinder primitive centered on the origin772#[derive(Clone, Copy, Debug, PartialEq)]773#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]774#[cfg_attr(775feature = "bevy_reflect",776derive(Reflect),777reflect(Debug, PartialEq, Default, Clone)778)]779#[cfg_attr(780all(feature = "serialize", feature = "bevy_reflect"),781reflect(Serialize, Deserialize)782)]783pub struct Cylinder {784/// The radius of the cylinder785pub radius: f32,786/// The half height of the cylinder787pub half_height: f32,788}789790impl Primitive3d for Cylinder {}791792impl Default for Cylinder {793/// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`.794fn default() -> Self {795Self {796radius: 0.5,797half_height: 0.5,798}799}800}801802impl Cylinder {803/// Create a new `Cylinder` from a radius and full height804#[inline]805pub const fn new(radius: f32, height: f32) -> Self {806Self {807radius,808half_height: height / 2.0,809}810}811812/// Get the base of the cylinder as a [`Circle`]813#[inline]814pub const fn base(&self) -> Circle {815Circle {816radius: self.radius,817}818}819820/// Get the surface area of the side of the cylinder,821/// also known as the lateral area822#[inline]823#[doc(alias = "side_area")]824pub const fn lateral_area(&self) -> f32 {8254.0 * PI * self.radius * self.half_height826}827828/// Get the surface area of one base of the cylinder829#[inline]830pub fn base_area(&self) -> f32 {831PI * self.radius.squared()832}833}834835impl Measured3d for Cylinder {836/// Get the total surface area of the cylinder837#[inline]838fn area(&self) -> f32 {8392.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)840}841842/// Get the volume of the cylinder843#[inline]844fn volume(&self) -> f32 {845self.base_area() * 2.0 * self.half_height846}847}848849/// A 3D capsule primitive centered on the origin850/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line851#[derive(Clone, Copy, Debug, PartialEq)]852#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]853#[cfg_attr(854feature = "bevy_reflect",855derive(Reflect),856reflect(Debug, PartialEq, Default, Clone)857)]858#[cfg_attr(859all(feature = "serialize", feature = "bevy_reflect"),860reflect(Serialize, Deserialize)861)]862pub struct Capsule3d {863/// The radius of the capsule864pub radius: f32,865/// Half the height of the capsule, excluding the hemispheres866pub half_length: f32,867}868869impl Primitive3d for Capsule3d {}870871impl Default for Capsule3d {872/// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`.873/// The total height is `2.0`.874fn default() -> Self {875Self {876radius: 0.5,877half_length: 0.5,878}879}880}881882impl Capsule3d {883/// Create a new `Capsule3d` from a radius and length884pub const fn new(radius: f32, length: f32) -> Self {885Self {886radius,887half_length: length / 2.0,888}889}890891/// Get the part connecting the hemispherical ends892/// of the capsule as a [`Cylinder`]893#[inline]894pub const fn to_cylinder(&self) -> Cylinder {895Cylinder {896radius: self.radius,897half_height: self.half_length,898}899}900}901902impl Measured3d for Capsule3d {903/// Get the surface area of the capsule904#[inline]905fn area(&self) -> f32 {906// Modified version of 2pi * r * (2r + h)9074.0 * PI * self.radius * (self.radius + self.half_length)908}909910/// Get the volume of the capsule911#[inline]912fn volume(&self) -> f32 {913// Modified version of pi * r^2 * (4/3 * r + a)914let diameter = self.radius * 2.0;915PI * self.radius * diameter * (diameter / 3.0 + self.half_length)916}917}918919/// A cone primitive centered on the midpoint between the tip of the cone and the center of its base.920///921/// The cone is oriented with its tip pointing towards the Y axis.922#[derive(Clone, Copy, Debug, PartialEq)]923#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]924#[cfg_attr(925feature = "bevy_reflect",926derive(Reflect),927reflect(Debug, PartialEq, Default, Clone)928)]929#[cfg_attr(930all(feature = "serialize", feature = "bevy_reflect"),931reflect(Serialize, Deserialize)932)]933pub struct Cone {934/// The radius of the base935pub radius: f32,936/// The height of the cone937pub height: f32,938}939940impl Primitive3d for Cone {}941942impl Default for Cone {943/// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`.944fn default() -> Self {945Self {946radius: 0.5,947height: 1.0,948}949}950}951952impl Cone {953/// Create a new [`Cone`] from a radius and height.954pub const fn new(radius: f32, height: f32) -> Self {955Self { radius, height }956}957/// Get the base of the cone as a [`Circle`]958#[inline]959pub const fn base(&self) -> Circle {960Circle {961radius: self.radius,962}963}964965/// Get the slant height of the cone, the length of the line segment966/// connecting a point on the base to the apex967#[inline]968#[doc(alias = "side_length")]969pub fn slant_height(&self) -> f32 {970ops::hypot(self.radius, self.height)971}972973/// Get the surface area of the side of the cone,974/// also known as the lateral area975#[inline]976#[doc(alias = "side_area")]977pub fn lateral_area(&self) -> f32 {978PI * self.radius * self.slant_height()979}980981/// Get the surface area of the base of the cone982#[inline]983pub fn base_area(&self) -> f32 {984PI * self.radius.squared()985}986}987988impl Measured3d for Cone {989/// Get the total surface area of the cone990#[inline]991fn area(&self) -> f32 {992self.base_area() + self.lateral_area()993}994995/// Get the volume of the cone996#[inline]997fn volume(&self) -> f32 {998(self.base_area() * self.height) / 3.0999}1000}10011002/// A conical frustum primitive.1003/// A conical frustum can be created1004/// by slicing off a section of a cone.1005#[derive(Clone, Copy, Debug, PartialEq)]1006#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1007#[cfg_attr(1008feature = "bevy_reflect",1009derive(Reflect),1010reflect(Debug, PartialEq, Default, Clone)1011)]1012#[cfg_attr(1013all(feature = "serialize", feature = "bevy_reflect"),1014reflect(Serialize, Deserialize)1015)]1016pub struct ConicalFrustum {1017/// The radius of the top of the frustum1018pub radius_top: f32,1019/// The radius of the base of the frustum1020pub radius_bottom: f32,1021/// The height of the frustum1022pub height: f32,1023}10241025impl Primitive3d for ConicalFrustum {}10261027impl Default for ConicalFrustum {1028/// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`.1029fn default() -> Self {1030Self {1031radius_top: 0.25,1032radius_bottom: 0.5,1033height: 0.5,1034}1035}1036}10371038impl ConicalFrustum {1039/// Get the bottom base of the conical frustum as a [`Circle`]1040#[inline]1041pub const fn bottom_base(&self) -> Circle {1042Circle {1043radius: self.radius_bottom,1044}1045}10461047/// Get the top base of the conical frustum as a [`Circle`]1048#[inline]1049pub const fn top_base(&self) -> Circle {1050Circle {1051radius: self.radius_top,1052}1053}10541055/// Get the slant height of the conical frustum, the length of the line segment1056/// connecting a point on the base to the closest point on the top1057#[inline]1058#[doc(alias = "side_length")]1059pub fn slant_height(&self) -> f32 {1060ops::hypot(self.radius_bottom - self.radius_top, self.height)1061}10621063/// Get the surface area of the side of the conical frustum,1064/// also known as the lateral area1065#[inline]1066#[doc(alias = "side_area")]1067pub fn lateral_area(&self) -> f32 {1068PI * (self.radius_bottom + self.radius_top) * self.slant_height()1069}10701071/// Get the surface area of the bottom base of the conical frustum1072#[inline]1073pub fn bottom_base_area(&self) -> f32 {1074PI * self.radius_bottom.squared()1075}10761077/// Get the surface area of the top base of the conical frustum1078#[inline]1079pub fn top_base_area(&self) -> f32 {1080PI * self.radius_top.squared()1081}1082}10831084impl Measured3d for ConicalFrustum {1085#[inline]1086fn volume(&self) -> f32 {1087FRAC_PI_31088* self.height1089* (self.radius_bottom * self.radius_bottom1090+ self.radius_top * self.radius_top1091+ self.radius_top * self.radius_bottom)1092}1093#[inline]1094fn area(&self) -> f32 {1095self.bottom_base_area() + self.top_base_area() + self.lateral_area()1096}1097}10981099/// The type of torus determined by the minor and major radii1100#[derive(Clone, Copy, Debug, PartialEq, Eq)]1101pub enum TorusKind {1102/// A torus that has a ring.1103/// The major radius is greater than the minor radius1104Ring,1105/// A torus that has no hole but also doesn't intersect itself.1106/// The major radius is equal to the minor radius1107Horn,1108/// A self-intersecting torus.1109/// The major radius is less than the minor radius1110Spindle,1111/// A torus with non-geometric properties like1112/// a minor or major radius that is non-positive,1113/// infinite, or `NaN`1114Invalid,1115}11161117/// A torus primitive, often representing a ring or donut shape1118/// The set of points some distance from a circle centered at the origin1119#[derive(Clone, Copy, Debug, PartialEq)]1120#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1121#[cfg_attr(1122feature = "bevy_reflect",1123derive(Reflect),1124reflect(Debug, PartialEq, Default, Clone)1125)]1126#[cfg_attr(1127all(feature = "serialize", feature = "bevy_reflect"),1128reflect(Serialize, Deserialize)1129)]1130pub struct Torus {1131/// The radius of the tube of the torus1132#[doc(1133alias = "ring_radius",1134alias = "tube_radius",1135alias = "cross_section_radius"1136)]1137pub minor_radius: f32,1138/// The distance from the center of the torus to the center of the tube1139#[doc(alias = "radius_of_revolution")]1140pub major_radius: f32,1141}11421143impl Primitive3d for Torus {}11441145impl Default for Torus {1146/// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`.1147fn default() -> Self {1148Self {1149minor_radius: 0.25,1150major_radius: 0.75,1151}1152}1153}11541155impl Torus {1156/// Create a new `Torus` from an inner and outer radius.1157///1158/// The inner radius is the radius of the hole, and the outer radius1159/// is the radius of the entire object1160#[inline]1161pub const fn new(inner_radius: f32, outer_radius: f32) -> Self {1162let minor_radius = (outer_radius - inner_radius) / 2.0;1163let major_radius = outer_radius - minor_radius;11641165Self {1166minor_radius,1167major_radius,1168}1169}11701171/// Get the inner radius of the torus.1172/// For a ring torus, this corresponds to the radius of the hole,1173/// or `major_radius - minor_radius`1174#[inline]1175pub const fn inner_radius(&self) -> f32 {1176self.major_radius - self.minor_radius1177}11781179/// Get the outer radius of the torus.1180/// This corresponds to the overall radius of the entire object,1181/// or `major_radius + minor_radius`1182#[inline]1183pub const fn outer_radius(&self) -> f32 {1184self.major_radius + self.minor_radius1185}11861187/// Get the [`TorusKind`] determined by the minor and major radii.1188///1189/// The torus can either be a *ring torus* that has a hole,1190/// a *horn torus* that doesn't have a hole but also isn't self-intersecting,1191/// or a *spindle torus* that is self-intersecting.1192///1193/// If the minor or major radius is non-positive, infinite, or `NaN`,1194/// [`TorusKind::Invalid`] is returned1195#[inline]1196pub fn kind(&self) -> TorusKind {1197// Invalid if minor or major radius is non-positive, infinite, or NaN1198if self.minor_radius <= 0.01199|| !self.minor_radius.is_finite()1200|| self.major_radius <= 0.01201|| !self.major_radius.is_finite()1202{1203return TorusKind::Invalid;1204}12051206match self.major_radius.partial_cmp(&self.minor_radius).unwrap() {1207core::cmp::Ordering::Greater => TorusKind::Ring,1208core::cmp::Ordering::Equal => TorusKind::Horn,1209core::cmp::Ordering::Less => TorusKind::Spindle,1210}1211}1212}12131214impl Measured3d for Torus {1215/// Get the surface area of the torus. Note that this only produces1216/// the expected result when the torus has a ring and isn't self-intersecting1217#[inline]1218fn area(&self) -> f32 {12194.0 * PI.squared() * self.major_radius * self.minor_radius1220}12211222/// Get the volume of the torus. Note that this only produces1223/// the expected result when the torus has a ring and isn't self-intersecting1224#[inline]1225fn volume(&self) -> f32 {12262.0 * PI.squared() * self.major_radius * self.minor_radius.squared()1227}1228}12291230/// A 3D triangle primitive.1231#[derive(Clone, Copy, Debug, PartialEq)]1232#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1233#[cfg_attr(1234feature = "bevy_reflect",1235derive(Reflect),1236reflect(Debug, PartialEq, Default, Clone)1237)]1238#[cfg_attr(1239all(feature = "serialize", feature = "bevy_reflect"),1240reflect(Serialize, Deserialize)1241)]1242pub struct Triangle3d {1243/// The vertices of the triangle.1244pub vertices: [Vec3; 3],1245}12461247impl Primitive3d for Triangle3d {}12481249impl Default for Triangle3d {1250/// 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]`.1251fn default() -> Self {1252Self {1253vertices: [1254Vec3::new(0.0, 0.5, 0.0),1255Vec3::new(-0.5, -0.5, 0.0),1256Vec3::new(0.5, -0.5, 0.0),1257],1258}1259}1260}12611262impl Triangle3d {1263/// Create a new [`Triangle3d`] from points `a`, `b`, and `c`.1264#[inline]1265pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {1266Self {1267vertices: [a, b, c],1268}1269}12701271/// Get the normal of the triangle in the direction of the right-hand rule, assuming1272/// the vertices are ordered in a counter-clockwise direction.1273///1274/// The normal is computed as the cross product of the vectors `ab` and `ac`.1275///1276/// # Errors1277///1278/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length1279/// of the given vector is zero (or very close to zero), infinite, or `NaN`.1280#[inline]1281pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {1282let [a, b, c] = self.vertices;1283let ab = b - a;1284let ac = c - a;1285Dir3::new(ab.cross(ac))1286}12871288/// Checks if the triangle is degenerate, meaning it has zero area.1289///1290/// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`.1291/// This indicates that the three vertices are collinear or nearly collinear.1292#[inline]1293pub fn is_degenerate(&self) -> bool {1294let [a, b, c] = self.vertices;1295let ab = b - a;1296let ac = c - a;1297ab.cross(ac).length() < 10e-71298}12991300/// Checks if the triangle is acute, meaning all angles are less than 90 degrees1301#[inline]1302pub fn is_acute(&self) -> bool {1303let [a, b, c] = self.vertices;1304let ab = b - a;1305let bc = c - b;1306let ca = a - c;13071308// a^2 + b^2 < c^2 for an acute triangle1309let side_lengths = [1310ab.length_squared(),1311bc.length_squared(),1312ca.length_squared(),1313];1314let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];1315let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);1316sum - max > max1317}13181319/// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees1320#[inline]1321pub fn is_obtuse(&self) -> bool {1322let [a, b, c] = self.vertices;1323let ab = b - a;1324let bc = c - b;1325let ca = a - c;13261327// a^2 + b^2 > c^2 for an obtuse triangle1328let side_lengths = [1329ab.length_squared(),1330bc.length_squared(),1331ca.length_squared(),1332];1333let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];1334let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);1335sum - max < max1336}13371338/// Reverse the triangle by swapping the first and last vertices.1339#[inline]1340pub fn reverse(&mut self) {1341self.vertices.swap(0, 2);1342}13431344/// This triangle but reversed.1345#[inline]1346#[must_use]1347pub fn reversed(mut self) -> Triangle3d {1348self.reverse();1349self1350}13511352/// Get the centroid of the triangle.1353///1354/// This function finds the geometric center of the triangle by averaging the vertices:1355/// `centroid = (a + b + c) / 3`.1356#[doc(alias("center", "barycenter", "baricenter"))]1357#[inline]1358pub fn centroid(&self) -> Vec3 {1359(self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.01360}13611362/// Get the largest side of the triangle.1363///1364/// Returns the two points that form the largest side of the triangle.1365#[inline]1366pub fn largest_side(&self) -> (Vec3, Vec3) {1367let [a, b, c] = self.vertices;1368let ab = b - a;1369let bc = c - b;1370let ca = a - c;13711372let mut largest_side_points = (a, b);1373let mut largest_side_length = ab.length_squared();13741375let bc_length = bc.length_squared();1376if bc_length > largest_side_length {1377largest_side_points = (b, c);1378largest_side_length = bc_length;1379}13801381let ca_length = ca.length_squared();1382if ca_length > largest_side_length {1383largest_side_points = (a, c);1384}13851386largest_side_points1387}13881389/// Get the circumcenter of the triangle.1390#[inline]1391pub fn circumcenter(&self) -> Vec3 {1392if self.is_degenerate() {1393// If the triangle is degenerate, the circumcenter is the midpoint of the largest side.1394let (p1, p2) = self.largest_side();1395return (p1 + p2) / 2.0;1396}13971398let [a, b, c] = self.vertices;1399let ab = b - a;1400let ac = c - a;1401let n = ab.cross(ac);14021403// Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d1404a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))1405/ (2.0 * n.length_squared()))1406}1407}14081409impl Measured2d for Triangle3d {1410/// Get the area of the triangle.1411#[inline]1412fn area(&self) -> f32 {1413let [a, b, c] = self.vertices;1414let ab = b - a;1415let ac = c - a;1416ab.cross(ac).length() / 2.01417}14181419/// Get the perimeter of the triangle.1420#[inline]1421fn perimeter(&self) -> f32 {1422let [a, b, c] = self.vertices;1423a.distance(b) + b.distance(c) + c.distance(a)1424}1425}14261427/// A tetrahedron primitive.1428#[derive(Clone, Copy, Debug, PartialEq)]1429#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1430#[cfg_attr(1431feature = "bevy_reflect",1432derive(Reflect),1433reflect(Debug, PartialEq, Default, Clone)1434)]1435#[cfg_attr(1436all(feature = "serialize", feature = "bevy_reflect"),1437reflect(Serialize, Deserialize)1438)]1439pub struct Tetrahedron {1440/// The vertices of the tetrahedron.1441pub vertices: [Vec3; 4],1442}14431444impl Primitive3d for Tetrahedron {}14451446impl Default for Tetrahedron {1447/// Returns the default [`Tetrahedron`] with the vertices1448/// `[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]`.1449fn default() -> Self {1450Self {1451vertices: [1452Vec3::new(0.5, 0.5, 0.5),1453Vec3::new(-0.5, 0.5, -0.5),1454Vec3::new(-0.5, -0.5, 0.5),1455Vec3::new(0.5, -0.5, -0.5),1456],1457}1458}1459}14601461impl Tetrahedron {1462/// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`.1463#[inline]1464pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self {1465Self {1466vertices: [a, b, c, d],1467}1468}14691470/// Get the signed volume of the tetrahedron.1471///1472/// If it's negative, the normal vector of the face defined by1473/// the first three points using the right-hand rule points1474/// away from the fourth vertex.1475#[inline]1476pub fn signed_volume(&self) -> f32 {1477let [a, b, c, d] = self.vertices;1478let ab = b - a;1479let ac = c - a;1480let ad = d - a;1481Mat3::from_cols(ab, ac, ad).determinant() / 6.01482}14831484/// Get the centroid of the tetrahedron.1485///1486/// This function finds the geometric center of the tetrahedron1487/// by averaging the vertices: `centroid = (a + b + c + d) / 4`.1488#[doc(alias("center", "barycenter", "baricenter"))]1489#[inline]1490pub fn centroid(&self) -> Vec3 {1491(self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.01492}14931494/// Get the triangles that form the faces of this tetrahedron.1495///1496/// Note that the orientations of the faces are determined by that of the tetrahedron; if the1497/// signed volume of this tetrahedron is positive, then the triangles' normals will point1498/// outward, and if the signed volume is negative they will point inward.1499#[inline]1500pub fn faces(&self) -> [Triangle3d; 4] {1501let [a, b, c, d] = self.vertices;1502[1503Triangle3d::new(b, c, d),1504Triangle3d::new(a, c, d).reversed(),1505Triangle3d::new(a, b, d),1506Triangle3d::new(a, b, c).reversed(),1507]1508}1509}15101511impl Measured3d for Tetrahedron {1512/// Get the surface area of the tetrahedron.1513#[inline]1514fn area(&self) -> f32 {1515let [a, b, c, d] = self.vertices;1516let ab = b - a;1517let ac = c - a;1518let ad = d - a;1519let bc = c - b;1520let bd = d - b;1521(ab.cross(ac).length()1522+ ab.cross(ad).length()1523+ ac.cross(ad).length()1524+ bc.cross(bd).length())1525/ 2.01526}15271528/// Get the volume of the tetrahedron.1529#[inline]1530fn volume(&self) -> f32 {1531ops::abs(self.signed_volume())1532}1533}15341535/// A 3D shape representing an extruded 2D `base_shape`.1536///1537/// Extruding a shape effectively "thickens" a 2D shapes,1538/// creating a shape with the same cross-section over the entire depth.1539///1540/// The resulting volumes are prisms.1541/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder.1542#[doc(alias = "Prism")]1543#[derive(Clone, Copy, Debug, PartialEq)]1544#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1545pub struct Extrusion<T: Primitive2d> {1546/// The base shape of the extrusion1547pub base_shape: T,1548/// Half of the depth of the extrusion1549pub half_depth: f32,1550}15511552impl<T: Primitive2d> Primitive3d for Extrusion<T> {}15531554impl<T: Primitive2d> Extrusion<T> {1555/// Create a new `Extrusion<T>` from a given `base_shape` and `depth`1556pub fn new(base_shape: T, depth: f32) -> Self {1557Self {1558base_shape,1559half_depth: depth / 2.,1560}1561}1562}15631564impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {1565/// Get the surface area of the extrusion1566fn area(&self) -> f32 {15672. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())1568}15691570/// Get the volume of the extrusion1571fn volume(&self) -> f32 {15722. * self.base_shape.area() * self.half_depth1573}1574}15751576#[cfg(test)]1577mod tests {1578// Reference values were computed by hand and/or with external tools15791580use super::*;1581use crate::{InvalidDirectionError, Quat};1582use approx::assert_relative_eq;15831584#[test]1585fn direction_creation() {1586assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));1587assert_eq!(1588Dir3::new(Vec3::new(0.0, 0.0, 0.0)),1589Err(InvalidDirectionError::Zero)1590);1591assert_eq!(1592Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),1593Err(InvalidDirectionError::Infinite)1594);1595assert_eq!(1596Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),1597Err(InvalidDirectionError::Infinite)1598);1599assert_eq!(1600Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),1601Err(InvalidDirectionError::NaN)1602);1603assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));16041605// Test rotation1606assert!(1607(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X)1608.abs_diff_eq(Vec3::Y, 10e-6)1609);1610}16111612#[test]1613fn cuboid_closest_point() {1614let cuboid = Cuboid::new(2.0, 2.0, 2.0);1615assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X);1616assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE);1617assert_eq!(1618cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)),1619Vec3::new(0.25, 0.1, 0.3)1620);1621}16221623#[test]1624fn sphere_closest_point() {1625let sphere = Sphere { radius: 1.0 };1626assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X);1627assert_eq!(1628sphere.closest_point(Vec3::NEG_ONE * 10.0),1629Vec3::NEG_ONE.normalize()1630);1631assert_eq!(1632sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),1633Vec3::new(0.25, 0.1, 0.3)1634);1635}16361637#[test]1638fn segment_closest_point() {1639assert_eq!(1640Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0))1641.closest_point(Vec3::new(1.0, 6.0, -2.0)),1642Vec3::new(1.0, 0.0, 0.0)1643);16441645let segments = [1646Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)),1647Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),1648Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)),1649Segment3d::new(1650Vec3::new(1.0, 0.0, 0.0),1651Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0),1652),1653];1654let points = [1655Vec3::new(0.0, 0.0, 0.0),1656Vec3::new(1.0, 0.0, 0.0),1657Vec3::new(-1.0, 1.0, 2.0),1658Vec3::new(1.0, 1.0, 1.0),1659Vec3::new(-1.0, 0.0, 0.0),1660Vec3::new(5.0, -1.0, 0.5),1661Vec3::new(1.0, f32::EPSILON, 0.0),1662];16631664for point in points.iter() {1665for segment in segments.iter() {1666let closest = segment.closest_point(*point);1667assert!(1668point.distance_squared(closest) <= point.distance_squared(segment.point1()),1669"Closest point must always be at least as close as either vertex."1670);1671assert!(1672point.distance_squared(closest) <= point.distance_squared(segment.point2()),1673"Closest point must always be at least as close as either vertex."1674);1675assert!(1676point.distance_squared(closest) <= point.distance_squared(segment.center()),1677"Closest point must always be at least as close as the center."1678);1679let closest_to_closest = segment.closest_point(closest);1680// Closest point must already be on the segment1681assert_relative_eq!(closest_to_closest, closest);1682}1683}1684}16851686#[test]1687fn sphere_math() {1688let sphere = Sphere { radius: 4.0 };1689assert_eq!(sphere.diameter(), 8.0, "incorrect diameter");1690assert_eq!(sphere.area(), 201.06193, "incorrect area");1691assert_eq!(sphere.volume(), 268.08257, "incorrect volume");1692}16931694#[test]1695fn plane_from_points() {1696let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);1697assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");1698assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size");1699assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation");1700}17011702#[test]1703fn infinite_plane_math() {1704let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);1705assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");1706assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation");17071708let point_in_plane = Vec3::X + Vec3::Z;1709assert_eq!(1710plane.signed_distance(origin, point_in_plane),17110.0,1712"incorrect distance"1713);1714assert_eq!(1715plane.project_point(origin, point_in_plane),1716point_in_plane,1717"incorrect point"1718);17191720let point_outside = Vec3::Y;1721assert_eq!(1722plane.signed_distance(origin, point_outside),1723-1.0,1724"incorrect distance"1725);1726assert_eq!(1727plane.project_point(origin, point_outside),1728Vec3::ZERO,1729"incorrect point"1730);17311732let point_outside = Vec3::NEG_Y;1733assert_eq!(1734plane.signed_distance(origin, point_outside),17351.0,1736"incorrect distance"1737);1738assert_eq!(1739plane.project_point(origin, point_outside),1740Vec3::ZERO,1741"incorrect point"1742);17431744let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5;1745let (proj, inj) = plane.isometries_xy(origin);17461747let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO];1748assert_eq!(area_f(triangle), 0.5, "incorrect area");17491750let triangle_proj = triangle.map(|vec3| proj * vec3);1751assert_relative_eq!(area_f(triangle_proj), 0.5);17521753let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3);1754assert_relative_eq!(area_f(triangle_proj_inj), 0.5);1755}17561757#[test]1758fn cuboid_math() {1759let cuboid = Cuboid::new(3.0, 7.0, 2.0);1760assert_eq!(1761cuboid,1762Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)),1763"incorrect dimensions when created from corners"1764);1765assert_eq!(cuboid.area(), 82.0, "incorrect area");1766assert_eq!(cuboid.volume(), 42.0, "incorrect volume");1767}17681769#[test]1770fn cylinder_math() {1771let cylinder = Cylinder::new(2.0, 9.0);1772assert_eq!(1773cylinder.base(),1774Circle { radius: 2.0 },1775"base produces incorrect circle"1776);1777assert_eq!(1778cylinder.lateral_area(),1779113.097336,1780"incorrect lateral area"1781);1782assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area");1783assert_relative_eq!(cylinder.area(), 138.23007);1784assert_eq!(cylinder.volume(), 113.097336, "incorrect volume");1785}17861787#[test]1788fn capsule_math() {1789let capsule = Capsule3d::new(2.0, 9.0);1790assert_eq!(1791capsule.to_cylinder(),1792Cylinder::new(2.0, 9.0),1793"cylinder wasn't created correctly from a capsule"1794);1795assert_eq!(capsule.area(), 163.36282, "incorrect area");1796assert_relative_eq!(capsule.volume(), 146.60765);1797}17981799#[test]1800fn cone_math() {1801let cone = Cone {1802radius: 2.0,1803height: 9.0,1804};1805assert_eq!(1806cone.base(),1807Circle { radius: 2.0 },1808"base produces incorrect circle"1809);1810assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height");1811assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area");1812assert_eq!(cone.base_area(), 12.566371, "incorrect base area");1813assert_relative_eq!(cone.area(), 70.49447);1814assert_eq!(cone.volume(), 37.699111, "incorrect volume");1815}18161817#[test]1818fn conical_frustum_math() {1819let frustum = ConicalFrustum {1820height: 9.0,1821radius_top: 1.0,1822radius_bottom: 2.0,1823};1824assert_eq!(1825frustum.bottom_base(),1826Circle { radius: 2.0 },1827"bottom base produces incorrect circle"1828);1829assert_eq!(1830frustum.top_base(),1831Circle { radius: 1.0 },1832"top base produces incorrect circle"1833);1834assert_eq!(frustum.slant_height(), 9.055386, "incorrect slant height");1835assert_eq!(frustum.lateral_area(), 85.345, "incorrect lateral area");1836assert_eq!(1837frustum.bottom_base_area(),183812.566371,1839"incorrect bottom base area"1840);1841assert_eq!(frustum.top_base_area(), PI, "incorrect top base area");1842assert_eq!(frustum.area(), 101.05296, "incorrect surface area");1843assert_eq!(frustum.volume(), 65.97345, "incorrect volume");1844}18451846#[test]1847fn torus_math() {1848let torus = Torus {1849minor_radius: 0.3,1850major_radius: 2.8,1851};1852assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius");1853assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius");1854assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind");1855assert_eq!(1856Torus::new(0.0, 1.0).kind(),1857TorusKind::Horn,1858"incorrect torus kind"1859);1860assert_eq!(1861Torus::new(-0.5, 1.0).kind(),1862TorusKind::Spindle,1863"incorrect torus kind"1864);1865assert_eq!(1866Torus::new(1.5, 1.0).kind(),1867TorusKind::Invalid,1868"torus should be invalid"1869);1870assert_relative_eq!(torus.area(), 33.16187);1871assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001);1872}18731874#[test]1875fn tetrahedron_math() {1876let tetrahedron = Tetrahedron {1877vertices: [1878Vec3::new(0.3, 1.0, 1.7),1879Vec3::new(-2.0, -1.0, 0.0),1880Vec3::new(1.8, 0.5, 1.0),1881Vec3::new(-1.0, -2.0, 3.5),1882],1883};1884assert_eq!(tetrahedron.area(), 19.251068, "incorrect area");1885assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume");1886assert_eq!(1887tetrahedron.signed_volume(),18883.2058334,1889"incorrect signed volume"1890);1891assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55));18921893assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area");1894assert_eq!(1895Tetrahedron::default().volume(),18960.33333334,1897"incorrect volume"1898);1899assert_eq!(1900Tetrahedron::default().signed_volume(),1901-0.33333334,1902"incorrect signed volume"1903);1904assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);1905}19061907#[test]1908fn extrusion_math() {1909let circle = Circle::new(0.75);1910let cylinder = Extrusion::new(circle, 2.5);1911assert_eq!(cylinder.area(), 15.315264, "incorrect surface area");1912assert_eq!(cylinder.volume(), 4.417865, "incorrect volume");19131914let annulus = crate::primitives::Annulus::new(0.25, 1.375);1915let tube = Extrusion::new(annulus, 0.333);1916assert_eq!(tube.area(), 14.886437, "incorrect surface area");1917assert_eq!(tube.volume(), 1.9124937, "incorrect volume");19181919let polygon = crate::primitives::RegularPolygon::new(3.8, 7);1920let regular_prism = Extrusion::new(polygon, 1.25);1921assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area");1922assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume");1923}19241925#[test]1926fn triangle_math() {1927// Default triangle tests1928let mut default_triangle = Triangle3d::default();1929let reverse_default_triangle = Triangle3d::new(1930Vec3::new(0.5, -0.5, 0.0),1931Vec3::new(-0.5, -0.5, 0.0),1932Vec3::new(0.0, 0.5, 0.0),1933);1934assert_eq!(default_triangle.area(), 0.5, "incorrect area");1935assert_relative_eq!(1936default_triangle.perimeter(),19371.0 + 2.0 * ops::sqrt(1.25_f32),1938epsilon = 10e-91939);1940assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");1941assert!(1942!default_triangle.is_degenerate(),1943"incorrect degenerate check"1944);1945assert_eq!(1946default_triangle.centroid(),1947Vec3::new(0.0, -0.16666667, 0.0),1948"incorrect centroid"1949);1950assert_eq!(1951default_triangle.largest_side(),1952(Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0))1953);1954default_triangle.reverse();1955assert_eq!(1956default_triangle, reverse_default_triangle,1957"incorrect reverse"1958);1959assert_eq!(1960default_triangle.circumcenter(),1961Vec3::new(0.0, -0.125, 0.0),1962"incorrect circumcenter"1963);19641965// Custom triangle tests1966let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y);1967let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0));1968let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0));19691970assert_eq!(1971right_triangle.circumcenter(),1972Vec3::new(0.5, 0.5, 0.0),1973"incorrect circumcenter"1974);1975assert_eq!(1976obtuse_triangle.circumcenter(),1977Vec3::new(0.0, -4.95, 0.0),1978"incorrect circumcenter"1979);1980assert_eq!(1981acute_triangle.circumcenter(),1982Vec3::new(0.5, 2.475, 0.0),1983"incorrect circumcenter"1984);19851986assert!(acute_triangle.is_acute());1987assert!(!acute_triangle.is_obtuse());1988assert!(!obtuse_triangle.is_acute());1989assert!(obtuse_triangle.is_obtuse());19901991// Arbitrary triangle tests1992let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)];1993let triangle = Triangle3d::new(a, b, c);19941995assert!(!triangle.is_degenerate(), "incorrectly found degenerate");1996assert_eq!(triangle.area(), 3.0233467, "incorrect area");1997assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter");1998assert_eq!(1999triangle.circumcenter(),2000Vec3::new(-1., 1.75, 0.75),2001"incorrect circumcenter"2002);2003assert_eq!(2004triangle.normal(),2005Ok(Dir3::new_unchecked(Vec3::new(2006-0.04134491,2007-0.4134491,20080.909588042009))),2010"incorrect normal"2011);20122013// Degenerate triangle tests2014let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);2015assert!(2016zero_degenerate_triangle.is_degenerate(),2017"incorrect degenerate check"2018);2019assert_eq!(2020zero_degenerate_triangle.normal(),2021Err(InvalidDirectionError::Zero),2022"incorrect normal"2023);2024assert_eq!(2025zero_degenerate_triangle.largest_side(),2026(Vec3::ZERO, Vec3::ZERO),2027"incorrect largest side"2028);20292030let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);2031assert!(2032dup_degenerate_triangle.is_degenerate(),2033"incorrect degenerate check"2034);2035assert_eq!(2036dup_degenerate_triangle.normal(),2037Err(InvalidDirectionError::Zero),2038"incorrect normal"2039);2040assert_eq!(2041dup_degenerate_triangle.largest_side(),2042(Vec3::ZERO, Vec3::X),2043"incorrect largest side"2044);20452046let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);2047assert!(2048collinear_degenerate_triangle.is_degenerate(),2049"incorrect degenerate check"2050);2051assert_eq!(2052collinear_degenerate_triangle.normal(),2053Err(InvalidDirectionError::Zero),2054"incorrect normal"2055);2056assert_eq!(2057collinear_degenerate_triangle.largest_side(),2058(Vec3::NEG_X, Vec3::X),2059"incorrect largest side"2060);2061}2062}206320642065