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