avian2d/collision/collider/parry/
mod.rs

1#![allow(clippy::unnecessary_cast)]
2
3pub mod contact_query;
4
5#[cfg(feature = "2d")]
6mod primitives2d;
7#[cfg(feature = "3d")]
8mod primitives3d;
9
10#[cfg(feature = "2d")]
11pub use primitives2d::{EllipseColliderShape, RegularPolygonColliderShape};
12
13use crate::{make_isometry, prelude::*};
14#[cfg(feature = "collider-from-mesh")]
15use bevy::mesh::{Indices, VertexAttributeValues};
16use bevy::{log, prelude::*};
17use contact_query::UnsupportedShape;
18use itertools::Either;
19use parry::{
20    math::Point,
21    shape::{RoundShape, SharedShape, TypedShape, Voxels},
22};
23
24impl<T: IntoCollider<Collider>> From<T> for Collider {
25    fn from(value: T) -> Self {
26        value.collider()
27    }
28}
29
30/// Parameters controlling the VHACD convex decomposition.
31///
32/// See <https://github.com/Unity-Technologies/VHACD#parameters> for details.
33#[derive(Clone, PartialEq, Debug, Reflect)]
34#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
35#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
36#[reflect(PartialEq, Debug)]
37pub struct VhacdParameters {
38    /// Maximum concavity.
39    ///
40    /// Default: 0.1 (in 2D), 0.01 (in 3D).
41    /// Valid range `[0.0, 1.0]`.
42    pub concavity: Scalar,
43    /// Controls the bias toward clipping along symmetry planes.
44    ///
45    /// Default: 0.05.
46    /// Valid Range: `[0.0, 1.0]`.
47    pub alpha: Scalar,
48    /// Controls the bias toward clipping along revolution planes.
49    ///
50    /// Default: 0.05.
51    /// Valid Range: `[0.0, 1.0]`.
52    pub beta: Scalar,
53    /// Resolution used during the voxelization stage.
54    ///
55    /// Default: 256 (in 2D), 64 (in 3D).
56    pub resolution: u32,
57    /// Controls the granularity of the search for the best
58    /// clipping plane during the decomposition.
59    ///
60    /// Default: 4
61    pub plane_downsampling: u32,
62    /// Controls the precision of the convex-hull generation
63    /// process during the clipping plane selection stage.
64    ///
65    /// Default: 4
66    pub convex_hull_downsampling: u32,
67    /// Controls the way the input mesh or polyline is being
68    /// voxelized.
69    ///
70    /// Default: `FillMode::FloodFill { detect_cavities: false, detect_self_intersections: false }`
71    pub fill_mode: FillMode,
72    /// Controls whether the convex-hull should be approximated during the decomposition stage.
73    /// Setting this to `true` increases performances with a slight degradation of the decomposition
74    /// quality.
75    ///
76    /// Default: true
77    pub convex_hull_approximation: bool,
78    /// Controls the max number of convex-hull generated by the convex decomposition.
79    ///
80    /// Default: 1024
81    pub max_convex_hulls: u32,
82}
83
84impl Default for VhacdParameters {
85    fn default() -> Self {
86        Self {
87            #[cfg(feature = "3d")]
88            resolution: 64,
89            #[cfg(feature = "3d")]
90            concavity: 0.01,
91            #[cfg(feature = "2d")]
92            resolution: 256,
93            #[cfg(feature = "2d")]
94            concavity: 0.1,
95            plane_downsampling: 4,
96            convex_hull_downsampling: 4,
97            alpha: 0.05,
98            beta: 0.05,
99            convex_hull_approximation: true,
100            max_convex_hulls: 1024,
101            fill_mode: FillMode::FloodFill {
102                detect_cavities: false,
103                #[cfg(feature = "2d")]
104                detect_self_intersections: false,
105            },
106        }
107    }
108}
109
110impl From<VhacdParameters> for parry::transformation::vhacd::VHACDParameters {
111    fn from(value: VhacdParameters) -> Self {
112        Self {
113            concavity: value.concavity,
114            alpha: value.alpha,
115            beta: value.beta,
116            resolution: value.resolution,
117            plane_downsampling: value.plane_downsampling,
118            convex_hull_downsampling: value.convex_hull_downsampling,
119            fill_mode: value.fill_mode.into(),
120            convex_hull_approximation: value.convex_hull_approximation,
121            max_convex_hulls: value.max_convex_hulls,
122        }
123    }
124}
125
126/// Controls how the voxelization determines which voxel needs
127/// to be considered empty, and which ones will be considered full.
128#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
129#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
130#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
131#[reflect(Hash, PartialEq, Debug)]
132pub enum FillMode {
133    /// Only consider full the voxels intersecting the surface of the
134    /// shape being voxelized.
135    SurfaceOnly,
136    /// Use a flood-fill technique to consider fill the voxels intersecting
137    /// the surface of the shape being voxelized, as well as all the voxels
138    /// bounded of them.
139    FloodFill {
140        /// Detects holes inside of a solid contour.
141        detect_cavities: bool,
142        /// Attempts to properly handle self-intersections.
143        #[cfg(feature = "2d")]
144        detect_self_intersections: bool,
145    },
146}
147
148impl From<FillMode> for parry::transformation::voxelization::FillMode {
149    fn from(value: FillMode) -> Self {
150        match value {
151            FillMode::SurfaceOnly => Self::SurfaceOnly,
152            FillMode::FloodFill {
153                detect_cavities,
154                #[cfg(feature = "2d")]
155                detect_self_intersections,
156            } => Self::FloodFill {
157                detect_cavities,
158                #[cfg(feature = "2d")]
159                detect_self_intersections,
160            },
161        }
162    }
163}
164
165/// Flags used for the preprocessing of a triangle mesh collider.
166#[repr(transparent)]
167#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
168#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
169#[reflect(opaque, Hash, PartialEq, Debug)]
170pub struct TrimeshFlags(u8);
171
172bitflags::bitflags! {
173    impl TrimeshFlags: u8 {
174        /// If set, the half-edge topology of the trimesh will be computed if possible.
175        const HALF_EDGE_TOPOLOGY = 0b0000_0001;
176        /// If set, the half-edge topology and connected components of the trimesh will be computed if possible.
177        ///
178        /// Because of the way it is currently implemented, connected components can only be computed on
179        /// a mesh where the half-edge topology computation succeeds. It will no longer be the case in the
180        /// future once we decouple the computations.
181        const CONNECTED_COMPONENTS = 0b0000_0010;
182        /// If set, any triangle that results in a failing half-hedge topology computation will be deleted.
183        const DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100;
184        /// If set, the trimesh will be assumed to be oriented (with outward normals).
185        ///
186        /// The pseudo-normals of its vertices and edges will be computed.
187        const ORIENTED = 0b0000_1000;
188        /// If set, the duplicate vertices of the trimesh will be merged.
189        ///
190        /// Two vertices with the exact same coordinates will share the same entry on the
191        /// vertex buffer and the index buffer is adjusted accordingly.
192        const MERGE_DUPLICATE_VERTICES = 0b0001_0000;
193        /// If set, the triangles sharing two vertices with identical index values will be removed.
194        ///
195        /// Because of the way it is currently implemented, this methods implies that duplicate
196        /// vertices will be merged. It will no longer be the case in the future once we decouple
197        /// the computations.
198        const DELETE_DEGENERATE_TRIANGLES = 0b0010_0000;
199        /// If set, two triangles sharing three vertices with identical index values (in any order) will be removed.
200        ///
201        /// Because of the way it is currently implemented, this methods implies that duplicate
202        /// vertices will be merged. It will no longer be the case in the future once we decouple
203        /// the computations.
204        const DELETE_DUPLICATE_TRIANGLES = 0b0100_0000;
205        /// If set, a special treatment will be applied to contact manifold calculation to eliminate
206        /// or fix contacts normals that could lead to incorrect bumps in physics simulation
207        /// (especially on flat surfaces).
208        ///
209        /// This is achieved by taking into account adjacent triangle normals when computing contact
210        /// points for a given triangle.
211        const FIX_INTERNAL_EDGES = 0b1000_0000 | Self::ORIENTED.bits() | Self::MERGE_DUPLICATE_VERTICES.bits();
212    }
213}
214
215impl From<TrimeshFlags> for parry::shape::TriMeshFlags {
216    fn from(value: TrimeshFlags) -> Self {
217        Self::from_bits(value.bits().into()).unwrap()
218    }
219}
220
221/// An error indicating an inconsistency when building a triangle mesh collider.
222pub type TrimeshBuilderError = parry::shape::TriMeshBuilderError;
223
224/// A collider used for detecting collisions and generating contacts.
225///
226/// # Creation
227///
228/// `Collider` has tons of methods for creating colliders of various shapes:
229///
230/// ```
231#[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
232#[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
233/// # use bevy::prelude::*;
234/// #
235/// # fn setup(mut commands: Commands) {
236/// // Create a ball collider with a given radius
237#[cfg_attr(feature = "2d", doc = "commands.spawn(Collider::circle(0.5));")]
238#[cfg_attr(feature = "3d", doc = "commands.spawn(Collider::sphere(0.5));")]
239/// // Create a capsule collider with a given radius and height
240/// commands.spawn(Collider::capsule(0.5, 2.0));
241/// # }
242/// ```
243///
244/// Colliders on their own only detect contacts and generate
245/// [collision events](crate::collision#collision-events).
246/// To make colliders apply contact forces, they have to be attached
247/// to [rigid bodies](RigidBody):
248///
249/// ```
250#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
251#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
252/// use bevy::prelude::*;
253///
254/// // Spawn a dynamic body that falls onto a static platform
255/// fn setup(mut commands: Commands) {
256///     commands.spawn((
257///         RigidBody::Dynamic,
258#[cfg_attr(feature = "2d", doc = "        Collider::circle(0.5),")]
259#[cfg_attr(feature = "3d", doc = "        Collider::sphere(0.5),")]
260///         Transform::from_xyz(0.0, 2.0, 0.0),
261///     ));
262#[cfg_attr(
263    feature = "2d",
264    doc = "    commands.spawn((RigidBody::Static, Collider::rectangle(5.0, 0.5)));"
265)]
266#[cfg_attr(
267    feature = "3d",
268    doc = "    commands.spawn((RigidBody::Static, Collider::cuboid(5.0, 0.5, 5.0)));"
269)]
270/// }
271/// ```
272///
273/// Colliders can be further configured using various components like [`Friction`], [`Restitution`],
274/// [`Sensor`], [`CollisionLayers`], [`CollisionMargin`], and [`ColliderDensity`].
275///
276/// If you need to specify the shape of the collider statically, use [`ColliderConstructor`] and build your collider
277/// with the [`Collider::try_from_constructor`] method.
278/// This can also be done automatically by simply placing the [`ColliderConstructor`] on an entity.
279///
280#[cfg_attr(
281    feature = "3d",
282    doc = "Colliders can also be generated automatically for meshes and scenes. See [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]."
283)]
284///
285/// ## Multiple Colliders
286///
287/// It can often be useful to attach multiple colliders to the same rigid body.
288///
289/// This can be done in two ways. Either use [`Collider::compound`] to have one collider that consists of many
290/// shapes, or for more control, spawn several collider entities as the children of a rigid body:
291///
292/// ```
293#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
294#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
295/// use bevy::prelude::*;
296///
297/// fn setup(mut commands: Commands) {
298///     // Spawn a rigid body with one collider on the same entity and two as children
299///     commands
300#[cfg_attr(
301    feature = "2d",
302    doc = "        .spawn((RigidBody::Dynamic, Collider::circle(0.5)))"
303)]
304#[cfg_attr(
305    feature = "3d",
306    doc = "        .spawn((RigidBody::Dynamic, Collider::sphere(0.5)))"
307)]
308///         .with_children(|children| {
309///             // Spawn the child colliders positioned relative to the rigid body
310#[cfg_attr(
311    feature = "2d",
312    doc = "            children.spawn((Collider::circle(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
313            children.spawn((Collider::circle(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
314)]
315#[cfg_attr(
316    feature = "3d",
317    doc = "            children.spawn((Collider::sphere(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
318            children.spawn((Collider::sphere(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
319)]
320///         });
321/// }
322/// ```
323///
324/// Colliders can be arbitrarily nested and transformed relative to the parent.
325/// The rigid body that a collider is attached to can be accessed using the [`ColliderOf`] component.
326///
327/// The benefit of using separate entities for the colliders is that each collider can have its own
328/// [friction](Friction), [restitution](Restitution), [collision layers](CollisionLayers),
329/// and other configuration options, and they send separate [collision events](crate::collision#collision-events).
330///
331/// # See More
332///
333/// - [Rigid bodies](RigidBody)
334/// - [Density](ColliderDensity)
335/// - [Friction] and [restitution](Restitution) (bounciness)
336/// - [Collision layers](CollisionLayers)
337/// - [Sensors](Sensor)
338/// - [Collision margins for adding extra thickness to colliders](CollisionMargin)
339#[cfg_attr(
340    feature = "3d",
341    doc = "- Generating colliders for meshes and scenes with [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]"
342)]
343/// - [Get colliding entities](CollidingEntities)
344/// - [Collision events](crate::collision#collision-events)
345/// - [Accessing collision data](Collisions)
346/// - [Filtering and modifying contacts with hooks](CollisionHooks)
347/// - [Manual contact queries](contact_query)
348///
349/// # Advanced Usage
350///
351/// Internally, `Collider` uses the shapes provided by `parry`. If you want to create a collider
352/// using these shapes, you can simply use `Collider::from(SharedShape::some_method())`.
353///
354/// To get a reference to the internal [`SharedShape`], you can use the [`Collider::shape()`]
355/// or [`Collider::shape_scaled()`] methods.
356///
357/// `Collider` is currently not `Reflect`. If you need to reflect it, you can use [`ColliderConstructor`] as a workaround.
358#[derive(Clone, Component, Debug)]
359#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
360#[require(
361    ColliderMarker,
362    ColliderAabb,
363    CollisionLayers,
364    ColliderDensity,
365    ColliderMassProperties
366)]
367pub struct Collider {
368    /// The raw unscaled collider shape.
369    shape: SharedShape,
370    /// The scaled version of the collider shape.
371    ///
372    /// If the scale is `Vector::ONE`, this will be `None` and `unscaled_shape`
373    /// will be used instead.
374    scaled_shape: SharedShape,
375    /// The global scale used for the collider shape.
376    scale: Vector,
377}
378
379impl From<SharedShape> for Collider {
380    fn from(value: SharedShape) -> Self {
381        Self {
382            shape: value.clone(),
383            scaled_shape: value,
384            scale: Vector::ONE,
385        }
386    }
387}
388
389impl Default for Collider {
390    fn default() -> Self {
391        #[cfg(feature = "2d")]
392        {
393            Self::rectangle(0.5, 0.5)
394        }
395        #[cfg(feature = "3d")]
396        {
397            Self::cuboid(0.5, 0.5, 0.5)
398        }
399    }
400}
401
402impl AnyCollider for Collider {
403    type Context = ();
404
405    fn aabb_with_context(
406        &self,
407        position: Vector,
408        rotation: impl Into<Rotation>,
409        _: AabbContext<Self::Context>,
410    ) -> ColliderAabb {
411        let aabb = self
412            .shape_scaled()
413            .compute_aabb(&make_isometry(position, rotation));
414        ColliderAabb {
415            min: aabb.mins.into(),
416            max: aabb.maxs.into(),
417        }
418    }
419
420    fn contact_manifolds_with_context(
421        &self,
422        other: &Self,
423        position1: Vector,
424        rotation1: impl Into<Rotation>,
425        position2: Vector,
426        rotation2: impl Into<Rotation>,
427        prediction_distance: Scalar,
428        manifolds: &mut Vec<ContactManifold>,
429        _: ContactManifoldContext<Self::Context>,
430    ) {
431        contact_query::contact_manifolds(
432            self,
433            position1,
434            rotation1,
435            other,
436            position2,
437            rotation2,
438            prediction_distance,
439            manifolds,
440        )
441    }
442}
443
444// TODO: `bevy_heavy` supports computing the individual mass properties efficiently for Bevy's primitive shapes,
445//       but Parry doesn't support it for its own shapes, so we have to compute all mass properties in each method :(
446#[cfg(feature = "2d")]
447impl ComputeMassProperties for Collider {
448    fn mass(&self, density: f32) -> f32 {
449        let props = self.shape_scaled().mass_properties(density as Scalar);
450        props.mass() as f32
451    }
452
453    fn unit_angular_inertia(&self) -> f32 {
454        self.angular_inertia(1.0)
455    }
456
457    fn angular_inertia(&self, mass: f32) -> f32 {
458        let props = self.shape_scaled().mass_properties(mass as Scalar);
459        props.principal_inertia() as f32
460    }
461
462    fn center_of_mass(&self) -> Vec2 {
463        let props = self.shape_scaled().mass_properties(1.0);
464        Vector::from(props.local_com).f32()
465    }
466
467    fn mass_properties(&self, density: f32) -> MassProperties {
468        let props = self.shape_scaled().mass_properties(density as Scalar);
469
470        MassProperties {
471            mass: props.mass() as f32,
472            #[cfg(feature = "2d")]
473            angular_inertia: props.principal_inertia() as f32,
474            #[cfg(feature = "3d")]
475            principal_angular_inertia: Vector::from(props.principal_inertia()).f32(),
476            #[cfg(feature = "3d")]
477            local_inertial_frame: Quaternion::from(props.principal_inertia_local_frame).f32(),
478            center_of_mass: Vector::from(props.local_com).f32(),
479        }
480    }
481}
482
483#[cfg(feature = "3d")]
484impl ComputeMassProperties for Collider {
485    fn mass(&self, density: f32) -> f32 {
486        let props = self.shape_scaled().mass_properties(density as Scalar);
487        props.mass() as f32
488    }
489
490    fn unit_principal_angular_inertia(&self) -> Vec3 {
491        self.principal_angular_inertia(1.0)
492    }
493
494    fn principal_angular_inertia(&self, mass: f32) -> Vec3 {
495        let props = self.shape_scaled().mass_properties(mass as Scalar);
496        Vector::from(props.principal_inertia()).f32()
497    }
498
499    fn local_inertial_frame(&self) -> Quat {
500        let props = self.shape_scaled().mass_properties(1.0);
501        Quaternion::from(props.principal_inertia_local_frame).f32()
502    }
503
504    fn center_of_mass(&self) -> Vec3 {
505        let props = self.shape_scaled().mass_properties(1.0);
506        Vector::from(props.local_com).f32()
507    }
508
509    fn mass_properties(&self, density: f32) -> MassProperties {
510        let props = self.shape_scaled().mass_properties(density as Scalar);
511
512        MassProperties {
513            mass: props.mass() as f32,
514            #[cfg(feature = "2d")]
515            angular_inertia: props.principal_inertia() as f32,
516            #[cfg(feature = "3d")]
517            principal_angular_inertia: Vector::from(props.principal_inertia()).f32(),
518            #[cfg(feature = "3d")]
519            local_inertial_frame: Quaternion::from(props.principal_inertia_local_frame).f32(),
520            center_of_mass: Vector::from(props.local_com).f32(),
521        }
522    }
523}
524
525impl ScalableCollider for Collider {
526    fn scale(&self) -> Vector {
527        self.scale()
528    }
529
530    fn set_scale(&mut self, scale: Vector, detail: u32) {
531        self.set_scale(scale, detail)
532    }
533}
534
535impl Collider {
536    /// Returns the raw unscaled shape of the collider.
537    pub fn shape(&self) -> &SharedShape {
538        &self.shape
539    }
540
541    /// Returns a mutable reference to the raw unscaled shape of the collider.
542    pub fn shape_mut(&mut self) -> &mut SharedShape {
543        &mut self.shape
544    }
545
546    /// Returns the shape of the collider with the scale from its `GlobalTransform` applied.
547    pub fn shape_scaled(&self) -> &SharedShape {
548        &self.scaled_shape
549    }
550
551    /// Sets the unscaled shape of the collider. The collider's scale will be applied to this shape.
552    pub fn set_shape(&mut self, shape: SharedShape) {
553        self.shape = shape;
554
555        // TODO: The number of subdivisions probably shouldn't be hard-coded
556        if let Ok(scaled) = scale_shape(&self.shape, self.scale, 10) {
557            self.scaled_shape = scaled;
558        } else {
559            log::error!("Failed to create convex hull for scaled collider.");
560        }
561    }
562
563    /// Returns the global scale of the collider.
564    pub fn scale(&self) -> Vector {
565        self.scale
566    }
567
568    /// Set the global scaling factor of this shape.
569    ///
570    /// If the scaling factor is not uniform, and the scaled shape can’t be
571    /// represented as a supported shape, the shape is approximated as
572    /// a convex polygon or polyhedron using `num_subdivisions`.
573    ///
574    /// For example, if a ball was scaled to an ellipse, the new shape would be approximated.
575    pub fn set_scale(&mut self, scale: Vector, num_subdivisions: u32) {
576        if scale == self.scale {
577            return;
578        }
579
580        if scale == Vector::ONE {
581            // Trivial case.
582            self.scaled_shape = self.shape.clone();
583            self.scale = Vector::ONE;
584            return;
585        }
586
587        if let Ok(scaled) = scale_shape(&self.shape, scale, num_subdivisions) {
588            self.scaled_shape = scaled;
589            self.scale = scale;
590        } else {
591            log::error!("Failed to create convex hull for scaled collider.");
592        }
593    }
594
595    /// Projects the given `point` onto `self` transformed by `translation` and `rotation`.
596    /// The returned tuple contains the projected point and whether it is inside the collider.
597    ///
598    /// If `solid` is true and the given `point` is inside of the collider, the projection will be at the point.
599    /// Otherwise, the collider will be treated as hollow, and the projection will be at the collider's boundary.
600    pub fn project_point(
601        &self,
602        translation: impl Into<Position>,
603        rotation: impl Into<Rotation>,
604        point: Vector,
605        solid: bool,
606    ) -> (Vector, bool) {
607        let projection = self.shape_scaled().project_point(
608            &make_isometry(translation, rotation),
609            &point.into(),
610            solid,
611        );
612        (projection.point.into(), projection.is_inside)
613    }
614
615    /// Computes the minimum distance between the given `point` and `self` transformed by `translation` and `rotation`.
616    ///
617    /// If `solid` is true and the given `point` is inside of the collider, the returned distance will be `0.0`.
618    /// Otherwise, the collider will be treated as hollow, and the distance will be the distance
619    /// to the collider's boundary.
620    pub fn distance_to_point(
621        &self,
622        translation: impl Into<Position>,
623        rotation: impl Into<Rotation>,
624        point: Vector,
625        solid: bool,
626    ) -> Scalar {
627        self.shape_scaled().distance_to_point(
628            &make_isometry(translation, rotation),
629            &point.into(),
630            solid,
631        )
632    }
633
634    /// Tests whether the given `point` is inside of `self` transformed by `translation` and `rotation`.
635    pub fn contains_point(
636        &self,
637        translation: impl Into<Position>,
638        rotation: impl Into<Rotation>,
639        point: Vector,
640    ) -> bool {
641        self.shape_scaled()
642            .contains_point(&make_isometry(translation, rotation), &point.into())
643    }
644
645    /// Computes the distance and normal between the given ray and `self`
646    /// transformed by `translation` and `rotation`.
647    ///
648    /// The returned tuple is in the format `(distance, normal)`.
649    ///
650    /// # Arguments
651    ///
652    /// - `ray_origin`: Where the ray is cast from.
653    /// - `ray_direction`: What direction the ray is cast in.
654    /// - `max_distance`: The maximum distance the ray can travel.
655    /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself.
656    ///   Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary.
657    pub fn cast_ray(
658        &self,
659        translation: impl Into<Position>,
660        rotation: impl Into<Rotation>,
661        ray_origin: Vector,
662        ray_direction: Vector,
663        max_distance: Scalar,
664        solid: bool,
665    ) -> Option<(Scalar, Vector)> {
666        let hit = self.shape_scaled().cast_ray_and_get_normal(
667            &make_isometry(translation, rotation),
668            &parry::query::Ray::new(ray_origin.into(), ray_direction.into()),
669            max_distance,
670            solid,
671        );
672        hit.map(|hit| (hit.time_of_impact, hit.normal.into()))
673    }
674
675    /// Tests whether the given ray intersects `self` transformed by `translation` and `rotation`.
676    ///
677    /// # Arguments
678    ///
679    /// - `ray_origin`: Where the ray is cast from.
680    /// - `ray_direction`: What direction the ray is cast in.
681    /// - `max_distance`: The maximum distance the ray can travel.
682    pub fn intersects_ray(
683        &self,
684        translation: impl Into<Position>,
685        rotation: impl Into<Rotation>,
686        ray_origin: Vector,
687        ray_direction: Vector,
688        max_distance: Scalar,
689    ) -> bool {
690        self.shape_scaled().intersects_ray(
691            &make_isometry(translation, rotation),
692            &parry::query::Ray::new(ray_origin.into(), ray_direction.into()),
693            max_distance,
694        )
695    }
696
697    /// Creates a collider with a compound shape defined by a given vector of colliders with a position and a rotation.
698    ///
699    /// Especially for dynamic rigid bodies, compound shape colliders should be preferred over triangle meshes and polylines,
700    /// because convex shapes typically provide more reliable results.
701    ///
702    /// If you want to create a compound shape from a 3D triangle mesh or 2D polyline, consider using the
703    /// [`Collider::convex_decomposition`] method.
704    pub fn compound(
705        shapes: Vec<(
706            impl Into<Position>,
707            impl Into<Rotation>,
708            impl Into<Collider>,
709        )>,
710    ) -> Self {
711        let shapes = shapes
712            .into_iter()
713            .map(|(p, r, c)| {
714                (
715                    make_isometry(*p.into(), r.into()),
716                    c.into().shape_scaled().clone(),
717                )
718            })
719            .collect::<Vec<_>>();
720        SharedShape::compound(shapes).into()
721    }
722
723    /// Creates a collider with a circle shape defined by its radius.
724    #[cfg(feature = "2d")]
725    pub fn circle(radius: Scalar) -> Self {
726        SharedShape::ball(radius).into()
727    }
728
729    /// Creates a collider with a sphere shape defined by its radius.
730    #[cfg(feature = "3d")]
731    pub fn sphere(radius: Scalar) -> Self {
732        SharedShape::ball(radius).into()
733    }
734
735    /// Creates a collider with an ellipse shape defined by a half-width and half-height.
736    #[cfg(feature = "2d")]
737    pub fn ellipse(half_width: Scalar, half_height: Scalar) -> Self {
738        SharedShape::new(EllipseColliderShape(Ellipse::new(
739            half_width as f32,
740            half_height as f32,
741        )))
742        .into()
743    }
744
745    /// Creates a collider with a rectangle shape defined by its extents.
746    #[cfg(feature = "2d")]
747    pub fn rectangle(x_length: Scalar, y_length: Scalar) -> Self {
748        SharedShape::cuboid(x_length * 0.5, y_length * 0.5).into()
749    }
750
751    /// Creates a collider with a cuboid shape defined by its extents.
752    #[cfg(feature = "3d")]
753    pub fn cuboid(x_length: Scalar, y_length: Scalar, z_length: Scalar) -> Self {
754        SharedShape::cuboid(x_length * 0.5, y_length * 0.5, z_length * 0.5).into()
755    }
756
757    /// Creates a collider with a rectangle shape defined by its extents and rounded corners.
758    #[cfg(feature = "2d")]
759    pub fn round_rectangle(x_length: Scalar, y_length: Scalar, border_radius: Scalar) -> Self {
760        SharedShape::round_cuboid(x_length * 0.5, y_length * 0.5, border_radius).into()
761    }
762
763    /// Creates a collider with a cuboid shape defined by its extents and rounded corners.
764    #[cfg(feature = "3d")]
765    pub fn round_cuboid(
766        x_length: Scalar,
767        y_length: Scalar,
768        z_length: Scalar,
769        border_radius: Scalar,
770    ) -> Self {
771        SharedShape::round_cuboid(
772            x_length * 0.5,
773            y_length * 0.5,
774            z_length * 0.5,
775            border_radius,
776        )
777        .into()
778    }
779
780    /// Creates a collider with a cylinder shape defined by its radius
781    /// on the `XZ` plane and its height along the `Y` axis.
782    #[cfg(feature = "3d")]
783    pub fn cylinder(radius: Scalar, height: Scalar) -> Self {
784        SharedShape::cylinder(height * 0.5, radius).into()
785    }
786
787    /// Creates a collider with a cone shape defined by the radius of its base
788    /// on the `XZ` plane and its height along the `Y` axis.
789    #[cfg(feature = "3d")]
790    pub fn cone(radius: Scalar, height: Scalar) -> Self {
791        SharedShape::cone(height * 0.5, radius).into()
792    }
793
794    /// Creates a collider with a capsule shape defined by its radius
795    /// and its height along the `Y` axis, excluding the hemispheres.
796    pub fn capsule(radius: Scalar, length: Scalar) -> Self {
797        SharedShape::capsule(
798            (Vector::Y * length * 0.5).into(),
799            (Vector::NEG_Y * length * 0.5).into(),
800            radius,
801        )
802        .into()
803    }
804
805    /// Creates a collider with a capsule shape defined by its radius and endpoints `a` and `b`.
806    pub fn capsule_endpoints(radius: Scalar, a: Vector, b: Vector) -> Self {
807        SharedShape::capsule(a.into(), b.into(), radius).into()
808    }
809
810    /// Creates a collider with a [half-space](https://en.wikipedia.org/wiki/Half-space_(geometry)) shape
811    /// defined by the outward normal of its planar boundary.
812    pub fn half_space(outward_normal: Vector) -> Self {
813        SharedShape::halfspace(nalgebra::Unit::new_normalize(outward_normal.into())).into()
814    }
815
816    /// Creates a collider with a segment shape defined by its endpoints `a` and `b`.
817    pub fn segment(a: Vector, b: Vector) -> Self {
818        SharedShape::segment(a.into(), b.into()).into()
819    }
820
821    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
822    ///
823    /// If the triangle is oriented clockwise, it will be reversed to be counterclockwise
824    /// by swapping `b` and `c`. This is needed for collision detection.
825    ///
826    /// If you know that the given points produce a counterclockwise triangle,
827    /// consider using [`Collider::triangle_unchecked`] instead.
828    #[cfg(feature = "2d")]
829    pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
830        let mut triangle = parry::shape::Triangle::new(a.into(), b.into(), c.into());
831
832        // Make sure the triangle is counterclockwise. This is needed for collision detection.
833        if triangle.orientation(1e-8) == parry::shape::TriangleOrientation::Clockwise {
834            triangle.reverse();
835        }
836
837        SharedShape::new(triangle).into()
838    }
839
840    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
841    ///
842    /// The orientation of the triangle is assumed to be counterclockwise.
843    /// This is needed for collision detection.
844    ///
845    /// If you are unsure about the orientation of the triangle, consider using [`Collider::triangle`] instead.
846    #[cfg(feature = "2d")]
847    pub fn triangle_unchecked(a: Vector, b: Vector, c: Vector) -> Self {
848        SharedShape::triangle(a.into(), b.into(), c.into()).into()
849    }
850
851    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
852    #[cfg(feature = "3d")]
853    pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
854        SharedShape::triangle(a.into(), b.into(), c.into()).into()
855    }
856
857    /// Creates a collider with a regular polygon shape defined by the circumradius and the number of sides.
858    #[cfg(feature = "2d")]
859    pub fn regular_polygon(circumradius: f32, sides: u32) -> Self {
860        RegularPolygon::new(circumradius, sides).collider()
861    }
862
863    /// Creates a collider with a polyline shape defined by its vertices and optionally an index buffer.
864    pub fn polyline(vertices: Vec<Vector>, indices: Option<Vec<[u32; 2]>>) -> Self {
865        let vertices = vertices.into_iter().map(|v| v.into()).collect();
866        SharedShape::polyline(vertices, indices).into()
867    }
868
869    /// Creates a collider with a triangle mesh shape defined by its vertex and index buffers.
870    ///
871    /// Note that the resulting collider will be hollow and have no interior.
872    /// This makes it more prone to tunneling and other collision issues.
873    ///
874    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
875    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
876    ///
877    /// # Panics
878    ///
879    /// Panics if the given vertex and index buffers do not contain any triangles,
880    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
881    pub fn trimesh(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
882        Self::try_trimesh(vertices, indices)
883            .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
884    }
885
886    /// Tries to create a collider with a triangle mesh shape defined by its vertex and index buffers.
887    ///
888    /// Note that the resulting collider will be hollow and have no interior.
889    /// This makes it more prone to tunneling and other collision issues.
890    ///
891    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
892    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
893    ///
894    /// # Errors
895    ///
896    /// Returns a [`TrimeshBuilderError`] if the given vertex and index buffers do not contain any triangles,
897    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
898    pub fn try_trimesh(
899        vertices: Vec<Vector>,
900        indices: Vec<[u32; 3]>,
901    ) -> Result<Self, TrimeshBuilderError> {
902        let vertices = vertices.into_iter().map(|v| v.into()).collect();
903        SharedShape::trimesh(vertices, indices).map(|trimesh| trimesh.into())
904    }
905
906    /// Creates a collider with a triangle mesh shape defined by its vertex and index buffers
907    /// and flags controlling the preprocessing.
908    ///
909    /// Note that the resulting collider will be hollow and have no interior.
910    /// This makes it more prone to tunneling and other collision issues.
911    ///
912    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
913    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
914    ///
915    /// # Panics
916    ///
917    /// Panics if after preprocessing the given vertex and index buffers do not contain any triangles,
918    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
919    pub fn trimesh_with_config(
920        vertices: Vec<Vector>,
921        indices: Vec<[u32; 3]>,
922        flags: TrimeshFlags,
923    ) -> Self {
924        Self::try_trimesh_with_config(vertices, indices, flags)
925            .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
926    }
927
928    /// Tries to create a collider with a triangle mesh shape defined by its vertex and index buffers
929    /// and flags controlling the preprocessing.
930    ///
931    /// Note that the resulting collider will be hollow and have no interior.
932    /// This makes it more prone to tunneling and other collision issues.
933    ///
934    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
935    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
936    ///
937    /// # Errors
938    ///
939    /// Returns a [`TrimeshBuilderError`] if after preprocessing the given vertex and index buffers do not contain any triangles,
940    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
941    pub fn try_trimesh_with_config(
942        vertices: Vec<Vector>,
943        indices: Vec<[u32; 3]>,
944        flags: TrimeshFlags,
945    ) -> Result<Self, TrimeshBuilderError> {
946        let vertices = vertices.into_iter().map(|v| v.into()).collect();
947        SharedShape::trimesh_with_flags(vertices, indices, flags.into())
948            .map(|trimesh| trimesh.into())
949    }
950
951    /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline
952    /// defined by its vertex and index buffers.
953    #[cfg(feature = "2d")]
954    pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 2]>) -> Self {
955        let vertices = vertices.iter().map(|v| (*v).into()).collect::<Vec<_>>();
956        SharedShape::convex_decomposition(&vertices, &indices).into()
957    }
958
959    /// Creates a collider shape with a compound shape obtained from the decomposition of a given trimesh
960    /// defined by its vertex and index buffers.
961    #[cfg(feature = "3d")]
962    pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
963        let vertices = vertices.iter().map(|v| (*v).into()).collect::<Vec<_>>();
964        SharedShape::convex_decomposition(&vertices, &indices).into()
965    }
966
967    /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline
968    /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring
969    /// the decomposition process.
970    #[cfg(feature = "2d")]
971    pub fn convex_decomposition_with_config(
972        vertices: Vec<Vector>,
973        indices: Vec<[u32; 2]>,
974        params: &VhacdParameters,
975    ) -> Self {
976        let vertices = vertices.iter().map(|v| (*v).into()).collect::<Vec<_>>();
977        SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.clone().into())
978            .into()
979    }
980
981    /// Creates a collider shape with a compound shape obtained from the decomposition of a given trimesh
982    /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring
983    /// the decomposition process.
984    #[cfg(feature = "3d")]
985    pub fn convex_decomposition_with_config(
986        vertices: Vec<Vector>,
987        indices: Vec<[u32; 3]>,
988        params: VhacdParameters,
989    ) -> Self {
990        let vertices = vertices.iter().map(|v| (*v).into()).collect::<Vec<_>>();
991        SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.clone().into())
992            .into()
993    }
994
995    /// Creates a collider with a [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) shape obtained after computing
996    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points.
997    #[cfg(feature = "2d")]
998    pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
999        let points = points.iter().map(|v| (*v).into()).collect::<Vec<_>>();
1000        SharedShape::convex_hull(&points).map(Into::into)
1001    }
1002
1003    /// Creates a collider with a [convex polyhedron](https://en.wikipedia.org/wiki/Convex_polytope) shape obtained after computing
1004    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points.
1005    #[cfg(feature = "3d")]
1006    pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
1007        let points = points.iter().map(|v| (*v).into()).collect::<Vec<_>>();
1008        SharedShape::convex_hull(&points).map(Into::into)
1009    }
1010
1011    /// Creates a collider with a [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) shape **without** computing
1012    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points: convexity of the input is
1013    /// assumed and not checked.
1014    #[cfg(feature = "2d")]
1015    pub fn convex_polyline(points: Vec<Vector>) -> Option<Self> {
1016        let points = points.iter().map(|v| (*v).into()).collect::<Vec<_>>();
1017        SharedShape::convex_polyline(points).map(Into::into)
1018    }
1019
1020    /// Creates a collider shape made of voxels.
1021    ///
1022    /// Each voxel has the size `voxel_size` and grid coordinate given by `grid_coordinates`.
1023    pub fn voxels(voxel_size: Vector, grid_coordinates: &[IVector]) -> Self {
1024        let shape = Voxels::new(
1025            voxel_size.into(),
1026            &Self::ivec_array_from_point_int_array(grid_coordinates),
1027        );
1028        SharedShape::new(shape).into()
1029    }
1030
1031    /// Creates a collider shape made of voxels.
1032    ///
1033    /// Each voxel has the size `voxel_size` and contains at least one point from `points`.
1034    pub fn voxels_from_points(voxel_size: Vector, points: &[Vector]) -> Self {
1035        SharedShape::voxels_from_points(
1036            voxel_size.into(),
1037            &Self::vec_array_from_point_float_array(points),
1038        )
1039        .into()
1040    }
1041
1042    /// Creates a voxel collider obtained from the decomposition of the given polyline into voxelized convex parts.
1043    #[cfg(feature = "2d")]
1044    pub fn voxelized_polyline(
1045        vertices: &[Vector],
1046        indices: &[[u32; 2]],
1047        voxel_size: Scalar,
1048        fill_mode: FillMode,
1049    ) -> Self {
1050        let vertices = Self::vec_array_from_point_float_array(vertices);
1051        SharedShape::voxelized_mesh(&vertices, indices, voxel_size, fill_mode.into()).into()
1052    }
1053
1054    /// Creates a voxel collider obtained from the decomposition of the given trimesh into voxelized convex parts.
1055    #[cfg(feature = "3d")]
1056    pub fn voxelized_trimesh(
1057        vertices: &[Vector],
1058        indices: &[[u32; 3]],
1059        voxel_size: Scalar,
1060        fill_mode: FillMode,
1061    ) -> Self {
1062        let vertices = Self::vec_array_from_point_float_array(vertices);
1063        SharedShape::voxelized_mesh(&vertices, indices, voxel_size, fill_mode.into()).into()
1064    }
1065
1066    /// Creates a voxel collider obtained from the decomposition of the given `Mesh` into voxelized convex parts.
1067    ///
1068    /// This method is only available if the `collider-from-mesh` feature is enabled.
1069    #[cfg(feature = "collider-from-mesh")]
1070    pub fn voxelized_trimesh_from_mesh(
1071        mesh: &Mesh,
1072        voxel_size: Scalar,
1073        fill_mode: FillMode,
1074    ) -> Option<Self> {
1075        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1076            SharedShape::voxelized_mesh(&vertices, &indices, voxel_size, fill_mode.into()).into()
1077        })
1078    }
1079
1080    #[cfg_attr(
1081        feature = "2d",
1082        doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1083    )]
1084    #[cfg_attr(
1085        feature = "3d",
1086        doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1087    )]
1088    pub fn voxelized_convex_decomposition(
1089        vertices: &[Vector],
1090        indices: &[[u32; DIM]],
1091    ) -> Vec<Self> {
1092        Self::voxelized_convex_decomposition_with_config(
1093            vertices,
1094            indices,
1095            &VhacdParameters::default(),
1096        )
1097    }
1098
1099    #[cfg_attr(
1100        feature = "2d",
1101        doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1102    )]
1103    #[cfg_attr(
1104        feature = "3d",
1105        doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1106    )]
1107    pub fn voxelized_convex_decomposition_with_config(
1108        vertices: &[Vector],
1109        indices: &[[u32; DIM]],
1110        parameters: &VhacdParameters,
1111    ) -> Vec<Self> {
1112        SharedShape::voxelized_convex_decomposition_with_params(
1113            &Self::vec_array_from_point_float_array(vertices),
1114            indices,
1115            &parameters.clone().into(),
1116        )
1117        .into_iter()
1118        .map(|c| c.into())
1119        .collect()
1120    }
1121
1122    fn ivec_array_from_point_int_array(points: &[IVector]) -> Vec<Point<i32>> {
1123        points
1124            .iter()
1125            .map(|p| {
1126                #[cfg(feature = "2d")]
1127                return Point::new(p.x, p.y);
1128                #[cfg(feature = "3d")]
1129                return Point::new(p.x, p.y, p.z);
1130            })
1131            .collect::<Vec<_>>()
1132    }
1133
1134    fn vec_array_from_point_float_array(points: &[Vector]) -> Vec<Point<Scalar>> {
1135        points
1136            .iter()
1137            .map(|p| {
1138                #[cfg(feature = "2d")]
1139                return Point::new(p.x, p.y);
1140                #[cfg(feature = "3d")]
1141                return Point::new(p.x, p.y, p.z);
1142            })
1143            .collect::<Vec<_>>()
1144    }
1145
1146    /// Creates a collider with a heightfield shape.
1147    ///
1148    /// A 2D heightfield is a segment along the `X` axis, subdivided at regular intervals.
1149    ///
1150    /// `heights` is a list indicating the altitude of each subdivision point, and `scale` controls
1151    /// the scaling factor along each axis.
1152    #[cfg(feature = "2d")]
1153    pub fn heightfield(heights: Vec<Scalar>, scale: Vector) -> Self {
1154        SharedShape::heightfield(heights.into(), scale.into()).into()
1155    }
1156
1157    /// Creates a collider with a heightfield shape.
1158    ///
1159    /// A 3D heightfield is a rectangle on the `XZ` plane, subdivided in a grid pattern at regular intervals.
1160    ///
1161    /// `heights` is a matrix indicating the altitude of each subdivision point. The number of rows indicates
1162    /// the number of subdivisions along the `X` axis, while the number of columns indicates the number of
1163    /// subdivisions along the `Z` axis.
1164    ///
1165    /// `scale` controls the scaling factor along each axis.
1166    #[cfg(feature = "3d")]
1167    pub fn heightfield(heights: Vec<Vec<Scalar>>, scale: Vector) -> Self {
1168        let row_count = heights.len();
1169        let column_count = heights[0].len();
1170        let data: Vec<Scalar> = heights.into_iter().flatten().collect();
1171
1172        assert_eq!(
1173            data.len(),
1174            row_count * column_count,
1175            "Each row in `heights` must have the same amount of points"
1176        );
1177
1178        let heights = nalgebra::DMatrix::from_vec(row_count, column_count, data);
1179        SharedShape::heightfield(heights, scale.into()).into()
1180    }
1181
1182    /// Creates a collider with a triangle mesh shape from a `Mesh`.
1183    ///
1184    /// Note that the resulting collider will be hollow and have no interior.
1185    /// This makes it more prone to tunneling and other collision issues.
1186    ///
1187    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
1188    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
1189    ///
1190    /// # Example
1191    ///
1192    /// ```
1193    /// use avian3d::prelude::*;
1194    /// use bevy::prelude::*;
1195    ///
1196    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1197    ///     let mesh = Mesh::from(Cuboid::default());
1198    ///     commands.spawn((
1199    ///         Collider::trimesh_from_mesh(&mesh).unwrap(),
1200    ///         Mesh3d(meshes.add(mesh)),
1201    ///     ));
1202    /// }
1203    /// ```
1204    #[cfg(feature = "collider-from-mesh")]
1205    pub fn trimesh_from_mesh(mesh: &Mesh) -> Option<Self> {
1206        extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1207            SharedShape::trimesh_with_flags(
1208                vertices,
1209                indices,
1210                TrimeshFlags::MERGE_DUPLICATE_VERTICES.into(),
1211            )
1212            .map(|trimesh| trimesh.into())
1213            .ok()
1214        })
1215    }
1216
1217    /// Creates a collider with a triangle mesh shape from a `Mesh` using the given [`TrimeshFlags`]
1218    /// for controlling the preprocessing.
1219    ///
1220    /// Note that the resulting collider will be hollow and have no interior.
1221    /// This makes it more prone to tunneling and other collision issues.
1222    ///
1223    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
1224    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
1225    ///
1226    /// # Example
1227    ///
1228    /// ```
1229    /// use avian3d::prelude::*;
1230    /// use bevy::prelude::*;
1231    ///
1232    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1233    ///     let mesh = Mesh::from(Cuboid::default());
1234    ///     commands.spawn((
1235    ///         Collider::trimesh_from_mesh_with_config(&mesh, TrimeshFlags::all()).unwrap(),
1236    ///         Mesh3d(meshes.add(mesh)),
1237    ///     ));
1238    /// }
1239    /// ```
1240    #[cfg(feature = "collider-from-mesh")]
1241    pub fn trimesh_from_mesh_with_config(mesh: &Mesh, flags: TrimeshFlags) -> Option<Self> {
1242        extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1243            SharedShape::trimesh_with_flags(vertices, indices, flags.into())
1244                .map(|trimesh| trimesh.into())
1245                .ok()
1246        })
1247    }
1248
1249    /// Creates a collider with a convex polygon shape obtained from the convex hull of a `Mesh`.
1250    ///
1251    /// # Example
1252    ///
1253    /// ```
1254    /// use avian3d::prelude::*;
1255    /// use bevy::prelude::*;
1256    ///
1257    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1258    ///     let mesh = Mesh::from(Cuboid::default());
1259    ///     commands.spawn((
1260    ///         Collider::convex_hull_from_mesh(&mesh).unwrap(),
1261    ///         Mesh3d(meshes.add(mesh)),
1262    ///     ));
1263    /// }
1264    /// ```
1265    #[cfg(feature = "collider-from-mesh")]
1266    pub fn convex_hull_from_mesh(mesh: &Mesh) -> Option<Self> {
1267        extract_mesh_vertices_indices(mesh)
1268            .and_then(|(vertices, _)| SharedShape::convex_hull(&vertices).map(|shape| shape.into()))
1269    }
1270
1271    /// Creates a compound shape obtained from the decomposition of a `Mesh`.
1272    ///
1273    /// # Example
1274    ///
1275    /// ```
1276    /// use avian3d::prelude::*;
1277    /// use bevy::prelude::*;
1278    ///
1279    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1280    ///     let mesh = Mesh::from(Cuboid::default());
1281    ///     commands.spawn((
1282    ///         Collider::convex_decomposition_from_mesh(&mesh).unwrap(),
1283    ///         Mesh3d(meshes.add(mesh)),
1284    ///     ));
1285    /// }
1286    /// ```
1287    #[cfg(feature = "collider-from-mesh")]
1288    pub fn convex_decomposition_from_mesh(mesh: &Mesh) -> Option<Self> {
1289        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1290            SharedShape::convex_decomposition(&vertices, &indices).into()
1291        })
1292    }
1293
1294    /// Creates a compound shape obtained from the decomposition of a `Mesh`
1295    /// with the given [`VhacdParameters`] passed to the decomposition algorithm.
1296    ///
1297    /// # Example
1298    ///
1299    /// ```
1300    /// use avian3d::prelude::*;
1301    /// use bevy::prelude::*;
1302    ///
1303    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1304    ///     let mesh = Mesh::from(Cuboid::default());
1305    ///     let config = VhacdParameters {
1306    ///         convex_hull_approximation: false,
1307    ///         ..default()
1308    ///     };
1309    ///     commands.spawn((
1310    ///         Collider::convex_decomposition_from_mesh_with_config(&mesh, &config).unwrap(),
1311    ///         Mesh3d(meshes.add(mesh)),
1312    ///     ));
1313    /// }
1314    /// ```
1315    #[cfg(feature = "collider-from-mesh")]
1316    pub fn convex_decomposition_from_mesh_with_config(
1317        mesh: &Mesh,
1318        parameters: &VhacdParameters,
1319    ) -> Option<Self> {
1320        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1321            SharedShape::convex_decomposition_with_params(
1322                &vertices,
1323                &indices,
1324                &parameters.clone().into(),
1325            )
1326            .into()
1327        })
1328    }
1329
1330    /// Attempts to create a collider with the given [`ColliderConstructor`].
1331    /// By using this, you can serialize and deserialize the collider's creation method
1332    /// separately from the collider itself via the [`ColliderConstructor`] enum.
1333    ///
1334    #[cfg_attr(
1335        feature = "collider-from-mesh",
1336        doc = "Returns `None` in the following cases:
1337- The given [`ColliderConstructor`] requires a mesh, but none was provided.
1338- Creating the collider from the given [`ColliderConstructor`] failed."
1339    )]
1340    #[cfg_attr(
1341        not(feature = "collider-from-mesh"),
1342        doc = "Returns `None` if creating the collider from the given [`ColliderConstructor`] failed."
1343    )]
1344    pub fn try_from_constructor(
1345        collider_constructor: ColliderConstructor,
1346        #[cfg(feature = "collider-from-mesh")] mesh: Option<&Mesh>,
1347    ) -> Option<Self> {
1348        match collider_constructor {
1349            #[cfg(feature = "2d")]
1350            ColliderConstructor::Circle { radius } => Some(Self::circle(radius)),
1351            #[cfg(feature = "3d")]
1352            ColliderConstructor::Sphere { radius } => Some(Self::sphere(radius)),
1353            #[cfg(feature = "2d")]
1354            ColliderConstructor::Ellipse {
1355                half_width,
1356                half_height,
1357            } => Some(Self::ellipse(half_width, half_height)),
1358            #[cfg(feature = "2d")]
1359            ColliderConstructor::Rectangle { x_length, y_length } => {
1360                Some(Self::rectangle(x_length, y_length))
1361            }
1362            #[cfg(feature = "3d")]
1363            ColliderConstructor::Cuboid {
1364                x_length,
1365                y_length,
1366                z_length,
1367            } => Some(Self::cuboid(x_length, y_length, z_length)),
1368            #[cfg(feature = "2d")]
1369            ColliderConstructor::RoundRectangle {
1370                x_length,
1371                y_length,
1372                border_radius,
1373            } => Some(Self::round_rectangle(x_length, y_length, border_radius)),
1374            #[cfg(feature = "3d")]
1375            ColliderConstructor::RoundCuboid {
1376                x_length,
1377                y_length,
1378                z_length,
1379                border_radius,
1380            } => Some(Self::round_cuboid(
1381                x_length,
1382                y_length,
1383                z_length,
1384                border_radius,
1385            )),
1386            #[cfg(feature = "3d")]
1387            ColliderConstructor::Cylinder { radius, height } => {
1388                Some(Self::cylinder(radius, height))
1389            }
1390            #[cfg(feature = "3d")]
1391            ColliderConstructor::Cone { radius, height } => Some(Self::cone(radius, height)),
1392            ColliderConstructor::Capsule { radius, height } => Some(Self::capsule(radius, height)),
1393            ColliderConstructor::CapsuleEndpoints { radius, a, b } => {
1394                Some(Self::capsule_endpoints(radius, a, b))
1395            }
1396            ColliderConstructor::HalfSpace { outward_normal } => {
1397                Some(Self::half_space(outward_normal))
1398            }
1399            ColliderConstructor::Segment { a, b } => Some(Self::segment(a, b)),
1400            ColliderConstructor::Triangle { a, b, c } => Some(Self::triangle(a, b, c)),
1401            #[cfg(feature = "2d")]
1402            ColliderConstructor::RegularPolygon {
1403                circumradius,
1404                sides,
1405            } => Some(Self::regular_polygon(circumradius, sides)),
1406            ColliderConstructor::Polyline { vertices, indices } => {
1407                Some(Self::polyline(vertices, indices))
1408            }
1409            ColliderConstructor::Trimesh { vertices, indices } => {
1410                Some(Self::trimesh(vertices, indices))
1411            }
1412            ColliderConstructor::TrimeshWithConfig {
1413                vertices,
1414                indices,
1415                flags,
1416            } => Some(Self::trimesh_with_config(vertices, indices, flags)),
1417            #[cfg(feature = "2d")]
1418            ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1419                Some(Self::convex_decomposition(vertices, indices))
1420            }
1421            #[cfg(feature = "3d")]
1422            ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1423                Some(Self::convex_decomposition(vertices, indices))
1424            }
1425            #[cfg(feature = "2d")]
1426            ColliderConstructor::ConvexDecompositionWithConfig {
1427                vertices,
1428                indices,
1429                params,
1430            } => Some(Self::convex_decomposition_with_config(
1431                vertices, indices, &params,
1432            )),
1433            #[cfg(feature = "3d")]
1434            ColliderConstructor::ConvexDecompositionWithConfig {
1435                vertices,
1436                indices,
1437                params,
1438            } => Some(Self::convex_decomposition_with_config(
1439                vertices, indices, params,
1440            )),
1441            #[cfg(feature = "2d")]
1442            ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1443            #[cfg(feature = "3d")]
1444            ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1445            #[cfg(feature = "2d")]
1446            ColliderConstructor::ConvexPolyline { points } => Self::convex_polyline(points),
1447            ColliderConstructor::Voxels {
1448                voxel_size,
1449                grid_coordinates,
1450            } => Some(Self::voxels(voxel_size, &grid_coordinates)),
1451            #[cfg(feature = "2d")]
1452            ColliderConstructor::VoxelizedPolyline {
1453                vertices,
1454                indices,
1455                voxel_size,
1456                fill_mode,
1457            } => Some(Self::voxelized_polyline(
1458                &vertices, &indices, voxel_size, fill_mode,
1459            )),
1460            #[cfg(feature = "3d")]
1461            ColliderConstructor::VoxelizedTrimesh {
1462                vertices,
1463                indices,
1464                voxel_size,
1465                fill_mode,
1466            } => Some(Self::voxelized_trimesh(
1467                &vertices, &indices, voxel_size, fill_mode,
1468            )),
1469            #[cfg(feature = "2d")]
1470            ColliderConstructor::Heightfield { heights, scale } => {
1471                Some(Self::heightfield(heights, scale))
1472            }
1473            #[cfg(feature = "3d")]
1474            ColliderConstructor::Heightfield { heights, scale } => {
1475                Some(Self::heightfield(heights, scale))
1476            }
1477            #[cfg(feature = "collider-from-mesh")]
1478            ColliderConstructor::TrimeshFromMesh => Self::trimesh_from_mesh(mesh?),
1479            #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1480            ColliderConstructor::TrimeshFromMeshWithConfig(flags) => {
1481                Self::trimesh_from_mesh_with_config(mesh?, flags)
1482            }
1483            #[cfg(feature = "collider-from-mesh")]
1484            ColliderConstructor::ConvexDecompositionFromMesh => {
1485                Self::convex_decomposition_from_mesh(mesh?)
1486            }
1487            #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1488            ColliderConstructor::ConvexDecompositionFromMeshWithConfig(params) => {
1489                Self::convex_decomposition_from_mesh_with_config(mesh?, &params)
1490            }
1491            #[cfg(feature = "collider-from-mesh")]
1492            ColliderConstructor::ConvexHullFromMesh => Self::convex_hull_from_mesh(mesh?),
1493            #[cfg(feature = "collider-from-mesh")]
1494            ColliderConstructor::VoxelizedTrimeshFromMesh {
1495                voxel_size,
1496                fill_mode,
1497            } => Self::voxelized_trimesh_from_mesh(mesh?, voxel_size, fill_mode),
1498            ColliderConstructor::Compound(compound_constructors) => {
1499                let shapes: Vec<_> =
1500                    ColliderConstructor::flatten_compound_constructors(compound_constructors)
1501                        .into_iter()
1502                        .filter_map(|(position, rotation, collider_constructor)| {
1503                            Self::try_from_constructor(
1504                                collider_constructor,
1505                                #[cfg(feature = "collider-from-mesh")]
1506                                mesh,
1507                            )
1508                            .map(|collider| (position, rotation, collider))
1509                        })
1510                        .collect();
1511
1512                (!shapes.is_empty()).then(|| Self::compound(shapes))
1513            }
1514        }
1515    }
1516}
1517
1518#[cfg(feature = "collider-from-mesh")]
1519type VerticesIndices = (Vec<nalgebra::Point3<Scalar>>, Vec<[u32; 3]>);
1520
1521#[cfg(feature = "collider-from-mesh")]
1522fn extract_mesh_vertices_indices(mesh: &Mesh) -> Option<VerticesIndices> {
1523    let vertices = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?;
1524    let indices = mesh.indices()?;
1525
1526    let vtx: Vec<_> = match vertices {
1527        VertexAttributeValues::Float32(vtx) => Some(
1528            vtx.chunks(3)
1529                .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1530                .collect(),
1531        ),
1532        VertexAttributeValues::Float32x3(vtx) => Some(
1533            vtx.iter()
1534                .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1535                .collect(),
1536        ),
1537        _ => None,
1538    }?;
1539
1540    let idx = match indices {
1541        Indices::U16(idx) => idx
1542            .chunks_exact(3)
1543            .map(|i| [i[0] as u32, i[1] as u32, i[2] as u32])
1544            .collect(),
1545        Indices::U32(idx) => idx.chunks_exact(3).map(|i| [i[0], i[1], i[2]]).collect(),
1546    };
1547
1548    Some((vtx, idx))
1549}
1550
1551fn scale_shape(
1552    shape: &SharedShape,
1553    scale: Vector,
1554    num_subdivisions: u32,
1555) -> Result<SharedShape, UnsupportedShape> {
1556    let scale = scale.abs();
1557    match shape.as_typed_shape() {
1558        TypedShape::Cuboid(s) => Ok(SharedShape::new(s.scaled(&scale.abs().into()))),
1559        TypedShape::RoundCuboid(s) => Ok(SharedShape::new(RoundShape {
1560            border_radius: s.border_radius,
1561            inner_shape: s.inner_shape.scaled(&scale.abs().into()),
1562        })),
1563        TypedShape::Capsule(c) => match c.scaled(&scale.abs().into(), num_subdivisions) {
1564            None => {
1565                log::error!("Failed to apply scale {} to Capsule shape.", scale);
1566                Ok(SharedShape::ball(0.0))
1567            }
1568            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1569            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1570        },
1571        TypedShape::Ball(b) => {
1572            #[cfg(feature = "2d")]
1573            {
1574                if scale.x == scale.y {
1575                    Ok(SharedShape::ball(b.radius * scale.x.abs()))
1576                } else {
1577                    // A 2D circle becomes an ellipse when scaled non-uniformly.
1578                    Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1579                        half_size: Vec2::splat(b.radius as f32) * scale.f32().abs(),
1580                    })))
1581                }
1582            }
1583            #[cfg(feature = "3d")]
1584            match b.scaled(&scale.abs().into(), num_subdivisions) {
1585                None => {
1586                    log::error!("Failed to apply scale {} to Ball shape.", scale);
1587                    Ok(SharedShape::ball(0.0))
1588                }
1589                Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1590                Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1591            }
1592        }
1593        TypedShape::Segment(s) => Ok(SharedShape::new(s.scaled(&scale.into()))),
1594        TypedShape::Triangle(t) => Ok(SharedShape::new(t.scaled(&scale.into()))),
1595        TypedShape::RoundTriangle(t) => Ok(SharedShape::new(RoundShape {
1596            border_radius: t.border_radius,
1597            inner_shape: t.inner_shape.scaled(&scale.into()),
1598        })),
1599        TypedShape::TriMesh(t) => Ok(SharedShape::new(t.clone().scaled(&scale.into()))),
1600        TypedShape::Polyline(p) => Ok(SharedShape::new(p.clone().scaled(&scale.into()))),
1601        TypedShape::HalfSpace(h) => match h.scaled(&scale.into()) {
1602            None => {
1603                log::error!("Failed to apply scale {} to HalfSpace shape.", scale);
1604                Ok(SharedShape::ball(0.0))
1605            }
1606            Some(scaled) => Ok(SharedShape::new(scaled)),
1607        },
1608        TypedShape::Voxels(v) => Ok(SharedShape::new(v.clone().scaled(&scale.into()))),
1609        TypedShape::HeightField(h) => Ok(SharedShape::new(h.clone().scaled(&scale.into()))),
1610        #[cfg(feature = "2d")]
1611        TypedShape::ConvexPolygon(cp) => match cp.clone().scaled(&scale.into()) {
1612            None => {
1613                log::error!("Failed to apply scale {} to ConvexPolygon shape.", scale);
1614                Ok(SharedShape::ball(0.0))
1615            }
1616            Some(scaled) => Ok(SharedShape::new(scaled)),
1617        },
1618        #[cfg(feature = "2d")]
1619        TypedShape::RoundConvexPolygon(cp) => match cp.inner_shape.clone().scaled(&scale.into()) {
1620            None => {
1621                log::error!(
1622                    "Failed to apply scale {} to RoundConvexPolygon shape.",
1623                    scale
1624                );
1625                Ok(SharedShape::ball(0.0))
1626            }
1627            Some(scaled) => Ok(SharedShape::new(RoundShape {
1628                border_radius: cp.border_radius,
1629                inner_shape: scaled,
1630            })),
1631        },
1632        #[cfg(feature = "3d")]
1633        TypedShape::ConvexPolyhedron(cp) => match cp.clone().scaled(&scale.into()) {
1634            None => {
1635                log::error!("Failed to apply scale {} to ConvexPolyhedron shape.", scale);
1636                Ok(SharedShape::ball(0.0))
1637            }
1638            Some(scaled) => Ok(SharedShape::new(scaled)),
1639        },
1640        #[cfg(feature = "3d")]
1641        TypedShape::RoundConvexPolyhedron(cp) => {
1642            match cp.clone().inner_shape.scaled(&scale.into()) {
1643                None => {
1644                    log::error!(
1645                        "Failed to apply scale {} to RoundConvexPolyhedron shape.",
1646                        scale
1647                    );
1648                    Ok(SharedShape::ball(0.0))
1649                }
1650                Some(scaled) => Ok(SharedShape::new(RoundShape {
1651                    border_radius: cp.border_radius,
1652                    inner_shape: scaled,
1653                })),
1654            }
1655        }
1656        #[cfg(feature = "3d")]
1657        TypedShape::Cylinder(c) => match c.scaled(&scale.abs().into(), num_subdivisions) {
1658            None => {
1659                log::error!("Failed to apply scale {} to Cylinder shape.", scale);
1660                Ok(SharedShape::ball(0.0))
1661            }
1662            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1663            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1664        },
1665        #[cfg(feature = "3d")]
1666        TypedShape::RoundCylinder(c) => {
1667            match c.inner_shape.scaled(&scale.abs().into(), num_subdivisions) {
1668                None => {
1669                    log::error!("Failed to apply scale {} to RoundCylinder shape.", scale);
1670                    Ok(SharedShape::ball(0.0))
1671                }
1672                Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1673                    border_radius: c.border_radius,
1674                    inner_shape: scaled,
1675                })),
1676                Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1677                    border_radius: c.border_radius,
1678                    inner_shape: scaled,
1679                })),
1680            }
1681        }
1682        #[cfg(feature = "3d")]
1683        TypedShape::Cone(c) => match c.scaled(&scale.into(), num_subdivisions) {
1684            None => {
1685                log::error!("Failed to apply scale {} to Cone shape.", scale);
1686                Ok(SharedShape::ball(0.0))
1687            }
1688            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1689            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1690        },
1691        #[cfg(feature = "3d")]
1692        TypedShape::RoundCone(c) => match c.inner_shape.scaled(&scale.into(), num_subdivisions) {
1693            None => {
1694                log::error!("Failed to apply scale {} to RoundCone shape.", scale);
1695                Ok(SharedShape::ball(0.0))
1696            }
1697            Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1698                border_radius: c.border_radius,
1699                inner_shape: scaled,
1700            })),
1701            Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1702                border_radius: c.border_radius,
1703                inner_shape: scaled,
1704            })),
1705        },
1706        TypedShape::Compound(c) => {
1707            let mut scaled = Vec::with_capacity(c.shapes().len());
1708
1709            for (iso, shape) in c.shapes() {
1710                scaled.push((
1711                    #[cfg(feature = "2d")]
1712                    make_isometry(
1713                        Vector::from(iso.translation) * scale,
1714                        Rotation::radians(iso.rotation.angle()),
1715                    ),
1716                    #[cfg(feature = "3d")]
1717                    make_isometry(
1718                        Vector::from(iso.translation) * scale,
1719                        Quaternion::from(iso.rotation),
1720                    ),
1721                    scale_shape(shape, scale, num_subdivisions)?,
1722                ));
1723            }
1724            Ok(SharedShape::compound(scaled))
1725        }
1726        TypedShape::Custom(_shape) => {
1727            #[cfg(feature = "2d")]
1728            {
1729                if let Some(ellipse) = _shape.as_shape::<EllipseColliderShape>() {
1730                    return Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1731                        half_size: ellipse.half_size * scale.f32().abs(),
1732                    })));
1733                }
1734                if let Some(polygon) = _shape.as_shape::<RegularPolygonColliderShape>() {
1735                    if scale.x == scale.y {
1736                        return Ok(SharedShape::new(RegularPolygonColliderShape(
1737                            RegularPolygon::new(
1738                                polygon.circumradius() * scale.x.abs() as f32,
1739                                polygon.sides,
1740                            ),
1741                        )));
1742                    } else {
1743                        let vertices = polygon
1744                            .vertices(0.0)
1745                            .into_iter()
1746                            .map(|v| v.adjust_precision().into())
1747                            .collect::<Vec<_>>();
1748
1749                        return scale_shape(
1750                            &SharedShape::convex_hull(&vertices).unwrap(),
1751                            scale,
1752                            num_subdivisions,
1753                        );
1754                    }
1755                }
1756            }
1757            Err(parry::query::Unsupported)
1758        }
1759    }
1760}
1761
1762#[cfg(all(test, feature = "3d"))]
1763mod tests {
1764    use super::*;
1765    use approx::assert_relative_eq;
1766
1767    #[test]
1768    fn test_flatten_compound_constructors() {
1769        let input = vec![
1770            (
1771                Position(Vector::new(10.0, 0.0, 0.0)),
1772                Rotation::default(),
1773                ColliderConstructor::Sphere { radius: 1.0 },
1774            ),
1775            (
1776                Position(Vector::new(5.0, 0.0, 0.0)),
1777                Rotation::from(Quaternion::from_rotation_z(PI / 2.0)),
1778                ColliderConstructor::Compound(vec![
1779                    (
1780                        Position(Vector::new(2.0, 0.0, 0.0)),
1781                        Rotation::from(Quaternion::from_rotation_y(PI)),
1782                        ColliderConstructor::Compound(vec![(
1783                            Position(Vector::new(1.0, 0.0, 0.0)),
1784                            Rotation::default(),
1785                            ColliderConstructor::Sphere { radius: 0.5 },
1786                        )]),
1787                    ),
1788                    (
1789                        Position(Vector::new(0.0, 3.0, 0.0)),
1790                        Rotation::default(),
1791                        ColliderConstructor::Sphere { radius: 0.25 },
1792                    ),
1793                ]),
1794            ),
1795        ];
1796
1797        let flattened = ColliderConstructor::flatten_compound_constructors(input);
1798        assert_eq!(flattened.len(), 3);
1799
1800        let unchanged_simple_sphere = &flattened[0];
1801        let flattened_grandchild = &flattened[1];
1802        let flattened_sibling = &flattened[2];
1803
1804        // Top level colliders should remain unchanged
1805        assert_eq!(
1806            unchanged_simple_sphere.0,
1807            Position(Vector::new(10.0, 0.0, 0.0))
1808        );
1809        assert_eq!(unchanged_simple_sphere.1, Rotation::default());
1810
1811        // Grandchild local position: (1, 0, 0)
1812        // 1. Apply parent's 180 Y rotation -> (-1, 0, 0)
1813        // 2. Add parent's position (2, 0, 0) -> (1, 0, 0)
1814        // 3. Apply grandparent's 90 Z rotation -> (0, 1, 0)
1815        // 4. Add grandparent's position (5, 0, 0) -> (5, 1, 0)
1816        let expected_grandchild_world_pos = Vector::new(5.0, 1.0, 0.0);
1817        let actual_grandchild_world_pos = flattened_grandchild.0.0;
1818
1819        assert_relative_eq!(
1820            actual_grandchild_world_pos.x,
1821            expected_grandchild_world_pos.x,
1822            epsilon = 1e-6
1823        );
1824        assert_relative_eq!(
1825            actual_grandchild_world_pos.y,
1826            expected_grandchild_world_pos.y,
1827            epsilon = 1e-6
1828        );
1829        assert_relative_eq!(
1830            actual_grandchild_world_pos.z,
1831            expected_grandchild_world_pos.z,
1832            epsilon = 1e-6
1833        );
1834
1835        // Sibling local position: (0, 3, 0)
1836        // 1. Apply parent's 90 Z rotation -> (-3, 0, 0)
1837        // 2. Add parent's position (5, 0, 0) -> (2, 0, 0)
1838        let expected_sibling_world_pos = Vector::new(2.0, 0.0, 0.0);
1839        let actual_sibling_world_pos = flattened_sibling.0.0;
1840
1841        assert_relative_eq!(
1842            actual_sibling_world_pos.x,
1843            expected_sibling_world_pos.x,
1844            epsilon = 1e-6
1845        );
1846        assert_relative_eq!(
1847            actual_sibling_world_pos.y,
1848            expected_sibling_world_pos.y,
1849            epsilon = 1e-6
1850        );
1851        assert_relative_eq!(
1852            actual_sibling_world_pos.z,
1853            expected_sibling_world_pos.z,
1854            epsilon = 1e-6
1855        );
1856    }
1857}