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