bevy_math/primitives/
dim3.rs

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