avian3d/collision/collider/
constructor.rs

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