bevy_math/primitives/
dim3.rs

1use core::f32::consts::{FRAC_PI_3, PI};
2
3use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};
4use crate::{
5    ops::{self, FloatPow},
6    Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3,
7};
8
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
12use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
13use glam::Quat;
14
15#[cfg(feature = "alloc")]
16use alloc::{boxed::Box, vec::Vec};
17
18/// A sphere primitive, representing the set of all points some distance from the origin
19#[derive(Clone, Copy, Debug, PartialEq)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(
22    feature = "bevy_reflect",
23    derive(Reflect),
24    reflect(Debug, PartialEq, Default, Clone)
25)]
26#[cfg_attr(
27    all(feature = "serialize", feature = "bevy_reflect"),
28    reflect(Serialize, Deserialize)
29)]
30pub struct Sphere {
31    /// The radius of the sphere
32    pub radius: f32,
33}
34impl Primitive3d for Sphere {}
35
36impl Default for Sphere {
37    /// Returns the default [`Sphere`] with a radius of `0.5`.
38    fn default() -> Self {
39        Self { radius: 0.5 }
40    }
41}
42
43impl Sphere {
44    /// Create a new [`Sphere`] from a `radius`
45    #[inline(always)]
46    pub const fn new(radius: f32) -> Self {
47        Self { radius }
48    }
49
50    /// Get the diameter of the sphere
51    #[inline(always)]
52    pub fn diameter(&self) -> f32 {
53        2.0 * self.radius
54    }
55
56    /// 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)]
61    pub fn closest_point(&self, point: Vec3) -> Vec3 {
62        let distance_squared = point.length_squared();
63
64        if distance_squared <= self.radius.squared() {
65            // The point is inside the sphere.
66            point
67        } else {
68            // The point is outside the sphere.
69            // Find the closest point on the surface of the sphere.
70            let dir_to_point = point / ops::sqrt(distance_squared);
71            self.radius * dir_to_point
72        }
73    }
74}
75
76impl Measured3d for Sphere {
77    /// Get the surface area of the sphere
78    #[inline(always)]
79    fn area(&self) -> f32 {
80        4.0 * PI * self.radius.squared()
81    }
82
83    /// Get the volume of the sphere
84    #[inline(always)]
85    fn volume(&self) -> f32 {
86        4.0 * FRAC_PI_3 * self.radius.cubed()
87    }
88}
89
90/// 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(
94    feature = "bevy_reflect",
95    derive(Reflect),
96    reflect(Debug, PartialEq, Default, Clone)
97)]
98#[cfg_attr(
99    all(feature = "serialize", feature = "bevy_reflect"),
100    reflect(Serialize, Deserialize)
101)]
102pub struct Plane3d {
103    /// The normal of the plane. The plane will be placed perpendicular to this direction
104    pub normal: Dir3,
105    /// Half of the width and height of the plane
106    pub half_size: Vec2,
107}
108impl Primitive3d for Plane3d {}
109
110impl Default for Plane3d {
111    /// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`.
112    fn default() -> Self {
113        Self {
114            normal: Dir3::Y,
115            half_size: Vec2::splat(0.5),
116        }
117    }
118}
119
120impl Plane3d {
121    /// Create a new `Plane3d` from a normal and a half size
122    ///
123    /// # Panics
124    ///
125    /// Panics if the given `normal` is zero (or very close to zero), or non-finite.
126    #[inline(always)]
127    pub fn new(normal: Vec3, half_size: Vec2) -> Self {
128        Self {
129            normal: Dir3::new(normal).expect("normal must be nonzero and finite"),
130            half_size,
131        }
132    }
133
134    /// Create a new `Plane3d` based on three points and compute the geometric center
135    /// of those points.
136    ///
137    /// The direction of the plane normal is determined by the winding order
138    /// of the triangular shape formed by the points.
139    ///
140    /// # Panics
141    ///
142    /// Panics if a valid normal can not be computed, for example when the points
143    /// are *collinear* and lie on the same line.
144    #[inline(always)]
145    pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
146        let normal = Dir3::new((b - a).cross(c - a)).expect(
147            "finite plane must be defined by three finite points that don't lie on the same line",
148        );
149        let translation = (a + b + c) / 3.0;
150
151        (
152            Self {
153                normal,
154                ..Default::default()
155            },
156            translation,
157        )
158    }
159}
160
161/// An unbounded plane in 3D space. It forms a separating surface through the origin,
162/// stretching infinitely far
163#[derive(Clone, Copy, Debug, PartialEq)]
164#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
165#[cfg_attr(
166    feature = "bevy_reflect",
167    derive(Reflect),
168    reflect(Debug, PartialEq, Default, Clone)
169)]
170#[cfg_attr(
171    all(feature = "serialize", feature = "bevy_reflect"),
172    reflect(Serialize, Deserialize)
173)]
174pub struct InfinitePlane3d {
175    /// The normal of the plane. The plane will be placed perpendicular to this direction
176    pub normal: Dir3,
177}
178impl Primitive3d for InfinitePlane3d {}
179
180impl Default for InfinitePlane3d {
181    /// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction.
182    fn default() -> Self {
183        Self { normal: Dir3::Y }
184    }
185}
186
187impl InfinitePlane3d {
188    /// Create a new `InfinitePlane3d` from a normal
189    ///
190    /// # Panics
191    ///
192    /// Panics if the given `normal` is zero (or very close to zero), or non-finite.
193    #[inline(always)]
194    pub fn new<T: TryInto<Dir3>>(normal: T) -> Self
195    where
196        <T as TryInto<Dir3>>::Error: core::fmt::Debug,
197    {
198        Self {
199            normal: normal
200                .try_into()
201                .expect("normal must be nonzero and finite"),
202        }
203    }
204
205    /// Create a new `InfinitePlane3d` based on three points and compute the geometric center
206    /// of those points.
207    ///
208    /// The direction of the plane normal is determined by the winding order
209    /// of the triangular shape formed by the points.
210    ///
211    /// # Panics
212    ///
213    /// Panics if a valid normal can not be computed, for example when the points
214    /// are *collinear* and lie on the same line.
215    #[inline(always)]
216    pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
217        let normal = Dir3::new((b - a).cross(c - a)).expect(
218            "infinite plane must be defined by three finite points that don't lie on the same line",
219        );
220        let translation = (a + b + c) / 3.0;
221
222        (Self { normal }, translation)
223    }
224
225    /// Computes the shortest distance between a plane transformed with the given `isometry` and a
226    /// `point`. The result is a signed value; it's positive if the point lies in the half-space
227    /// that the plane's normal vector points towards.
228    #[inline]
229    pub fn signed_distance(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> f32 {
230        let isometry = isometry.into();
231        self.normal.dot(isometry.inverse() * point)
232    }
233
234    /// Injects the `point` into this plane transformed with the given `isometry`.
235    ///
236    /// This projects the point orthogonally along the shortest path onto the plane.
237    #[inline]
238    pub fn project_point(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> Vec3 {
239        point - self.normal * self.signed_distance(isometry, point)
240    }
241
242    /// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given
243    /// `origin` to the XY-plane.
244    ///
245    /// ## Guarantees
246    ///
247    /// * the transformation is a [congruence] meaning it will preserve all distances and angles of
248    ///   the transformed geometry
249    /// * uses the least rotation possible to transform the geometry
250    /// * if two geometries are transformed with the same isometry, then the relations between
251    ///   them, like distances, are also preserved
252    /// * compared to projections, the transformation is lossless (up to floating point errors)
253    ///   reversible
254    ///
255    /// ## Non-Guarantees
256    ///
257    /// * the rotation used is generally not unique
258    /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to
259    ///   enforce some kind of alignment the user has to use an extra transformation ontop of this
260    ///   one
261    ///
262    /// See [`isometries_xy`] for example usescases.
263    ///
264    /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)
265    /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`
266    #[inline]
267    pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d {
268        let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z);
269        let transformed_origin = rotation * origin;
270        Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation)
271    }
272
273    /// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the
274    /// given `origin`.
275    ///
276    /// ## Guarantees
277    ///
278    /// * the transformation is a [congruence] meaning it will preserve all distances and angles of
279    ///   the transformed geometry
280    /// * uses the least rotation possible to transform the geometry
281    /// * if two geometries are transformed with the same isometry, then the relations between
282    ///   them, like distances, are also preserved
283    /// * compared to projections, the transformation is lossless (up to floating point errors)
284    ///   reversible
285    ///
286    /// ## Non-Guarantees
287    ///
288    /// * the rotation used is generally not unique
289    /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to
290    ///   enforce some kind of alignment the user has to use an extra transformation ontop of this
291    ///   one
292    ///
293    /// See [`isometries_xy`] for example usescases.
294    ///
295    /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)
296    /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`
297    #[inline]
298    pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d {
299        self.isometry_into_xy(origin).inverse()
300    }
301
302    /// Computes both [isometries] which transforms points from the plane in 3D space with the
303    /// given `origin` to the XY-plane and back.
304    ///
305    /// [isometries]: `Isometry3d`
306    ///
307    /// # Example
308    ///
309    /// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The
310    /// workflow would usually look like this:
311    ///
312    /// ```
313    /// # use bevy_math::{Vec3, Dir3};
314    /// # use bevy_math::primitives::InfinitePlane3d;
315    ///
316    /// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z];
317    /// let center = (a + b + c) / 3.0;
318    ///
319    /// let plane = InfinitePlane3d::new(Vec3::ONE);
320    ///
321    /// let (to_xy, from_xy) = plane.isometries_xy(center);
322    ///
323    /// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate());
324    ///
325    /// // apply some algorithm to `triangle_2d`
326    ///
327    /// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3);
328    /// ```
329    #[inline]
330    pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) {
331        let projection = self.isometry_into_xy(origin);
332        (projection, projection.inverse())
333    }
334}
335
336/// An infinite line going through the origin along a direction in 3D space.
337///
338/// For a finite line: [`Segment3d`]
339#[derive(Clone, Copy, Debug, PartialEq)]
340#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
341#[cfg_attr(
342    feature = "bevy_reflect",
343    derive(Reflect),
344    reflect(Debug, PartialEq, Clone)
345)]
346#[cfg_attr(
347    all(feature = "serialize", feature = "bevy_reflect"),
348    reflect(Serialize, Deserialize)
349)]
350pub struct Line3d {
351    /// The direction of the line
352    pub direction: Dir3,
353}
354impl Primitive3d for Line3d {}
355
356/// A line segment defined by two endpoints in 3D space.
357#[derive(Clone, Copy, Debug, PartialEq)]
358#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
359#[cfg_attr(
360    feature = "bevy_reflect",
361    derive(Reflect),
362    reflect(Debug, PartialEq, Clone)
363)]
364#[cfg_attr(
365    all(feature = "serialize", feature = "bevy_reflect"),
366    reflect(Serialize, Deserialize)
367)]
368#[doc(alias = "LineSegment3d")]
369pub struct Segment3d {
370    /// The endpoints of the line segment.
371    pub vertices: [Vec3; 2],
372}
373impl Primitive3d for Segment3d {}
374
375impl Segment3d {
376    /// Create a new `Segment3d` from its endpoints.
377    #[inline(always)]
378    pub const fn new(point1: Vec3, point2: Vec3) -> Self {
379        Self {
380            vertices: [point1, point2],
381        }
382    }
383
384    /// Create a new `Segment3d` from its endpoints and compute its geometric center.
385    #[inline(always)]
386    #[deprecated(since = "0.16.0", note = "Use the `new` constructor instead")]
387    pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) {
388        (Self::new(point1, point2), (point1 + point2) / 2.)
389    }
390
391    /// Create a new `Segment3d` centered at the origin with the given direction and length.
392    ///
393    /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`.
394    #[inline(always)]
395    pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {
396        let endpoint = 0.5 * length * direction;
397        Self {
398            vertices: [-endpoint, endpoint],
399        }
400    }
401
402    /// Create a new `Segment3d` centered at the origin from a vector representing
403    /// the direction and length of the line segment.
404    ///
405    /// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`.
406    #[inline(always)]
407    pub fn from_scaled_direction(scaled_direction: Vec3) -> Self {
408        let endpoint = 0.5 * scaled_direction;
409        Self {
410            vertices: [-endpoint, endpoint],
411        }
412    }
413
414    /// Create a new `Segment3d` starting from the origin of the given `ray`,
415    /// going in the direction of the ray for the given `length`.
416    ///
417    /// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`.
418    #[inline(always)]
419    pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self {
420        Self {
421            vertices: [ray.origin, ray.get_point(length)],
422        }
423    }
424
425    /// Get the position of the first endpoint of the line segment.
426    #[inline(always)]
427    pub fn point1(&self) -> Vec3 {
428        self.vertices[0]
429    }
430
431    /// Get the position of the second endpoint of the line segment.
432    #[inline(always)]
433    pub fn point2(&self) -> Vec3 {
434        self.vertices[1]
435    }
436
437    /// Compute the midpoint between the two endpoints of the line segment.
438    #[inline(always)]
439    #[doc(alias = "midpoint")]
440    pub fn center(&self) -> Vec3 {
441        self.point1().midpoint(self.point2())
442    }
443
444    /// Compute the length of the line segment.
445    #[inline(always)]
446    pub fn length(&self) -> f32 {
447        self.point1().distance(self.point2())
448    }
449
450    /// Compute the squared length of the line segment.
451    #[inline(always)]
452    pub fn length_squared(&self) -> f32 {
453        self.point1().distance_squared(self.point2())
454    }
455
456    /// Compute the normalized direction pointing from the first endpoint to the second endpoint.
457    ///
458    /// For the non-panicking version, see [`Segment3d::try_direction`].
459    ///
460    /// # Panics
461    ///
462    /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.
463    #[inline(always)]
464    pub fn direction(&self) -> Dir3 {
465        self.try_direction().unwrap_or_else(|err| {
466            panic!("Failed to compute the direction of a line segment: {err}")
467        })
468    }
469
470    /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
471    ///
472    /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,
473    /// for example when the endpoints are coincident, NaN, or infinite.
474    #[inline(always)]
475    pub fn try_direction(&self) -> Result<Dir3, InvalidDirectionError> {
476        Dir3::new(self.scaled_direction())
477    }
478
479    /// Compute the vector from the first endpoint to the second endpoint.
480    #[inline(always)]
481    pub fn scaled_direction(&self) -> Vec3 {
482        self.point2() - self.point1()
483    }
484
485    /// Compute the segment transformed by the given [`Isometry3d`].
486    #[inline(always)]
487    pub fn transformed(&self, isometry: impl Into<Isometry3d>) -> Self {
488        let isometry: Isometry3d = isometry.into();
489        Self::new(
490            isometry.transform_point(self.point1()).into(),
491            isometry.transform_point(self.point2()).into(),
492        )
493    }
494
495    /// Compute the segment translated by the given vector.
496    #[inline(always)]
497    pub fn translated(&self, translation: Vec3) -> Segment3d {
498        Self::new(self.point1() + translation, self.point2() + translation)
499    }
500
501    /// Compute the segment rotated around the origin by the given rotation.
502    #[inline(always)]
503    pub fn rotated(&self, rotation: Quat) -> Segment3d {
504        Segment3d::new(rotation * self.point1(), rotation * self.point2())
505    }
506
507    /// Compute the segment rotated around the given point by the given rotation.
508    #[inline(always)]
509    pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {
510        // We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back
511        let offset = self.translated(-point);
512        let rotated = offset.rotated(rotation);
513        rotated.translated(point)
514    }
515
516    /// Compute the segment rotated around its own center.
517    #[inline(always)]
518    pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {
519        self.rotated_around(rotation, self.center())
520    }
521
522    /// Compute the segment with its center at the origin, keeping the same direction and length.
523    #[inline(always)]
524    pub fn centered(&self) -> Segment3d {
525        let center = self.center();
526        self.translated(-center)
527    }
528
529    /// Compute the segment with a new length, keeping the same direction and center.
530    #[inline(always)]
531    pub fn resized(&self, length: f32) -> Segment3d {
532        let offset_from_origin = self.center();
533        let centered = self.translated(-offset_from_origin);
534        let ratio = length / self.length();
535        let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);
536        segment.translated(offset_from_origin)
537    }
538
539    /// Reverses the direction of the line segment by swapping the endpoints.
540    #[inline(always)]
541    pub fn reverse(&mut self) {
542        let [point1, point2] = &mut self.vertices;
543        core::mem::swap(point1, point2);
544    }
545
546    /// Returns the line segment with its direction reversed by swapping the endpoints.
547    #[inline(always)]
548    #[must_use]
549    pub fn reversed(mut self) -> Self {
550        self.reverse();
551        self
552    }
553}
554
555impl From<[Vec3; 2]> for Segment3d {
556    #[inline(always)]
557    fn from(vertices: [Vec3; 2]) -> Self {
558        Self { vertices }
559    }
560}
561
562impl From<(Vec3, Vec3)> for Segment3d {
563    #[inline(always)]
564    fn from((point1, point2): (Vec3, Vec3)) -> Self {
565        Self::new(point1, point2)
566    }
567}
568
569/// A series of connected line segments in 3D space.
570///
571/// For a version without generics: [`BoxedPolyline3d`]
572#[derive(Clone, Debug, PartialEq)]
573#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
574#[cfg_attr(
575    feature = "bevy_reflect",
576    derive(Reflect),
577    reflect(Debug, PartialEq, Clone)
578)]
579#[cfg_attr(
580    all(feature = "serialize", feature = "bevy_reflect"),
581    reflect(Serialize, Deserialize)
582)]
583pub struct Polyline3d<const N: usize> {
584    /// The vertices of the polyline
585    #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))]
586    pub vertices: [Vec3; N],
587}
588impl<const N: usize> Primitive3d for Polyline3d<N> {}
589
590impl<const N: usize> FromIterator<Vec3> for Polyline3d<N> {
591    fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
592        let mut vertices: [Vec3; N] = [Vec3::ZERO; N];
593
594        for (index, i) in iter.into_iter().take(N).enumerate() {
595            vertices[index] = i;
596        }
597        Self { vertices }
598    }
599}
600
601impl<const N: usize> Polyline3d<N> {
602    /// Create a new `Polyline3d` from its vertices
603    pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
604        Self::from_iter(vertices)
605    }
606}
607
608/// A series of connected line segments in 3D space, allocated on the heap
609/// in a `Box<[Vec3]>`.
610///
611/// For a version without alloc: [`Polyline3d`]
612#[cfg(feature = "alloc")]
613#[derive(Clone, Debug, PartialEq)]
614#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
615pub struct BoxedPolyline3d {
616    /// The vertices of the polyline
617    pub vertices: Box<[Vec3]>,
618}
619
620#[cfg(feature = "alloc")]
621impl Primitive3d for BoxedPolyline3d {}
622
623#[cfg(feature = "alloc")]
624impl FromIterator<Vec3> for BoxedPolyline3d {
625    fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
626        let vertices: Vec<Vec3> = iter.into_iter().collect();
627        Self {
628            vertices: vertices.into_boxed_slice(),
629        }
630    }
631}
632
633#[cfg(feature = "alloc")]
634impl BoxedPolyline3d {
635    /// Create a new `BoxedPolyline3d` from its vertices
636    pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
637        Self::from_iter(vertices)
638    }
639}
640
641/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not
642/// required to be the same.
643#[derive(Clone, Copy, Debug, PartialEq)]
644#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
645#[cfg_attr(
646    feature = "bevy_reflect",
647    derive(Reflect),
648    reflect(Debug, PartialEq, Default, Clone)
649)]
650#[cfg_attr(
651    all(feature = "serialize", feature = "bevy_reflect"),
652    reflect(Serialize, Deserialize)
653)]
654pub struct Cuboid {
655    /// Half of the width, height and depth of the cuboid
656    pub half_size: Vec3,
657}
658impl Primitive3d for Cuboid {}
659
660impl Default for Cuboid {
661    /// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`.
662    fn default() -> Self {
663        Self {
664            half_size: Vec3::splat(0.5),
665        }
666    }
667}
668
669impl Cuboid {
670    /// Create a new `Cuboid` from a full x, y, and z length
671    #[inline(always)]
672    pub fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {
673        Self::from_size(Vec3::new(x_length, y_length, z_length))
674    }
675
676    /// Create a new `Cuboid` from a given full size
677    #[inline(always)]
678    pub fn from_size(size: Vec3) -> Self {
679        Self {
680            half_size: size / 2.0,
681        }
682    }
683
684    /// Create a new `Cuboid` from two corner points
685    #[inline(always)]
686    pub fn from_corners(point1: Vec3, point2: Vec3) -> Self {
687        Self {
688            half_size: (point2 - point1).abs() / 2.0,
689        }
690    }
691
692    /// Create a `Cuboid` from a single length.
693    /// The resulting `Cuboid` will be the same size in every direction.
694    #[inline(always)]
695    pub fn from_length(length: f32) -> Self {
696        Self {
697            half_size: Vec3::splat(length / 2.0),
698        }
699    }
700
701    /// Get the size of the cuboid
702    #[inline(always)]
703    pub fn size(&self) -> Vec3 {
704        2.0 * self.half_size
705    }
706
707    /// Finds the point on the cuboid that is closest to the given `point`.
708    ///
709    /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid.
710    /// Otherwise, it will be inside the cuboid and returned as is.
711    #[inline(always)]
712    pub fn closest_point(&self, point: Vec3) -> Vec3 {
713        // Clamp point coordinates to the cuboid
714        point.clamp(-self.half_size, self.half_size)
715    }
716}
717
718impl Measured3d for Cuboid {
719    /// Get the surface area of the cuboid
720    #[inline(always)]
721    fn area(&self) -> f32 {
722        8.0 * (self.half_size.x * self.half_size.y
723            + self.half_size.y * self.half_size.z
724            + self.half_size.x * self.half_size.z)
725    }
726
727    /// Get the volume of the cuboid
728    #[inline(always)]
729    fn volume(&self) -> f32 {
730        8.0 * self.half_size.x * self.half_size.y * self.half_size.z
731    }
732}
733
734/// A cylinder primitive centered on the origin
735#[derive(Clone, Copy, Debug, PartialEq)]
736#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
737#[cfg_attr(
738    feature = "bevy_reflect",
739    derive(Reflect),
740    reflect(Debug, PartialEq, Default, Clone)
741)]
742#[cfg_attr(
743    all(feature = "serialize", feature = "bevy_reflect"),
744    reflect(Serialize, Deserialize)
745)]
746pub struct Cylinder {
747    /// The radius of the cylinder
748    pub radius: f32,
749    /// The half height of the cylinder
750    pub half_height: f32,
751}
752impl Primitive3d for Cylinder {}
753
754impl Default for Cylinder {
755    /// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`.
756    fn default() -> Self {
757        Self {
758            radius: 0.5,
759            half_height: 0.5,
760        }
761    }
762}
763
764impl Cylinder {
765    /// Create a new `Cylinder` from a radius and full height
766    #[inline(always)]
767    pub fn new(radius: f32, height: f32) -> Self {
768        Self {
769            radius,
770            half_height: height / 2.0,
771        }
772    }
773
774    /// Get the base of the cylinder as a [`Circle`]
775    #[inline(always)]
776    pub fn base(&self) -> Circle {
777        Circle {
778            radius: self.radius,
779        }
780    }
781
782    /// Get the surface area of the side of the cylinder,
783    /// also known as the lateral area
784    #[inline(always)]
785    #[doc(alias = "side_area")]
786    pub fn lateral_area(&self) -> f32 {
787        4.0 * PI * self.radius * self.half_height
788    }
789
790    /// Get the surface area of one base of the cylinder
791    #[inline(always)]
792    pub fn base_area(&self) -> f32 {
793        PI * self.radius.squared()
794    }
795}
796
797impl Measured3d for Cylinder {
798    /// Get the total surface area of the cylinder
799    #[inline(always)]
800    fn area(&self) -> f32 {
801        2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)
802    }
803
804    /// Get the volume of the cylinder
805    #[inline(always)]
806    fn volume(&self) -> f32 {
807        self.base_area() * 2.0 * self.half_height
808    }
809}
810
811/// A 3D capsule primitive centered on the origin
812/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line
813#[derive(Clone, Copy, Debug, PartialEq)]
814#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
815#[cfg_attr(
816    feature = "bevy_reflect",
817    derive(Reflect),
818    reflect(Debug, PartialEq, Default, Clone)
819)]
820#[cfg_attr(
821    all(feature = "serialize", feature = "bevy_reflect"),
822    reflect(Serialize, Deserialize)
823)]
824pub struct Capsule3d {
825    /// The radius of the capsule
826    pub radius: f32,
827    /// Half the height of the capsule, excluding the hemispheres
828    pub half_length: f32,
829}
830impl Primitive3d for Capsule3d {}
831
832impl Default for Capsule3d {
833    /// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`.
834    /// The total height is `2.0`.
835    fn default() -> Self {
836        Self {
837            radius: 0.5,
838            half_length: 0.5,
839        }
840    }
841}
842
843impl Capsule3d {
844    /// Create a new `Capsule3d` from a radius and length
845    pub fn new(radius: f32, length: f32) -> Self {
846        Self {
847            radius,
848            half_length: length / 2.0,
849        }
850    }
851
852    /// Get the part connecting the hemispherical ends
853    /// of the capsule as a [`Cylinder`]
854    #[inline(always)]
855    pub fn to_cylinder(&self) -> Cylinder {
856        Cylinder {
857            radius: self.radius,
858            half_height: self.half_length,
859        }
860    }
861}
862
863impl Measured3d for Capsule3d {
864    /// Get the surface area of the capsule
865    #[inline(always)]
866    fn area(&self) -> f32 {
867        // Modified version of 2pi * r * (2r + h)
868        4.0 * PI * self.radius * (self.radius + self.half_length)
869    }
870
871    /// Get the volume of the capsule
872    #[inline(always)]
873    fn volume(&self) -> f32 {
874        // Modified version of pi * r^2 * (4/3 * r + a)
875        let diameter = self.radius * 2.0;
876        PI * self.radius * diameter * (diameter / 3.0 + self.half_length)
877    }
878}
879
880/// A cone primitive centered on the midpoint between the tip of the cone and the center of its base.
881///
882/// The cone is oriented with its tip pointing towards the Y axis.
883#[derive(Clone, Copy, Debug, PartialEq)]
884#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
885#[cfg_attr(
886    feature = "bevy_reflect",
887    derive(Reflect),
888    reflect(Debug, PartialEq, Default, Clone)
889)]
890#[cfg_attr(
891    all(feature = "serialize", feature = "bevy_reflect"),
892    reflect(Serialize, Deserialize)
893)]
894pub struct Cone {
895    /// The radius of the base
896    pub radius: f32,
897    /// The height of the cone
898    pub height: f32,
899}
900impl Primitive3d for Cone {}
901
902impl Default for Cone {
903    /// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`.
904    fn default() -> Self {
905        Self {
906            radius: 0.5,
907            height: 1.0,
908        }
909    }
910}
911
912impl Cone {
913    /// Create a new [`Cone`] from a radius and height.
914    pub fn new(radius: f32, height: f32) -> Self {
915        Self { radius, height }
916    }
917    /// Get the base of the cone as a [`Circle`]
918    #[inline(always)]
919    pub fn base(&self) -> Circle {
920        Circle {
921            radius: self.radius,
922        }
923    }
924
925    /// Get the slant height of the cone, the length of the line segment
926    /// connecting a point on the base to the apex
927    #[inline(always)]
928    #[doc(alias = "side_length")]
929    pub fn slant_height(&self) -> f32 {
930        ops::hypot(self.radius, self.height)
931    }
932
933    /// Get the surface area of the side of the cone,
934    /// also known as the lateral area
935    #[inline(always)]
936    #[doc(alias = "side_area")]
937    pub fn lateral_area(&self) -> f32 {
938        PI * self.radius * self.slant_height()
939    }
940
941    /// Get the surface area of the base of the cone
942    #[inline(always)]
943    pub fn base_area(&self) -> f32 {
944        PI * self.radius.squared()
945    }
946}
947
948impl Measured3d for Cone {
949    /// Get the total surface area of the cone
950    #[inline(always)]
951    fn area(&self) -> f32 {
952        self.base_area() + self.lateral_area()
953    }
954
955    /// Get the volume of the cone
956    #[inline(always)]
957    fn volume(&self) -> f32 {
958        (self.base_area() * self.height) / 3.0
959    }
960}
961
962/// A conical frustum primitive.
963/// A conical frustum can be created
964/// by slicing off a section of a cone.
965#[derive(Clone, Copy, Debug, PartialEq)]
966#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
967#[cfg_attr(
968    feature = "bevy_reflect",
969    derive(Reflect),
970    reflect(Debug, PartialEq, Default, Clone)
971)]
972#[cfg_attr(
973    all(feature = "serialize", feature = "bevy_reflect"),
974    reflect(Serialize, Deserialize)
975)]
976pub struct ConicalFrustum {
977    /// The radius of the top of the frustum
978    pub radius_top: f32,
979    /// The radius of the base of the frustum
980    pub radius_bottom: f32,
981    /// The height of the frustum
982    pub height: f32,
983}
984impl Primitive3d for ConicalFrustum {}
985
986impl Default for ConicalFrustum {
987    /// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`.
988    fn default() -> Self {
989        Self {
990            radius_top: 0.25,
991            radius_bottom: 0.5,
992            height: 0.5,
993        }
994    }
995}
996
997/// The type of torus determined by the minor and major radii
998#[derive(Clone, Copy, Debug, PartialEq, Eq)]
999pub enum TorusKind {
1000    /// A torus that has a ring.
1001    /// The major radius is greater than the minor radius
1002    Ring,
1003    /// A torus that has no hole but also doesn't intersect itself.
1004    /// The major radius is equal to the minor radius
1005    Horn,
1006    /// A self-intersecting torus.
1007    /// The major radius is less than the minor radius
1008    Spindle,
1009    /// A torus with non-geometric properties like
1010    /// a minor or major radius that is non-positive,
1011    /// infinite, or `NaN`
1012    Invalid,
1013}
1014
1015/// A torus primitive, often representing a ring or donut shape
1016/// The set of points some distance from a circle centered at the origin
1017#[derive(Clone, Copy, Debug, PartialEq)]
1018#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1019#[cfg_attr(
1020    feature = "bevy_reflect",
1021    derive(Reflect),
1022    reflect(Debug, PartialEq, Default, Clone)
1023)]
1024#[cfg_attr(
1025    all(feature = "serialize", feature = "bevy_reflect"),
1026    reflect(Serialize, Deserialize)
1027)]
1028pub struct Torus {
1029    /// The radius of the tube of the torus
1030    #[doc(
1031        alias = "ring_radius",
1032        alias = "tube_radius",
1033        alias = "cross_section_radius"
1034    )]
1035    pub minor_radius: f32,
1036    /// The distance from the center of the torus to the center of the tube
1037    #[doc(alias = "radius_of_revolution")]
1038    pub major_radius: f32,
1039}
1040impl Primitive3d for Torus {}
1041
1042impl Default for Torus {
1043    /// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`.
1044    fn default() -> Self {
1045        Self {
1046            minor_radius: 0.25,
1047            major_radius: 0.75,
1048        }
1049    }
1050}
1051
1052impl Torus {
1053    /// Create a new `Torus` from an inner and outer radius.
1054    ///
1055    /// The inner radius is the radius of the hole, and the outer radius
1056    /// is the radius of the entire object
1057    #[inline(always)]
1058    pub fn new(inner_radius: f32, outer_radius: f32) -> Self {
1059        let minor_radius = (outer_radius - inner_radius) / 2.0;
1060        let major_radius = outer_radius - minor_radius;
1061
1062        Self {
1063            minor_radius,
1064            major_radius,
1065        }
1066    }
1067
1068    /// Get the inner radius of the torus.
1069    /// For a ring torus, this corresponds to the radius of the hole,
1070    /// or `major_radius - minor_radius`
1071    #[inline(always)]
1072    pub fn inner_radius(&self) -> f32 {
1073        self.major_radius - self.minor_radius
1074    }
1075
1076    /// Get the outer radius of the torus.
1077    /// This corresponds to the overall radius of the entire object,
1078    /// or `major_radius + minor_radius`
1079    #[inline(always)]
1080    pub fn outer_radius(&self) -> f32 {
1081        self.major_radius + self.minor_radius
1082    }
1083
1084    /// Get the [`TorusKind`] determined by the minor and major radii.
1085    ///
1086    /// The torus can either be a *ring torus* that has a hole,
1087    /// a *horn torus* that doesn't have a hole but also isn't self-intersecting,
1088    /// or a *spindle torus* that is self-intersecting.
1089    ///
1090    /// If the minor or major radius is non-positive, infinite, or `NaN`,
1091    /// [`TorusKind::Invalid`] is returned
1092    #[inline(always)]
1093    pub fn kind(&self) -> TorusKind {
1094        // Invalid if minor or major radius is non-positive, infinite, or NaN
1095        if self.minor_radius <= 0.0
1096            || !self.minor_radius.is_finite()
1097            || self.major_radius <= 0.0
1098            || !self.major_radius.is_finite()
1099        {
1100            return TorusKind::Invalid;
1101        }
1102
1103        match self.major_radius.partial_cmp(&self.minor_radius).unwrap() {
1104            core::cmp::Ordering::Greater => TorusKind::Ring,
1105            core::cmp::Ordering::Equal => TorusKind::Horn,
1106            core::cmp::Ordering::Less => TorusKind::Spindle,
1107        }
1108    }
1109}
1110
1111impl Measured3d for Torus {
1112    /// Get the surface area of the torus. Note that this only produces
1113    /// the expected result when the torus has a ring and isn't self-intersecting
1114    #[inline(always)]
1115    fn area(&self) -> f32 {
1116        4.0 * PI.squared() * self.major_radius * self.minor_radius
1117    }
1118
1119    /// Get the volume of the torus. Note that this only produces
1120    /// the expected result when the torus has a ring and isn't self-intersecting
1121    #[inline(always)]
1122    fn volume(&self) -> f32 {
1123        2.0 * PI.squared() * self.major_radius * self.minor_radius.squared()
1124    }
1125}
1126
1127/// A 3D triangle primitive.
1128#[derive(Clone, Copy, Debug, PartialEq)]
1129#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1130#[cfg_attr(
1131    feature = "bevy_reflect",
1132    derive(Reflect),
1133    reflect(Debug, PartialEq, Default, Clone)
1134)]
1135#[cfg_attr(
1136    all(feature = "serialize", feature = "bevy_reflect"),
1137    reflect(Serialize, Deserialize)
1138)]
1139pub struct Triangle3d {
1140    /// The vertices of the triangle.
1141    pub vertices: [Vec3; 3],
1142}
1143
1144impl Primitive3d for Triangle3d {}
1145
1146impl Default for Triangle3d {
1147    /// 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]`.
1148    fn default() -> Self {
1149        Self {
1150            vertices: [
1151                Vec3::new(0.0, 0.5, 0.0),
1152                Vec3::new(-0.5, -0.5, 0.0),
1153                Vec3::new(0.5, -0.5, 0.0),
1154            ],
1155        }
1156    }
1157}
1158
1159impl Triangle3d {
1160    /// Create a new [`Triangle3d`] from points `a`, `b`, and `c`.
1161    #[inline(always)]
1162    pub fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {
1163        Self {
1164            vertices: [a, b, c],
1165        }
1166    }
1167
1168    /// Get the normal of the triangle in the direction of the right-hand rule, assuming
1169    /// the vertices are ordered in a counter-clockwise direction.
1170    ///
1171    /// The normal is computed as the cross product of the vectors `ab` and `ac`.
1172    ///
1173    /// # Errors
1174    ///
1175    /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
1176    /// of the given vector is zero (or very close to zero), infinite, or `NaN`.
1177    #[inline(always)]
1178    pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {
1179        let [a, b, c] = self.vertices;
1180        let ab = b - a;
1181        let ac = c - a;
1182        Dir3::new(ab.cross(ac))
1183    }
1184
1185    /// Checks if the triangle is degenerate, meaning it has zero area.
1186    ///
1187    /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`.
1188    /// This indicates that the three vertices are collinear or nearly collinear.
1189    #[inline(always)]
1190    pub fn is_degenerate(&self) -> bool {
1191        let [a, b, c] = self.vertices;
1192        let ab = b - a;
1193        let ac = c - a;
1194        ab.cross(ac).length() < 10e-7
1195    }
1196
1197    /// Checks if the triangle is acute, meaning all angles are less than 90 degrees
1198    #[inline(always)]
1199    pub fn is_acute(&self) -> bool {
1200        let [a, b, c] = self.vertices;
1201        let ab = b - a;
1202        let bc = c - b;
1203        let ca = a - c;
1204
1205        // a^2 + b^2 < c^2 for an acute triangle
1206        let mut side_lengths = [
1207            ab.length_squared(),
1208            bc.length_squared(),
1209            ca.length_squared(),
1210        ];
1211        side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
1212        side_lengths[0] + side_lengths[1] > side_lengths[2]
1213    }
1214
1215    /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees
1216    #[inline(always)]
1217    pub fn is_obtuse(&self) -> bool {
1218        let [a, b, c] = self.vertices;
1219        let ab = b - a;
1220        let bc = c - b;
1221        let ca = a - c;
1222
1223        // a^2 + b^2 > c^2 for an obtuse triangle
1224        let mut side_lengths = [
1225            ab.length_squared(),
1226            bc.length_squared(),
1227            ca.length_squared(),
1228        ];
1229        side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
1230        side_lengths[0] + side_lengths[1] < side_lengths[2]
1231    }
1232
1233    /// Reverse the triangle by swapping the first and last vertices.
1234    #[inline(always)]
1235    pub fn reverse(&mut self) {
1236        self.vertices.swap(0, 2);
1237    }
1238
1239    /// This triangle but reversed.
1240    #[inline(always)]
1241    #[must_use]
1242    pub fn reversed(mut self) -> Triangle3d {
1243        self.reverse();
1244        self
1245    }
1246
1247    /// Get the centroid of the triangle.
1248    ///
1249    /// This function finds the geometric center of the triangle by averaging the vertices:
1250    /// `centroid = (a + b + c) / 3`.
1251    #[doc(alias("center", "barycenter", "baricenter"))]
1252    #[inline(always)]
1253    pub fn centroid(&self) -> Vec3 {
1254        (self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0
1255    }
1256
1257    /// Get the largest side of the triangle.
1258    ///
1259    /// Returns the two points that form the largest side of the triangle.
1260    #[inline(always)]
1261    pub fn largest_side(&self) -> (Vec3, Vec3) {
1262        let [a, b, c] = self.vertices;
1263        let ab = b - a;
1264        let bc = c - b;
1265        let ca = a - c;
1266
1267        let mut largest_side_points = (a, b);
1268        let mut largest_side_length = ab.length();
1269
1270        if bc.length() > largest_side_length {
1271            largest_side_points = (b, c);
1272            largest_side_length = bc.length();
1273        }
1274
1275        if ca.length() > largest_side_length {
1276            largest_side_points = (a, c);
1277        }
1278
1279        largest_side_points
1280    }
1281
1282    /// Get the circumcenter of the triangle.
1283    #[inline(always)]
1284    pub fn circumcenter(&self) -> Vec3 {
1285        if self.is_degenerate() {
1286            // If the triangle is degenerate, the circumcenter is the midpoint of the largest side.
1287            let (p1, p2) = self.largest_side();
1288            return (p1 + p2) / 2.0;
1289        }
1290
1291        let [a, b, c] = self.vertices;
1292        let ab = b - a;
1293        let ac = c - a;
1294        let n = ab.cross(ac);
1295
1296        // Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d
1297        a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))
1298            / (2.0 * n.length_squared()))
1299    }
1300}
1301
1302impl Measured2d for Triangle3d {
1303    /// Get the area of the triangle.
1304    #[inline(always)]
1305    fn area(&self) -> f32 {
1306        let [a, b, c] = self.vertices;
1307        let ab = b - a;
1308        let ac = c - a;
1309        ab.cross(ac).length() / 2.0
1310    }
1311
1312    /// Get the perimeter of the triangle.
1313    #[inline(always)]
1314    fn perimeter(&self) -> f32 {
1315        let [a, b, c] = self.vertices;
1316        a.distance(b) + b.distance(c) + c.distance(a)
1317    }
1318}
1319
1320/// A tetrahedron primitive.
1321#[derive(Clone, Copy, Debug, PartialEq)]
1322#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1323#[cfg_attr(
1324    feature = "bevy_reflect",
1325    derive(Reflect),
1326    reflect(Debug, PartialEq, Default, Clone)
1327)]
1328#[cfg_attr(
1329    all(feature = "serialize", feature = "bevy_reflect"),
1330    reflect(Serialize, Deserialize)
1331)]
1332pub struct Tetrahedron {
1333    /// The vertices of the tetrahedron.
1334    pub vertices: [Vec3; 4],
1335}
1336impl Primitive3d for Tetrahedron {}
1337
1338impl Default for Tetrahedron {
1339    /// Returns the default [`Tetrahedron`] with the vertices
1340    /// `[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]`.
1341    fn default() -> Self {
1342        Self {
1343            vertices: [
1344                Vec3::new(0.5, 0.5, 0.5),
1345                Vec3::new(-0.5, 0.5, -0.5),
1346                Vec3::new(-0.5, -0.5, 0.5),
1347                Vec3::new(0.5, -0.5, -0.5),
1348            ],
1349        }
1350    }
1351}
1352
1353impl Tetrahedron {
1354    /// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`.
1355    #[inline(always)]
1356    pub fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self {
1357        Self {
1358            vertices: [a, b, c, d],
1359        }
1360    }
1361
1362    /// Get the signed volume of the tetrahedron.
1363    ///
1364    /// If it's negative, the normal vector of the face defined by
1365    /// the first three points using the right-hand rule points
1366    /// away from the fourth vertex.
1367    #[inline(always)]
1368    pub fn signed_volume(&self) -> f32 {
1369        let [a, b, c, d] = self.vertices;
1370        let ab = b - a;
1371        let ac = c - a;
1372        let ad = d - a;
1373        Mat3::from_cols(ab, ac, ad).determinant() / 6.0
1374    }
1375
1376    /// Get the centroid of the tetrahedron.
1377    ///
1378    /// This function finds the geometric center of the tetrahedron
1379    /// by averaging the vertices: `centroid = (a + b + c + d) / 4`.
1380    #[doc(alias("center", "barycenter", "baricenter"))]
1381    #[inline(always)]
1382    pub fn centroid(&self) -> Vec3 {
1383        (self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0
1384    }
1385
1386    /// Get the triangles that form the faces of this tetrahedron.
1387    ///
1388    /// Note that the orientations of the faces are determined by that of the tetrahedron; if the
1389    /// signed volume of this tetrahedron is positive, then the triangles' normals will point
1390    /// outward, and if the signed volume is negative they will point inward.
1391    #[inline(always)]
1392    pub fn faces(&self) -> [Triangle3d; 4] {
1393        let [a, b, c, d] = self.vertices;
1394        [
1395            Triangle3d::new(b, c, d),
1396            Triangle3d::new(a, c, d).reversed(),
1397            Triangle3d::new(a, b, d),
1398            Triangle3d::new(a, b, c).reversed(),
1399        ]
1400    }
1401}
1402
1403impl Measured3d for Tetrahedron {
1404    /// Get the surface area of the tetrahedron.
1405    #[inline(always)]
1406    fn area(&self) -> f32 {
1407        let [a, b, c, d] = self.vertices;
1408        let ab = b - a;
1409        let ac = c - a;
1410        let ad = d - a;
1411        let bc = c - b;
1412        let bd = d - b;
1413        (ab.cross(ac).length()
1414            + ab.cross(ad).length()
1415            + ac.cross(ad).length()
1416            + bc.cross(bd).length())
1417            / 2.0
1418    }
1419
1420    /// Get the volume of the tetrahedron.
1421    #[inline(always)]
1422    fn volume(&self) -> f32 {
1423        ops::abs(self.signed_volume())
1424    }
1425}
1426
1427/// A 3D shape representing an extruded 2D `base_shape`.
1428///
1429/// Extruding a shape effectively "thickens" a 2D shapes,
1430/// creating a shape with the same cross-section over the entire depth.
1431///
1432/// The resulting volumes are prisms.
1433/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder.
1434#[doc(alias = "Prism")]
1435#[derive(Clone, Copy, Debug, PartialEq)]
1436#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1437pub struct Extrusion<T: Primitive2d> {
1438    /// The base shape of the extrusion
1439    pub base_shape: T,
1440    /// Half of the depth of the extrusion
1441    pub half_depth: f32,
1442}
1443impl<T: Primitive2d> Primitive3d for Extrusion<T> {}
1444
1445impl<T: Primitive2d> Extrusion<T> {
1446    /// Create a new `Extrusion<T>` from a given `base_shape` and `depth`
1447    pub fn new(base_shape: T, depth: f32) -> Self {
1448        Self {
1449            base_shape,
1450            half_depth: depth / 2.,
1451        }
1452    }
1453}
1454
1455impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {
1456    /// Get the surface area of the extrusion
1457    fn area(&self) -> f32 {
1458        2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())
1459    }
1460
1461    /// Get the volume of the extrusion
1462    fn volume(&self) -> f32 {
1463        2. * self.base_shape.area() * self.half_depth
1464    }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469    // Reference values were computed by hand and/or with external tools
1470
1471    use super::*;
1472    use crate::{InvalidDirectionError, Quat};
1473    use approx::assert_relative_eq;
1474
1475    #[test]
1476    fn direction_creation() {
1477        assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));
1478        assert_eq!(
1479            Dir3::new(Vec3::new(0.0, 0.0, 0.0)),
1480            Err(InvalidDirectionError::Zero)
1481        );
1482        assert_eq!(
1483            Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),
1484            Err(InvalidDirectionError::Infinite)
1485        );
1486        assert_eq!(
1487            Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),
1488            Err(InvalidDirectionError::Infinite)
1489        );
1490        assert_eq!(
1491            Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),
1492            Err(InvalidDirectionError::NaN)
1493        );
1494        assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));
1495
1496        // Test rotation
1497        assert!(
1498            (Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X)
1499                .abs_diff_eq(Vec3::Y, 10e-6)
1500        );
1501    }
1502
1503    #[test]
1504    fn cuboid_closest_point() {
1505        let cuboid = Cuboid::new(2.0, 2.0, 2.0);
1506        assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X);
1507        assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE);
1508        assert_eq!(
1509            cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)),
1510            Vec3::new(0.25, 0.1, 0.3)
1511        );
1512    }
1513
1514    #[test]
1515    fn sphere_closest_point() {
1516        let sphere = Sphere { radius: 1.0 };
1517        assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X);
1518        assert_eq!(
1519            sphere.closest_point(Vec3::NEG_ONE * 10.0),
1520            Vec3::NEG_ONE.normalize()
1521        );
1522        assert_eq!(
1523            sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),
1524            Vec3::new(0.25, 0.1, 0.3)
1525        );
1526    }
1527
1528    #[test]
1529    fn sphere_math() {
1530        let sphere = Sphere { radius: 4.0 };
1531        assert_eq!(sphere.diameter(), 8.0, "incorrect diameter");
1532        assert_eq!(sphere.area(), 201.06193, "incorrect area");
1533        assert_eq!(sphere.volume(), 268.08257, "incorrect volume");
1534    }
1535
1536    #[test]
1537    fn plane_from_points() {
1538        let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
1539        assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
1540        assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size");
1541        assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation");
1542    }
1543
1544    #[test]
1545    fn infinite_plane_math() {
1546        let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
1547        assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
1548        assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation");
1549
1550        let point_in_plane = Vec3::X + Vec3::Z;
1551        assert_eq!(
1552            plane.signed_distance(origin, point_in_plane),
1553            0.0,
1554            "incorrect distance"
1555        );
1556        assert_eq!(
1557            plane.project_point(origin, point_in_plane),
1558            point_in_plane,
1559            "incorrect point"
1560        );
1561
1562        let point_outside = Vec3::Y;
1563        assert_eq!(
1564            plane.signed_distance(origin, point_outside),
1565            -1.0,
1566            "incorrect distance"
1567        );
1568        assert_eq!(
1569            plane.project_point(origin, point_outside),
1570            Vec3::ZERO,
1571            "incorrect point"
1572        );
1573
1574        let point_outside = Vec3::NEG_Y;
1575        assert_eq!(
1576            plane.signed_distance(origin, point_outside),
1577            1.0,
1578            "incorrect distance"
1579        );
1580        assert_eq!(
1581            plane.project_point(origin, point_outside),
1582            Vec3::ZERO,
1583            "incorrect point"
1584        );
1585
1586        let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5;
1587        let (proj, inj) = plane.isometries_xy(origin);
1588
1589        let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO];
1590        assert_eq!(area_f(triangle), 0.5, "incorrect area");
1591
1592        let triangle_proj = triangle.map(|vec3| proj * vec3);
1593        assert_relative_eq!(area_f(triangle_proj), 0.5);
1594
1595        let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3);
1596        assert_relative_eq!(area_f(triangle_proj_inj), 0.5);
1597    }
1598
1599    #[test]
1600    fn cuboid_math() {
1601        let cuboid = Cuboid::new(3.0, 7.0, 2.0);
1602        assert_eq!(
1603            cuboid,
1604            Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)),
1605            "incorrect dimensions when created from corners"
1606        );
1607        assert_eq!(cuboid.area(), 82.0, "incorrect area");
1608        assert_eq!(cuboid.volume(), 42.0, "incorrect volume");
1609    }
1610
1611    #[test]
1612    fn cylinder_math() {
1613        let cylinder = Cylinder::new(2.0, 9.0);
1614        assert_eq!(
1615            cylinder.base(),
1616            Circle { radius: 2.0 },
1617            "base produces incorrect circle"
1618        );
1619        assert_eq!(
1620            cylinder.lateral_area(),
1621            113.097336,
1622            "incorrect lateral area"
1623        );
1624        assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area");
1625        assert_relative_eq!(cylinder.area(), 138.23007);
1626        assert_eq!(cylinder.volume(), 113.097336, "incorrect volume");
1627    }
1628
1629    #[test]
1630    fn capsule_math() {
1631        let capsule = Capsule3d::new(2.0, 9.0);
1632        assert_eq!(
1633            capsule.to_cylinder(),
1634            Cylinder::new(2.0, 9.0),
1635            "cylinder wasn't created correctly from a capsule"
1636        );
1637        assert_eq!(capsule.area(), 163.36282, "incorrect area");
1638        assert_relative_eq!(capsule.volume(), 146.60765);
1639    }
1640
1641    #[test]
1642    fn cone_math() {
1643        let cone = Cone {
1644            radius: 2.0,
1645            height: 9.0,
1646        };
1647        assert_eq!(
1648            cone.base(),
1649            Circle { radius: 2.0 },
1650            "base produces incorrect circle"
1651        );
1652        assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height");
1653        assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area");
1654        assert_eq!(cone.base_area(), 12.566371, "incorrect base area");
1655        assert_relative_eq!(cone.area(), 70.49447);
1656        assert_eq!(cone.volume(), 37.699111, "incorrect volume");
1657    }
1658
1659    #[test]
1660    fn torus_math() {
1661        let torus = Torus {
1662            minor_radius: 0.3,
1663            major_radius: 2.8,
1664        };
1665        assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius");
1666        assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius");
1667        assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind");
1668        assert_eq!(
1669            Torus::new(0.0, 1.0).kind(),
1670            TorusKind::Horn,
1671            "incorrect torus kind"
1672        );
1673        assert_eq!(
1674            Torus::new(-0.5, 1.0).kind(),
1675            TorusKind::Spindle,
1676            "incorrect torus kind"
1677        );
1678        assert_eq!(
1679            Torus::new(1.5, 1.0).kind(),
1680            TorusKind::Invalid,
1681            "torus should be invalid"
1682        );
1683        assert_relative_eq!(torus.area(), 33.16187);
1684        assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001);
1685    }
1686
1687    #[test]
1688    fn tetrahedron_math() {
1689        let tetrahedron = Tetrahedron {
1690            vertices: [
1691                Vec3::new(0.3, 1.0, 1.7),
1692                Vec3::new(-2.0, -1.0, 0.0),
1693                Vec3::new(1.8, 0.5, 1.0),
1694                Vec3::new(-1.0, -2.0, 3.5),
1695            ],
1696        };
1697        assert_eq!(tetrahedron.area(), 19.251068, "incorrect area");
1698        assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume");
1699        assert_eq!(
1700            tetrahedron.signed_volume(),
1701            3.2058334,
1702            "incorrect signed volume"
1703        );
1704        assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55));
1705
1706        assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area");
1707        assert_eq!(
1708            Tetrahedron::default().volume(),
1709            0.33333334,
1710            "incorrect volume"
1711        );
1712        assert_eq!(
1713            Tetrahedron::default().signed_volume(),
1714            -0.33333334,
1715            "incorrect signed volume"
1716        );
1717        assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);
1718    }
1719
1720    #[test]
1721    fn extrusion_math() {
1722        let circle = Circle::new(0.75);
1723        let cylinder = Extrusion::new(circle, 2.5);
1724        assert_eq!(cylinder.area(), 15.315264, "incorrect surface area");
1725        assert_eq!(cylinder.volume(), 4.417865, "incorrect volume");
1726
1727        let annulus = crate::primitives::Annulus::new(0.25, 1.375);
1728        let tube = Extrusion::new(annulus, 0.333);
1729        assert_eq!(tube.area(), 14.886437, "incorrect surface area");
1730        assert_eq!(tube.volume(), 1.9124937, "incorrect volume");
1731
1732        let polygon = crate::primitives::RegularPolygon::new(3.8, 7);
1733        let regular_prism = Extrusion::new(polygon, 1.25);
1734        assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area");
1735        assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume");
1736    }
1737
1738    #[test]
1739    fn triangle_math() {
1740        // Default triangle tests
1741        let mut default_triangle = Triangle3d::default();
1742        let reverse_default_triangle = Triangle3d::new(
1743            Vec3::new(0.5, -0.5, 0.0),
1744            Vec3::new(-0.5, -0.5, 0.0),
1745            Vec3::new(0.0, 0.5, 0.0),
1746        );
1747        assert_eq!(default_triangle.area(), 0.5, "incorrect area");
1748        assert_relative_eq!(
1749            default_triangle.perimeter(),
1750            1.0 + 2.0 * ops::sqrt(1.25_f32),
1751            epsilon = 10e-9
1752        );
1753        assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");
1754        assert!(
1755            !default_triangle.is_degenerate(),
1756            "incorrect degenerate check"
1757        );
1758        assert_eq!(
1759            default_triangle.centroid(),
1760            Vec3::new(0.0, -0.16666667, 0.0),
1761            "incorrect centroid"
1762        );
1763        assert_eq!(
1764            default_triangle.largest_side(),
1765            (Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0))
1766        );
1767        default_triangle.reverse();
1768        assert_eq!(
1769            default_triangle, reverse_default_triangle,
1770            "incorrect reverse"
1771        );
1772        assert_eq!(
1773            default_triangle.circumcenter(),
1774            Vec3::new(0.0, -0.125, 0.0),
1775            "incorrect circumcenter"
1776        );
1777
1778        // Custom triangle tests
1779        let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y);
1780        let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0));
1781        let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0));
1782
1783        assert_eq!(
1784            right_triangle.circumcenter(),
1785            Vec3::new(0.5, 0.5, 0.0),
1786            "incorrect circumcenter"
1787        );
1788        assert_eq!(
1789            obtuse_triangle.circumcenter(),
1790            Vec3::new(0.0, -4.95, 0.0),
1791            "incorrect circumcenter"
1792        );
1793        assert_eq!(
1794            acute_triangle.circumcenter(),
1795            Vec3::new(0.5, 2.475, 0.0),
1796            "incorrect circumcenter"
1797        );
1798
1799        assert!(acute_triangle.is_acute());
1800        assert!(!acute_triangle.is_obtuse());
1801        assert!(!obtuse_triangle.is_acute());
1802        assert!(obtuse_triangle.is_obtuse());
1803
1804        // Arbitrary triangle tests
1805        let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)];
1806        let triangle = Triangle3d::new(a, b, c);
1807
1808        assert!(!triangle.is_degenerate(), "incorrectly found degenerate");
1809        assert_eq!(triangle.area(), 3.0233467, "incorrect area");
1810        assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter");
1811        assert_eq!(
1812            triangle.circumcenter(),
1813            Vec3::new(-1., 1.75, 0.75),
1814            "incorrect circumcenter"
1815        );
1816        assert_eq!(
1817            triangle.normal(),
1818            Ok(Dir3::new_unchecked(Vec3::new(
1819                -0.04134491,
1820                -0.4134491,
1821                0.90958804
1822            ))),
1823            "incorrect normal"
1824        );
1825
1826        // Degenerate triangle tests
1827        let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
1828        assert!(
1829            zero_degenerate_triangle.is_degenerate(),
1830            "incorrect degenerate check"
1831        );
1832        assert_eq!(
1833            zero_degenerate_triangle.normal(),
1834            Err(InvalidDirectionError::Zero),
1835            "incorrect normal"
1836        );
1837        assert_eq!(
1838            zero_degenerate_triangle.largest_side(),
1839            (Vec3::ZERO, Vec3::ZERO),
1840            "incorrect largest side"
1841        );
1842
1843        let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
1844        assert!(
1845            dup_degenerate_triangle.is_degenerate(),
1846            "incorrect degenerate check"
1847        );
1848        assert_eq!(
1849            dup_degenerate_triangle.normal(),
1850            Err(InvalidDirectionError::Zero),
1851            "incorrect normal"
1852        );
1853        assert_eq!(
1854            dup_degenerate_triangle.largest_side(),
1855            (Vec3::ZERO, Vec3::X),
1856            "incorrect largest side"
1857        );
1858
1859        let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
1860        assert!(
1861            collinear_degenerate_triangle.is_degenerate(),
1862            "incorrect degenerate check"
1863        );
1864        assert_eq!(
1865            collinear_degenerate_triangle.normal(),
1866            Err(InvalidDirectionError::Zero),
1867            "incorrect normal"
1868        );
1869        assert_eq!(
1870            collinear_degenerate_triangle.largest_side(),
1871            (Vec3::NEG_X, Vec3::X),
1872            "incorrect largest side"
1873        );
1874    }
1875}