bevy_math/primitives/
dim3.rs

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