avian3d/collision/collider/
constructor.rs

1use core::iter::once;
2
3use crate::prelude::*;
4use bevy::{platform::collections::HashMap, prelude::*};
5use itertools::Either;
6
7/// A component that will automatically generate [`Collider`]s on its descendants at runtime.
8/// The type of the generated collider can be specified using [`ColliderConstructor`].
9/// This supports computing the shape dynamically from the mesh, in which case only the descendants
10/// with a [`Mesh`] will have colliders generated.
11///
12/// In contrast to [`ColliderConstructor`], this component will *not* generate a collider on its own entity.
13///
14/// If this component is used on a scene, such as one spawned by a [`SceneRoot`], it will
15/// wait until the scene is loaded before generating colliders. Note that this requires
16/// the `bevy_scene` feature to be enabled.
17///
18/// The exact configuration for each descendant can be specified using the helper methods
19/// such as [`with_constructor_for_name`](Self::with_constructor_for_name).
20///
21/// This component will only override a pre-existing [`Collider`] component on a descendant entity
22/// when it has been explicitly mentioned in the `config`.
23///
24/// # See Also
25///
26/// For inserting colliders on the same entity, use [`ColliderConstructor`].
27///
28/// # Caveats
29///
30/// When a component has multiple ancestors with [`ColliderConstructorHierarchy`], the insertion order is undefined.
31///
32/// # Example
33///
34/// Below are some examples of using [`ColliderConstructorHierarchy`] to generate colliders
35/// for a glTF scene at runtime. Note that this requires the `bevy_scene` feature to be enabled.
36///
37/// ```
38#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
39#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
40/// use bevy::prelude::*;
41///
42/// fn setup(mut commands: Commands, mut assets: ResMut<AssetServer>) {
43///     let scene = assets.load("my_model.gltf#Scene0");
44///
45#[cfg_attr(
46    feature = "2d",
47    doc = "    // Spawn the scene and automatically generate circle colliders"
48)]
49#[cfg_attr(
50    feature = "3d",
51    doc = "    // Spawn the scene and automatically generate triangle mesh colliders"
52)]
53///     commands.spawn((
54///         SceneRoot(scene.clone()),
55#[cfg_attr(
56    feature = "2d",
57    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 }),"
58)]
59#[cfg_attr(
60    feature = "3d",
61    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh),"
62)]
63///     ));
64///
65///     // Specify configuration for specific meshes by name
66///     commands.spawn((
67///         SceneRoot(scene.clone()),
68#[cfg_attr(
69    feature = "2d",
70    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
71            .with_constructor_for_name(\"Tree\", ColliderConstructor::Rectangle { x_length: 1.0, y_length: 2.0 })"
72)]
73#[cfg_attr(
74    feature = "3d",
75    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh)
76            .with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh)"
77)]
78///             .with_layers_for_name("Tree", CollisionLayers::from_bits(0b0010, 0b1111))
79///             .with_density_for_name("Tree", 2.5),
80///     ));
81///
82///     // Only generate colliders for specific meshes by name
83///     commands.spawn((
84///         SceneRoot(scene.clone()),
85///         ColliderConstructorHierarchy::new(None)
86#[cfg_attr(
87    feature = "2d",
88    doc = "            .with_constructor_for_name(\"Tree\", ColliderConstructor::Circle { radius: 2.0 }),"
89)]
90#[cfg_attr(
91    feature = "3d",
92    doc = "            .with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh),"
93)]
94///     ));
95///
96///     // Generate colliders for everything except specific meshes by name
97///     commands.spawn((
98///         SceneRoot(scene),
99#[cfg_attr(
100    feature = "2d",
101    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
102            .without_constructor_for_name(\"Tree\"),"
103)]
104#[cfg_attr(
105    feature = "3d",
106    doc = "        ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMeshWithConfig(
107             TrimeshFlags::MERGE_DUPLICATE_VERTICES
108        ))
109        .without_constructor_for_name(\"Tree\"),"
110)]
111///     ));
112/// }
113/// ```
114#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
115#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
117#[reflect(Component, Debug, PartialEq, Default)]
118pub struct ColliderConstructorHierarchy {
119    /// The default collider type used for each entity that isn't included in [`config`](Self::config).
120    /// If `None`, all entities except the ones in [`config`](Self::config) will be skipped.
121    pub default_constructor: Option<ColliderConstructor>,
122    /// The default [`CollisionLayers`] used for colliders in the hierarchy.
123    ///
124    /// [`CollisionLayers::default()`] by default, with the first layer and all filters.
125    pub default_layers: CollisionLayers,
126    /// The default [`ColliderDensity`] used for colliders in the hierarchy.
127    ///
128    /// `1.0` by default.
129    pub default_density: ColliderDensity,
130    /// Specifies data like the [`ColliderConstructor`] and [`CollisionLayers`] for entities
131    /// in the hierarchy by `Name`. Entries with a `None` value will be skipped.
132    ///
133    /// For the entities not found in this `HashMap`, [`default_constructor`](Self::default_constructor),
134    /// [`default_layers`](Self::default_layers), and [`default_density`](Self::default_density) will be used instead.
135    pub config: HashMap<String, Option<ColliderConstructorHierarchyConfig>>,
136}
137
138/// Triggered when a [`ColliderConstructor`] successfully inserted a [`Collider`].
139///
140/// The event is not triggered when the [`ColliderConstructor`] failed to construct the [`Collider`]
141/// or when there was already a [`Collider`] on the entity.
142#[derive(EntityEvent, Clone, Copy, Debug, PartialEq)]
143#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
144pub struct ColliderConstructorReady {
145    /// The entity that held the [`ColliderConstructor`].
146    pub entity: Entity,
147}
148
149/// Triggered when a [`ColliderConstructorHierarchy`] finished inserting all its [`Collider`]s.
150///
151/// Note that the event will still be triggered when when the hierarchy had no colliders to insert
152/// or failed to insert all of them, so this event is not a guarantee that there are actually
153/// any colliders in the scene.
154#[derive(EntityEvent, Clone, Copy, Debug, PartialEq)]
155#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
156pub struct ColliderConstructorHierarchyReady {
157    /// The entity that held the [`ColliderConstructorHierarchy`].
158    pub entity: Entity,
159}
160
161impl ColliderConstructorHierarchy {
162    /// Creates a new [`ColliderConstructorHierarchy`] with the default [`ColliderConstructor`] used for
163    /// generating colliders set to the given `default_constructor`.
164    ///
165    /// If the given constructor type is `None`, collider generation is skipped
166    /// for all entities in the hierarchy except the ones in [`config`](Self::config).
167    ///
168    /// Collider constructors can be specified for individual entities using
169    /// [`with_constructor_for_name`](Self::with_constructor_for_name).
170    pub fn new(default_constructor: impl Into<Option<ColliderConstructor>>) -> Self {
171        Self {
172            default_constructor: default_constructor.into(),
173            default_layers: CollisionLayers::default(),
174            default_density: ColliderDensity(1.0),
175            config: default(),
176        }
177    }
178
179    /// Specifies the default [`CollisionLayers`] used for colliders not included in [`ColliderConstructorHierarchy::config`].
180    pub fn with_default_layers(mut self, layers: CollisionLayers) -> Self {
181        self.default_layers = layers;
182        self
183    }
184
185    /// Specifies the default [`ColliderDensity`] used for colliders not included in [`ColliderConstructorHierarchy::config`].
186    pub fn with_default_density(mut self, density: impl Into<ColliderDensity>) -> Self {
187        self.default_density = density.into();
188        self
189    }
190
191    /// Specifies the [`ColliderConstructor`] used for an entity with the given `name`.
192    pub fn with_constructor_for_name(
193        mut self,
194        name: &str,
195        constructor: ColliderConstructor,
196    ) -> Self {
197        if let Some(Some(data)) = self.config.get_mut(name) {
198            data.constructor = Some(constructor);
199        } else {
200            self.config.insert(
201                name.to_string(),
202                Some(ColliderConstructorHierarchyConfig {
203                    constructor: Some(constructor),
204                    ..default()
205                }),
206            );
207        }
208        self
209    }
210
211    /// Specifies the [`CollisionLayers`] used for an entity with the given `name`.
212    pub fn with_layers_for_name(self, name: &str, layers: CollisionLayers) -> Self {
213        self.with_config_for_name(name, |config| config.layers = Some(layers))
214    }
215
216    /// Specifies the [`ColliderDensity`] used for an entity with the given `name`.
217    pub fn with_density_for_name(self, name: &str, density: impl Into<ColliderDensity>) -> Self {
218        let density = density.into();
219        self.with_config_for_name(name, |config| config.density = Some(density))
220    }
221
222    /// Sets the [`ColliderConstructor`] for the entity associated with the given `name` to `None`,
223    /// skipping collider generation for it.
224    pub fn without_constructor_for_name(mut self, name: &str) -> Self {
225        self.config.insert(name.to_string(), None);
226        self
227    }
228
229    fn with_config_for_name(
230        mut self,
231        name: &str,
232        mut mutate_config: impl FnMut(&mut ColliderConstructorHierarchyConfig),
233    ) -> Self {
234        if let Some(Some(config)) = self.config.get_mut(name) {
235            mutate_config(config);
236        } else {
237            let mut config = ColliderConstructorHierarchyConfig::default();
238            mutate_config(&mut config);
239            self.config.insert(name.to_string(), Some(config));
240        }
241        self
242    }
243}
244
245/// Configuration for a specific collider generated from a scene using [`ColliderConstructorHierarchy`].
246#[derive(Clone, Debug, Default, PartialEq, Reflect)]
247#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
248#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
249#[reflect(Debug, Default, PartialEq)]
250pub struct ColliderConstructorHierarchyConfig {
251    /// The type of collider generated for the mesh.
252    ///
253    /// If `None`, [`ColliderConstructorHierarchy::default_constructor`] is used instead.
254    pub constructor: Option<ColliderConstructor>,
255    /// The [`CollisionLayers`] used for this collider.
256    ///
257    /// If `None`, [`ColliderConstructorHierarchy::default_layers`] is used instead.
258    pub layers: Option<CollisionLayers>,
259    /// The [`ColliderDensity`] used for this collider.
260    ///
261    /// If `None`, [`ColliderConstructorHierarchy::default_density`] is used instead.
262    pub density: Option<ColliderDensity>,
263}
264
265/// A component that will automatically generate a [`Collider`] at runtime using [`Collider::try_from_constructor`].
266/// Enabling the `collider-from-mesh` feature activates support for computing the shape dynamically from the mesh attached to the same entity.
267///
268/// Since [`Collider`] is not [`Reflect`], you can use this type to statically specify a collider's shape instead.
269///
270/// This component will never override a pre-existing [`Collider`] component on the same entity.
271///
272/// # See Also
273///
274/// For inserting colliders on an entity's descendants, use [`ColliderConstructorHierarchy`].
275///
276/// # Panics
277///
278/// The system handling the generation of colliders will panic if the specified [`ColliderConstructor`]
279/// requires a mesh, but the entity does not have a `Handle<Mesh>` component.
280///
281/// # Example
282///
283/// ```
284#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
285#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
286/// use bevy::prelude::*;
287///
288/// fn setup(mut commands: Commands, mut assets: ResMut<AssetServer>, mut meshes: Assets<Mesh>) {
289#[cfg_attr(feature = "2d", doc = "     // Spawn a circle with radius 2")]
290#[cfg_attr(
291    feature = "3d",
292    doc = "    // Spawn a cube with a convex hull collider generated from the mesh"
293)]
294///     commands.spawn((
295#[cfg_attr(
296    feature = "2d",
297    doc = "        ColliderConstructor::Circle { radius: 2.0 },"
298)]
299#[cfg_attr(
300    feature = "3d",
301    doc = "        ColliderConstructor::ConvexHullFromMesh,"
302)]
303///         Mesh3d(meshes.add(Cuboid::default())),
304///     ));
305/// }
306/// ```
307#[derive(Clone, Debug, PartialEq, Reflect, Component)]
308#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
309#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
310#[cfg_attr(feature = "collider-from-mesh", derive(Default))]
311#[cfg_attr(feature = "collider-from-mesh", reflect(Default))]
312#[reflect(Debug, Component, PartialEq)]
313#[reflect(no_field_bounds)]
314#[non_exhaustive]
315#[allow(missing_docs)]
316pub enum ColliderConstructor {
317    /// Constructs a collider with [`Collider::circle`].
318    #[cfg(feature = "2d")]
319    Circle { radius: Scalar },
320    /// Constructs a collider with [`Collider::sphere`].
321    #[cfg(feature = "3d")]
322    Sphere { radius: Scalar },
323    /// Constructs a collider with [`Collider::ellipse`].
324    #[cfg(feature = "2d")]
325    Ellipse {
326        half_width: Scalar,
327        half_height: Scalar,
328    },
329    /// Constructs a collider with [`Collider::rectangle`].
330    #[cfg(feature = "2d")]
331    Rectangle { x_length: Scalar, y_length: Scalar },
332    /// Constructs a collider with [`Collider::cuboid`].
333    #[cfg(feature = "3d")]
334    Cuboid {
335        x_length: Scalar,
336        y_length: Scalar,
337        z_length: Scalar,
338    },
339    /// Constructs a collider with [`Collider::round_rectangle`].
340    #[cfg(feature = "2d")]
341    RoundRectangle {
342        x_length: Scalar,
343        y_length: Scalar,
344        border_radius: Scalar,
345    },
346    /// Constructs a collider with [`Collider::round_cuboid`].
347    #[cfg(feature = "3d")]
348    RoundCuboid {
349        x_length: Scalar,
350        y_length: Scalar,
351        z_length: Scalar,
352        border_radius: Scalar,
353    },
354    /// Constructs a collider with [`Collider::cylinder`].
355    #[cfg(feature = "3d")]
356    Cylinder { radius: Scalar, height: Scalar },
357    /// Constructs a collider with [`Collider::cone`].
358    #[cfg(feature = "3d")]
359    Cone { radius: Scalar, height: Scalar },
360    /// Constructs a collider with [`Collider::capsule`].
361    Capsule { radius: Scalar, height: Scalar },
362    /// Constructs a collider with [`Collider::capsule_endpoints`].
363    CapsuleEndpoints {
364        radius: Scalar,
365        a: Vector,
366        b: Vector,
367    },
368    /// Constructs a collider with [`Collider::half_space`].
369    HalfSpace { outward_normal: Vector },
370    /// Constructs a collider with [`Collider::segment`].
371    Segment { a: Vector, b: Vector },
372    /// Constructs a collider with [`Collider::triangle`].
373    Triangle { a: Vector, b: Vector, c: Vector },
374    /// Constructs a collider with [`Collider::regular_polygon`].
375    #[cfg(feature = "2d")]
376    RegularPolygon { circumradius: f32, sides: u32 },
377    /// Constructs a collider with [`Collider::polyline`].
378    Polyline {
379        vertices: Vec<Vector>,
380        indices: Option<Vec<[u32; 2]>>,
381    },
382    /// Constructs a collider with [`Collider::trimesh`].
383    Trimesh {
384        vertices: Vec<Vector>,
385        indices: Vec<[u32; 3]>,
386    },
387    /// Constructs a collider with [`Collider::trimesh_with_config`].
388    TrimeshWithConfig {
389        vertices: Vec<Vector>,
390        indices: Vec<[u32; 3]>,
391        flags: TrimeshFlags,
392    },
393    /// Constructs a collider with [`Collider::convex_decomposition`].
394    #[cfg(feature = "2d")]
395    ConvexDecomposition {
396        vertices: Vec<Vector>,
397        indices: Vec<[u32; 2]>,
398    },
399    /// Constructs a collider with [`Collider::convex_decomposition`].
400    #[cfg(feature = "3d")]
401    ConvexDecomposition {
402        vertices: Vec<Vector>,
403        indices: Vec<[u32; 3]>,
404    },
405    /// Constructs a collider with [`Collider::convex_decomposition_with_config`].
406    #[cfg(feature = "2d")]
407    ConvexDecompositionWithConfig {
408        vertices: Vec<Vector>,
409        indices: Vec<[u32; 2]>,
410        params: VhacdParameters,
411    },
412    /// Constructs a collider with [`Collider::convex_decomposition_with_config`].
413    #[cfg(feature = "3d")]
414    ConvexDecompositionWithConfig {
415        vertices: Vec<Vector>,
416        indices: Vec<[u32; 3]>,
417        params: VhacdParameters,
418    },
419    /// Constructs a collider with [`Collider::convex_hull`].
420    #[cfg(feature = "2d")]
421    ConvexHull { points: Vec<Vector> },
422    /// Constructs a collider with [`Collider::convex_hull`].
423    #[cfg(feature = "3d")]
424    ConvexHull { points: Vec<Vector> },
425    /// Constructs a collider with [`Collider::convex_polyline`].
426    #[cfg(feature = "2d")]
427    ConvexPolyline { points: Vec<Vector> },
428    /// Constructs a collider with [`Collider::voxels`].
429    Voxels {
430        voxel_size: Vector,
431        grid_coordinates: Vec<IVector>,
432    },
433    /// Constructs a collider with [`Collider::voxelized_polyline`].
434    #[cfg(feature = "2d")]
435    VoxelizedPolyline {
436        vertices: Vec<Vector>,
437        indices: Vec<[u32; 2]>,
438        voxel_size: Scalar,
439        fill_mode: FillMode,
440    },
441    /// Constructs a collider with [`Collider::voxelized_trimesh`].
442    #[cfg(feature = "3d")]
443    VoxelizedTrimesh {
444        vertices: Vec<Vector>,
445        indices: Vec<[u32; 3]>,
446        voxel_size: Scalar,
447        fill_mode: FillMode,
448    },
449    /// Constructs a collider with [`Collider::heightfield`].
450    #[cfg(feature = "2d")]
451    Heightfield { heights: Vec<Scalar>, scale: Vector },
452    /// Constructs a collider with [`Collider::heightfield`].
453    #[cfg(feature = "3d")]
454    Heightfield {
455        heights: Vec<Vec<Scalar>>,
456        scale: Vector,
457    },
458    /// Constructs a collider with [`Collider::trimesh_from_mesh`].
459    #[cfg(feature = "collider-from-mesh")]
460    #[default]
461    TrimeshFromMesh,
462    /// Constructs a collider with [`Collider::trimesh_from_mesh_with_config`].
463    #[cfg(all(
464        feature = "3d",
465        feature = "collider-from-mesh",
466        feature = "default-collider"
467    ))]
468    TrimeshFromMeshWithConfig(TrimeshFlags),
469    /// Constructs a collider with [`Collider::convex_decomposition_from_mesh`].
470    #[cfg(feature = "collider-from-mesh")]
471    ConvexDecompositionFromMesh,
472    /// Constructs a collider with [`Collider::convex_decomposition_from_mesh_with_config`].
473    #[cfg(all(
474        feature = "3d",
475        feature = "collider-from-mesh",
476        feature = "default-collider"
477    ))]
478    ConvexDecompositionFromMeshWithConfig(VhacdParameters),
479    /// Constructs a collider with [`Collider::convex_hull_from_mesh`].
480    #[cfg(feature = "collider-from-mesh")]
481    ConvexHullFromMesh,
482    /// Constructs a collider with [`Collider::voxelized_trimesh_from_mesh`].
483    #[cfg(feature = "collider-from-mesh")]
484    VoxelizedTrimeshFromMesh {
485        voxel_size: Scalar,
486        fill_mode: FillMode,
487    },
488    /// Constructs a collider with [`Collider::compound`].
489    Compound(Vec<(Position, Rotation, ColliderConstructor)>),
490}
491
492impl ColliderConstructor {
493    /// Returns `true` if the collider type requires a mesh to be generated.
494    #[cfg(feature = "collider-from-mesh")]
495    pub fn requires_mesh(&self) -> bool {
496        matches!(
497            self,
498            Self::TrimeshFromMesh
499                | Self::TrimeshFromMeshWithConfig(_)
500                | Self::ConvexDecompositionFromMesh
501                | Self::ConvexDecompositionFromMeshWithConfig(_)
502                | Self::ConvexHullFromMesh
503                | Self::VoxelizedTrimeshFromMesh { .. }
504        )
505    }
506
507    /// Construct a [`ColliderConstructor::Compound`] from arbitrary [`Position`] and [`Rotation`] representations.
508    pub fn compound<P, R>(shapes: Vec<(P, R, ColliderConstructor)>) -> Self
509    where
510        P: Into<Position>,
511        R: Into<Rotation>,
512    {
513        Self::Compound(
514            shapes
515                .into_iter()
516                .map(|(pos, rot, constructor)| (pos.into(), rot.into(), constructor))
517                .collect(),
518        )
519    }
520
521    pub(crate) fn flatten_compound_constructors(
522        constructors: Vec<(Position, Rotation, ColliderConstructor)>,
523    ) -> Vec<(Position, Rotation, ColliderConstructor)> {
524        constructors
525            .into_iter()
526            .flat_map(|(pos, rot, constructor)| match constructor {
527                ColliderConstructor::Compound(nested) => {
528                    Either::Left(Self::flatten_compound_constructors(nested).into_iter().map(
529                        move |(nested_pos, nested_rot, nested_constructor)| {
530                            (
531                                Position(pos.0 + rot * nested_pos.0),
532                                rot * nested_rot,
533                                nested_constructor,
534                            )
535                        },
536                    ))
537                }
538                other => Either::Right(once((pos, rot, other))),
539            })
540            .collect()
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    #[cfg(feature = "bevy_scene")]
548    use bevy::scene::ScenePlugin;
549    use bevy::{ecs::query::QueryData, mesh::MeshPlugin};
550
551    #[test]
552    fn collider_constructor_requires_no_mesh_on_primitive() {
553        let mut app = create_test_app();
554
555        let entity = app.world_mut().spawn(PRIMITIVE_COLLIDER.clone()).id();
556
557        app.update();
558
559        assert!(app.query_ok::<&Collider>(entity));
560        assert!(app.query_err::<&ColliderConstructor>(entity));
561    }
562
563    #[cfg(feature = "collider-from-mesh")]
564    #[test]
565    #[should_panic]
566    fn collider_constructor_requires_mesh_on_computed() {
567        let mut app = create_test_app();
568
569        app.world_mut().spawn(COMPUTED_COLLIDER.clone());
570
571        app.update();
572    }
573
574    #[cfg(feature = "collider-from-mesh")]
575    #[test]
576    fn collider_constructor_converts_mesh_on_computed() {
577        let mut app = create_test_app();
578
579        let mesh = app.add_mesh();
580        let entity = app
581            .world_mut()
582            .spawn((COMPUTED_COLLIDER.clone(), Mesh3d(mesh)))
583            .id();
584
585        app.update();
586
587        assert!(app.query_ok::<&Collider>(entity));
588        assert!(app.query_ok::<&Mesh3d>(entity));
589        assert!(app.query_err::<&ColliderConstructor>(entity));
590    }
591
592    #[test]
593    fn collider_constructor_hierarchy_does_nothing_on_self_with_primitive() {
594        let mut app = create_test_app();
595
596        let entity = app
597            .world_mut()
598            .spawn(ColliderConstructorHierarchy::new(
599                PRIMITIVE_COLLIDER.clone(),
600            ))
601            .id();
602
603        app.update();
604
605        assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
606        assert!(app.query_err::<&Collider>(entity));
607    }
608
609    #[cfg(feature = "collider-from-mesh")]
610    #[test]
611    fn collider_constructor_hierarchy_does_nothing_on_self_with_computed() {
612        let mut app = create_test_app();
613
614        let mesh = app.add_mesh();
615        let entity = app
616            .world_mut()
617            .spawn((
618                ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()),
619                Mesh3d(mesh),
620            ))
621            .id();
622
623        app.update();
624
625        assert!(app.query_ok::<&Mesh3d>(entity));
626        assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
627        assert!(app.query_err::<&Collider>(entity));
628    }
629
630    #[cfg(feature = "collider-from-mesh")]
631    #[test]
632    fn collider_constructor_hierarchy_does_not_require_mesh_on_self_with_computed() {
633        let mut app = create_test_app();
634
635        let entity = app
636            .world_mut()
637            .spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
638            .id();
639
640        app.update();
641
642        assert!(app.query_err::<&Collider>(entity));
643        assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
644    }
645
646    #[test]
647    fn collider_constructor_hierarchy_inserts_primitive_colliders_on_all_descendants() {
648        let mut app = create_test_app();
649
650        // Hierarchy:
651        // - parent
652        //   - child1
653        //   - child2
654        //     - child3
655
656        let parent = app
657            .world_mut()
658            .spawn(ColliderConstructorHierarchy::new(
659                PRIMITIVE_COLLIDER.clone(),
660            ))
661            .id();
662        let child1 = app.world_mut().spawn(()).id();
663        let child2 = app.world_mut().spawn(()).id();
664        let child3 = app.world_mut().spawn(()).id();
665
666        app.world_mut()
667            .entity_mut(parent)
668            .add_children(&[child1, child2]);
669        app.world_mut().entity_mut(child2).add_children(&[child3]);
670
671        app.update();
672
673        // No entities should have ColliderConstructorHierarchy
674        assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
675        assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
676        assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
677        assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
678
679        assert!(app.query_err::<&Collider>(parent));
680        assert!(app.query_ok::<&Collider>(child1));
681        assert!(app.query_ok::<&Collider>(child2));
682        assert!(app.query_ok::<&Collider>(child3));
683    }
684
685    #[cfg(feature = "collider-from-mesh")]
686    #[test]
687    fn collider_constructor_hierarchy_inserts_computed_colliders_only_on_descendants_with_mesh() {
688        let mut app = create_test_app();
689        let mesh = Mesh3d(app.add_mesh());
690
691        // Hierarchy:
692        // - parent
693        //   - child1 (no mesh)
694        //   - child2 (no mesh)
695        //     - child3 (mesh)
696        //   - child4 (mesh)
697        //     - child5 (no mesh)
698        //   - child6 (mesh)
699        //   - child7 (mesh)
700        //     - child8 (mesh)
701
702        let parent = app
703            .world_mut()
704            .spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
705            .id();
706        let child1 = app.world_mut().spawn(()).id();
707        let child2 = app.world_mut().spawn(()).id();
708        let child3 = app.world_mut().spawn(mesh.clone()).id();
709        let child4 = app.world_mut().spawn(mesh.clone()).id();
710        let child5 = app.world_mut().spawn(()).id();
711        let child6 = app.world_mut().spawn(mesh.clone()).id();
712        let child7 = app.world_mut().spawn(mesh.clone()).id();
713        let child8 = app.world_mut().spawn(mesh.clone()).id();
714
715        app.world_mut()
716            .entity_mut(parent)
717            .add_children(&[child1, child2, child4, child6, child7]);
718        app.world_mut().entity_mut(child2).add_child(child3);
719        app.world_mut().entity_mut(child4).add_child(child5);
720        app.world_mut().entity_mut(child7).add_child(child8);
721
722        app.update();
723
724        // No entities should have ColliderConstructorHierarchy
725        assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
726        assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
727        assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
728        assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
729        assert!(app.query_err::<&ColliderConstructorHierarchy>(child4));
730        assert!(app.query_err::<&ColliderConstructorHierarchy>(child5));
731        assert!(app.query_err::<&ColliderConstructorHierarchy>(child6));
732        assert!(app.query_err::<&ColliderConstructorHierarchy>(child7));
733        assert!(app.query_err::<&ColliderConstructorHierarchy>(child8));
734
735        assert!(app.query_err::<&Collider>(parent));
736        assert!(app.query_err::<&Collider>(child1));
737        assert!(app.query_err::<&Collider>(child2));
738        assert!(app.query_ok::<&Collider>(child3));
739        assert!(app.query_ok::<&Collider>(child4));
740        assert!(app.query_err::<&Collider>(child5));
741        assert!(app.query_ok::<&Collider>(child6));
742        assert!(app.query_ok::<&Collider>(child7));
743        assert!(app.query_ok::<&Collider>(child8));
744    }
745
746    #[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
747    #[test]
748    #[cfg_attr(
749        target_os = "linux",
750        ignore = "The plugin setup requires access to the GPU, which is not available in the linux test environment"
751    )]
752    fn collider_constructor_hierarchy_inserts_correct_configs_on_scene() {
753        use bevy::gltf::GltfMeshName;
754        use parry::shape::ShapeType;
755
756        #[derive(Resource)]
757        struct SceneReady;
758
759        let mut app = create_gltf_test_app();
760
761        app.add_observer(
762            |_trigger: On<bevy::scene::SceneInstanceReady>, mut commands: Commands| {
763                commands.insert_resource(SceneReady);
764            },
765        );
766
767        let scene_handle = app
768            .world_mut()
769            .resource_mut::<AssetServer>()
770            .load("ferris.glb#Scene0");
771
772        let hierarchy = app
773            .world_mut()
774            .spawn((
775                SceneRoot(scene_handle),
776                ColliderConstructorHierarchy::new(ColliderConstructor::ConvexDecompositionFromMesh)
777                    // Use a primitive collider for the left arm.
778                    .with_constructor_for_name("armL_mesh.ferris_material", PRIMITIVE_COLLIDER)
779                    .with_density_for_name("armL_mesh.ferris_material", 2.0)
780                    // Remove the right arm. Don't worry, crabs can regrow lost limbs!
781                    .without_constructor_for_name("armR_mesh.ferris_material"),
782                RigidBody::Dynamic,
783            ))
784            .id();
785
786        let mut counter = 0;
787        loop {
788            if app.world().contains_resource::<SceneReady>() {
789                break;
790            }
791            app.update();
792            counter += 1;
793            if counter > 1000 {
794                panic!("SceneInstanceReady was never triggered");
795            }
796        }
797        app.update();
798
799        assert!(app.query_err::<&ColliderConstructorHierarchy>(hierarchy));
800        assert!(app.query_err::<&Collider>(hierarchy));
801
802        // Check densities
803        let densities: HashMap<_, _> = app
804            .world_mut()
805            .query::<(&GltfMeshName, &ColliderDensity)>()
806            .iter(app.world())
807            .map(|(name, density)| (name.to_string(), density.0))
808            .collect();
809
810        assert_eq!(densities["eyes_mesh"], 1.0);
811        assert_eq!(densities["armL_mesh"], 2.0);
812        assert!(densities.get("armR_mesh").is_none());
813
814        // Check collider shape types
815        let colliders: HashMap<_, _> = app
816            .world_mut()
817            .query::<(&GltfMeshName, &Collider)>()
818            .iter(app.world())
819            .map(|(name, collider)| (name.to_string(), collider))
820            .collect();
821
822        assert_eq!(
823            colliders["eyes_mesh"].shape().shape_type(),
824            ShapeType::Compound
825        );
826        assert_eq!(
827            colliders["armL_mesh"].shape().shape_type(),
828            ShapeType::Capsule
829        );
830        assert!(colliders.get("armR_mesh").is_none());
831    }
832
833    const PRIMITIVE_COLLIDER: ColliderConstructor = ColliderConstructor::Capsule {
834        height: 1.0,
835        radius: 0.5,
836    };
837
838    #[cfg(feature = "collider-from-mesh")]
839    const COMPUTED_COLLIDER: ColliderConstructor = ColliderConstructor::TrimeshFromMesh;
840
841    fn create_test_app() -> App {
842        let mut app = App::new();
843        app.add_plugins((
844            MinimalPlugins,
845            AssetPlugin::default(),
846            #[cfg(feature = "bevy_scene")]
847            ScenePlugin,
848            MeshPlugin,
849            PhysicsPlugins::default(),
850        ));
851
852        app
853    }
854
855    #[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
856    fn create_gltf_test_app() -> App {
857        use bevy::{diagnostic::DiagnosticsPlugin, winit::WinitPlugin};
858
859        // Todo: it would be best to disable all rendering-related plugins,
860        // but we have so far not succeeded in finding the right plugin combination
861        // that still results in `SceneInstanceReady` being triggered.
862        let mut app = App::new();
863        app.add_plugins((
864            DefaultPlugins
865                .build()
866                .disable::<WinitPlugin>()
867                .disable::<DiagnosticsPlugin>(),
868            PhysicsPlugins::default(),
869        ));
870        app.finish();
871        app.cleanup();
872        app
873    }
874
875    trait AppExt {
876        fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool;
877        fn query_err<D: QueryData>(&mut self, entity: Entity) -> bool {
878            !self.query_ok::<D>(entity)
879        }
880
881        #[cfg(feature = "collider-from-mesh")]
882        fn add_mesh(&mut self) -> Handle<Mesh>;
883    }
884
885    impl AppExt for App {
886        fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool {
887            let mut query = self.world_mut().query::<D>();
888            let component = query.get(self.world(), entity);
889            component.is_ok()
890        }
891
892        #[cfg(feature = "collider-from-mesh")]
893        fn add_mesh(&mut self) -> Handle<Mesh> {
894            self.world_mut()
895                .get_resource_mut::<Assets<Mesh>>()
896                .unwrap()
897                .add(Mesh::from(Cuboid::default()))
898        }
899    }
900}