bevy_math/primitives/
dim3.rs

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