1#![allow(clippy::unnecessary_cast)]
2
3pub mod contact_query;
4
5#[cfg(feature = "2d")]
6mod primitives2d;
7#[cfg(feature = "3d")]
8mod primitives3d;
9
10#[cfg(feature = "2d")]
11pub use primitives2d::{EllipseColliderShape, RegularPolygonColliderShape};
12
13use super::EnlargedAabb;
14use crate::{make_pose, prelude::*};
15#[cfg(feature = "collider-from-mesh")]
16use bevy::mesh::{Indices, VertexAttributeValues};
17use bevy::{log, prelude::*};
18use contact_query::UnsupportedShape;
19use itertools::Either;
20use parry::shape::{RoundShape, SharedShape, TypedShape, Voxels};
21
22impl<T: IntoCollider<Collider>> From<T> for Collider {
23 fn from(value: T) -> Self {
24 value.collider()
25 }
26}
27
28#[derive(Clone, PartialEq, Debug, Reflect)]
32#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
33#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
34#[reflect(PartialEq, Debug)]
35pub struct VhacdParameters {
36 pub concavity: Scalar,
41 pub alpha: Scalar,
46 pub beta: Scalar,
51 pub resolution: u32,
55 pub plane_downsampling: u32,
60 pub convex_hull_downsampling: u32,
65 pub fill_mode: FillMode,
70 pub convex_hull_approximation: bool,
76 pub max_convex_hulls: u32,
80}
81
82impl Default for VhacdParameters {
83 fn default() -> Self {
84 Self {
85 #[cfg(feature = "3d")]
86 resolution: 64,
87 #[cfg(feature = "3d")]
88 concavity: 0.01,
89 #[cfg(feature = "2d")]
90 resolution: 256,
91 #[cfg(feature = "2d")]
92 concavity: 0.1,
93 plane_downsampling: 4,
94 convex_hull_downsampling: 4,
95 alpha: 0.05,
96 beta: 0.05,
97 convex_hull_approximation: true,
98 max_convex_hulls: 1024,
99 fill_mode: FillMode::FloodFill {
100 detect_cavities: false,
101 #[cfg(feature = "2d")]
102 detect_self_intersections: false,
103 },
104 }
105 }
106}
107
108impl From<VhacdParameters> for parry::transformation::vhacd::VHACDParameters {
109 fn from(value: VhacdParameters) -> Self {
110 Self {
111 concavity: value.concavity,
112 alpha: value.alpha,
113 beta: value.beta,
114 resolution: value.resolution,
115 plane_downsampling: value.plane_downsampling,
116 convex_hull_downsampling: value.convex_hull_downsampling,
117 fill_mode: value.fill_mode.into(),
118 convex_hull_approximation: value.convex_hull_approximation,
119 max_convex_hulls: value.max_convex_hulls,
120 }
121 }
122}
123
124#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
127#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
128#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
129#[reflect(Hash, PartialEq, Debug)]
130pub enum FillMode {
131 SurfaceOnly,
134 FloodFill {
138 detect_cavities: bool,
140 #[cfg(feature = "2d")]
142 detect_self_intersections: bool,
143 },
144}
145
146impl From<FillMode> for parry::transformation::voxelization::FillMode {
147 fn from(value: FillMode) -> Self {
148 match value {
149 FillMode::SurfaceOnly => Self::SurfaceOnly,
150 FillMode::FloodFill {
151 detect_cavities,
152 #[cfg(feature = "2d")]
153 detect_self_intersections,
154 } => Self::FloodFill {
155 detect_cavities,
156 #[cfg(feature = "2d")]
157 detect_self_intersections,
158 },
159 }
160 }
161}
162
163#[repr(transparent)]
165#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
166#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
167#[reflect(opaque, Hash, PartialEq, Debug)]
168pub struct TrimeshFlags(u8);
169
170bitflags::bitflags! {
171 impl TrimeshFlags: u8 {
172 const HALF_EDGE_TOPOLOGY = 0b0000_0001;
174 const CONNECTED_COMPONENTS = 0b0000_0010;
180 const DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100;
182 const ORIENTED = 0b0000_1000;
186 const MERGE_DUPLICATE_VERTICES = 0b0001_0000;
191 const DELETE_DEGENERATE_TRIANGLES = 0b0010_0000;
197 const DELETE_DUPLICATE_TRIANGLES = 0b0100_0000;
203 const FIX_INTERNAL_EDGES = 0b1000_0000 | Self::ORIENTED.bits() | Self::MERGE_DUPLICATE_VERTICES.bits();
210 }
211}
212
213impl From<TrimeshFlags> for parry::shape::TriMeshFlags {
214 fn from(value: TrimeshFlags) -> Self {
215 Self::from_bits(value.bits().into()).unwrap()
216 }
217}
218
219pub type TrimeshBuilderError = parry::shape::TriMeshBuilderError;
221
222#[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
230#[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
231#[cfg_attr(feature = "2d", doc = "commands.spawn(Collider::circle(0.5));")]
236#[cfg_attr(feature = "3d", doc = "commands.spawn(Collider::sphere(0.5));")]
237#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
249#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
250#[cfg_attr(feature = "2d", doc = " Collider::circle(0.5),")]
257#[cfg_attr(feature = "3d", doc = " Collider::sphere(0.5),")]
258#[cfg_attr(
261 feature = "2d",
262 doc = " commands.spawn((RigidBody::Static, Collider::rectangle(5.0, 0.5)));"
263)]
264#[cfg_attr(
265 feature = "3d",
266 doc = " commands.spawn((RigidBody::Static, Collider::cuboid(5.0, 0.5, 5.0)));"
267)]
268#[cfg_attr(
279 feature = "3d",
280 doc = "Colliders can also be generated automatically for meshes and scenes. See [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]."
281)]
282#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
292#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
293#[cfg_attr(
299 feature = "2d",
300 doc = " .spawn((RigidBody::Dynamic, Collider::circle(0.5)))"
301)]
302#[cfg_attr(
303 feature = "3d",
304 doc = " .spawn((RigidBody::Dynamic, Collider::sphere(0.5)))"
305)]
306#[cfg_attr(
309 feature = "2d",
310 doc = " children.spawn((Collider::circle(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
311 children.spawn((Collider::circle(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
312)]
313#[cfg_attr(
314 feature = "3d",
315 doc = " children.spawn((Collider::sphere(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
316 children.spawn((Collider::sphere(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
317)]
318#[cfg_attr(
338 feature = "3d",
339 doc = "- Generating colliders for meshes and scenes with [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]"
340)]
341#[derive(Clone, Component, Debug)]
357#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
358#[require(
359 ColliderMarker,
360 ColliderAabb,
361 CollisionLayers,
362 EnlargedAabb,
363 ColliderDensity,
364 ColliderMassProperties
365)]
366pub struct Collider {
367 shape: SharedShape,
369 scaled_shape: SharedShape,
374 scale: Vector,
376}
377
378impl From<SharedShape> for Collider {
379 fn from(value: SharedShape) -> Self {
380 Self {
381 shape: value.clone(),
382 scaled_shape: value,
383 scale: Vector::ONE,
384 }
385 }
386}
387
388impl Default for Collider {
389 fn default() -> Self {
390 #[cfg(feature = "2d")]
391 {
392 Self::rectangle(0.5, 0.5)
393 }
394 #[cfg(feature = "3d")]
395 {
396 Self::cuboid(0.5, 0.5, 0.5)
397 }
398 }
399}
400
401impl AnyCollider for Collider {
402 type Context = ();
403
404 fn aabb_with_context(
405 &self,
406 position: Vector,
407 rotation: impl Into<Rotation>,
408 _: AabbContext<Self::Context>,
409 ) -> ColliderAabb {
410 let aabb = self
411 .shape_scaled()
412 .compute_aabb(&make_pose(position, rotation));
413 ColliderAabb {
414 min: aabb.mins,
415 max: aabb.maxs,
416 }
417 }
418
419 fn contact_manifolds_with_context(
420 &self,
421 other: &Self,
422 position1: Vector,
423 rotation1: impl Into<Rotation>,
424 position2: Vector,
425 rotation2: impl Into<Rotation>,
426 prediction_distance: Scalar,
427 manifolds: &mut Vec<ContactManifold>,
428 _: ContactManifoldContext<Self::Context>,
429 ) {
430 contact_query::contact_manifolds(
431 self,
432 position1,
433 rotation1,
434 other,
435 position2,
436 rotation2,
437 prediction_distance,
438 manifolds,
439 )
440 }
441}
442
443#[cfg(feature = "2d")]
446impl ComputeMassProperties for Collider {
447 fn mass(&self, density: f32) -> f32 {
448 let props = self.shape_scaled().mass_properties(density as Scalar);
449 props.mass() as f32
450 }
451
452 fn unit_angular_inertia(&self) -> f32 {
453 self.angular_inertia(1.0)
454 }
455
456 fn angular_inertia(&self, mass: f32) -> f32 {
457 let props = self.shape_scaled().mass_properties(mass as Scalar);
458 props.principal_inertia() as f32
459 }
460
461 fn center_of_mass(&self) -> Vec2 {
462 let props = self.shape_scaled().mass_properties(1.0);
463 props.local_com.f32()
464 }
465
466 fn mass_properties(&self, density: f32) -> MassProperties {
467 let props = self.shape_scaled().mass_properties(density as Scalar);
468
469 MassProperties {
470 mass: props.mass() as f32,
471 #[cfg(feature = "2d")]
472 angular_inertia: props.principal_inertia() as f32,
473 #[cfg(feature = "3d")]
474 principal_angular_inertia: props.principal_inertia().f32(),
475 #[cfg(feature = "3d")]
476 local_inertial_frame: props.principal_inertia_local_frame.f32(),
477 center_of_mass: props.local_com.f32(),
478 }
479 }
480}
481
482#[cfg(feature = "3d")]
483impl ComputeMassProperties for Collider {
484 fn mass(&self, density: f32) -> f32 {
485 let props = self.shape_scaled().mass_properties(density as Scalar);
486 props.mass() as f32
487 }
488
489 fn unit_principal_angular_inertia(&self) -> Vec3 {
490 self.principal_angular_inertia(1.0)
491 }
492
493 fn principal_angular_inertia(&self, mass: f32) -> Vec3 {
494 let props = self.shape_scaled().mass_properties(mass as Scalar);
495 props.principal_inertia().f32()
496 }
497
498 fn local_inertial_frame(&self) -> Quat {
499 let props = self.shape_scaled().mass_properties(1.0);
500 props.principal_inertia_local_frame.f32()
501 }
502
503 fn center_of_mass(&self) -> Vec3 {
504 let props = self.shape_scaled().mass_properties(1.0);
505 props.local_com.f32()
506 }
507
508 fn mass_properties(&self, density: f32) -> MassProperties {
509 let props = self.shape_scaled().mass_properties(density as Scalar);
510
511 MassProperties {
512 mass: props.mass() as f32,
513 #[cfg(feature = "2d")]
514 angular_inertia: props.principal_inertia() as f32,
515 #[cfg(feature = "3d")]
516 principal_angular_inertia: props.principal_inertia().f32(),
517 #[cfg(feature = "3d")]
518 local_inertial_frame: props.principal_inertia_local_frame.f32(),
519 center_of_mass: props.local_com.f32(),
520 }
521 }
522}
523
524impl ScalableCollider for Collider {
525 fn scale(&self) -> Vector {
526 self.scale()
527 }
528
529 fn set_scale(&mut self, scale: Vector, detail: u32) {
530 self.set_scale(scale, detail)
531 }
532}
533
534impl Collider {
535 pub fn shape(&self) -> &SharedShape {
537 &self.shape
538 }
539
540 pub fn shape_mut(&mut self) -> &mut SharedShape {
542 &mut self.shape
543 }
544
545 pub fn shape_scaled(&self) -> &SharedShape {
547 &self.scaled_shape
548 }
549
550 pub fn set_shape(&mut self, shape: SharedShape) {
552 self.shape = shape;
553
554 if let Ok(scaled) = scale_shape(&self.shape, self.scale, 10) {
556 self.scaled_shape = scaled;
557 } else {
558 log::error!("Failed to create convex hull for scaled collider.");
559 }
560 }
561
562 pub fn scale(&self) -> Vector {
564 self.scale
565 }
566
567 pub fn set_scale(&mut self, scale: Vector, num_subdivisions: u32) {
575 if scale == self.scale {
576 return;
577 }
578
579 if scale == Vector::ONE {
580 self.scaled_shape = self.shape.clone();
582 self.scale = Vector::ONE;
583 return;
584 }
585
586 if let Ok(scaled) = scale_shape(&self.shape, scale, num_subdivisions) {
587 self.scaled_shape = scaled;
588 self.scale = scale;
589 } else {
590 log::error!("Failed to create convex hull for scaled collider.");
591 }
592 }
593
594 pub fn project_point(
600 &self,
601 translation: impl Into<Position>,
602 rotation: impl Into<Rotation>,
603 point: Vector,
604 solid: bool,
605 ) -> (Vector, bool) {
606 let projection =
607 self.shape_scaled()
608 .project_point(&make_pose(translation, rotation), point, solid);
609 (projection.point, projection.is_inside)
610 }
611
612 pub fn distance_to_point(
618 &self,
619 translation: impl Into<Position>,
620 rotation: impl Into<Rotation>,
621 point: Vector,
622 solid: bool,
623 ) -> Scalar {
624 self.shape_scaled()
625 .distance_to_point(&make_pose(translation, rotation), point, solid)
626 }
627
628 pub fn contains_point(
630 &self,
631 translation: impl Into<Position>,
632 rotation: impl Into<Rotation>,
633 point: Vector,
634 ) -> bool {
635 self.shape_scaled()
636 .contains_point(&make_pose(translation, rotation), point)
637 }
638
639 pub fn cast_ray(
652 &self,
653 translation: impl Into<Position>,
654 rotation: impl Into<Rotation>,
655 ray_origin: Vector,
656 ray_direction: Vector,
657 max_distance: Scalar,
658 solid: bool,
659 ) -> Option<(Scalar, Vector)> {
660 let hit = self.shape_scaled().cast_ray_and_get_normal(
661 &make_pose(translation, rotation),
662 &parry::query::Ray::new(ray_origin, ray_direction),
663 max_distance,
664 solid,
665 );
666 hit.map(|hit| (hit.time_of_impact, hit.normal))
667 }
668
669 pub fn intersects_ray(
677 &self,
678 translation: impl Into<Position>,
679 rotation: impl Into<Rotation>,
680 ray_origin: Vector,
681 ray_direction: Vector,
682 max_distance: Scalar,
683 ) -> bool {
684 self.shape_scaled().intersects_ray(
685 &make_pose(translation, rotation),
686 &parry::query::Ray::new(ray_origin, ray_direction),
687 max_distance,
688 )
689 }
690
691 pub fn compound(
699 shapes: Vec<(
700 impl Into<Position>,
701 impl Into<Rotation>,
702 impl Into<Collider>,
703 )>,
704 ) -> Self {
705 let shapes = shapes
706 .into_iter()
707 .map(|(p, r, c)| {
708 (
709 make_pose(*p.into(), r.into()),
710 c.into().shape_scaled().clone(),
711 )
712 })
713 .collect::<Vec<_>>();
714 SharedShape::compound(shapes).into()
715 }
716
717 #[cfg(feature = "2d")]
719 pub fn circle(radius: Scalar) -> Self {
720 SharedShape::ball(radius).into()
721 }
722
723 #[cfg(feature = "3d")]
725 pub fn sphere(radius: Scalar) -> Self {
726 SharedShape::ball(radius).into()
727 }
728
729 #[cfg(feature = "2d")]
731 pub fn ellipse(half_width: Scalar, half_height: Scalar) -> Self {
732 SharedShape::new(EllipseColliderShape(Ellipse::new(
733 half_width as f32,
734 half_height as f32,
735 )))
736 .into()
737 }
738
739 #[cfg(feature = "2d")]
741 pub fn rectangle(x_length: Scalar, y_length: Scalar) -> Self {
742 SharedShape::cuboid(x_length * 0.5, y_length * 0.5).into()
743 }
744
745 #[cfg(feature = "3d")]
747 pub fn cuboid(x_length: Scalar, y_length: Scalar, z_length: Scalar) -> Self {
748 SharedShape::cuboid(x_length * 0.5, y_length * 0.5, z_length * 0.5).into()
749 }
750
751 #[cfg(feature = "2d")]
753 pub fn round_rectangle(x_length: Scalar, y_length: Scalar, border_radius: Scalar) -> Self {
754 SharedShape::round_cuboid(x_length * 0.5, y_length * 0.5, border_radius).into()
755 }
756
757 #[cfg(feature = "3d")]
759 pub fn round_cuboid(
760 x_length: Scalar,
761 y_length: Scalar,
762 z_length: Scalar,
763 border_radius: Scalar,
764 ) -> Self {
765 SharedShape::round_cuboid(
766 x_length * 0.5,
767 y_length * 0.5,
768 z_length * 0.5,
769 border_radius,
770 )
771 .into()
772 }
773
774 #[cfg(feature = "3d")]
777 pub fn cylinder(radius: Scalar, height: Scalar) -> Self {
778 SharedShape::cylinder(height * 0.5, radius).into()
779 }
780
781 #[cfg(feature = "3d")]
784 pub fn cone(radius: Scalar, height: Scalar) -> Self {
785 SharedShape::cone(height * 0.5, radius).into()
786 }
787
788 pub fn capsule(radius: Scalar, length: Scalar) -> Self {
791 SharedShape::capsule(
792 Vector::Y * length * 0.5,
793 Vector::NEG_Y * length * 0.5,
794 radius,
795 )
796 .into()
797 }
798
799 pub fn capsule_endpoints(radius: Scalar, a: Vector, b: Vector) -> Self {
801 SharedShape::capsule(a, b, radius).into()
802 }
803
804 pub fn half_space(outward_normal: Vector) -> Self {
807 SharedShape::halfspace(outward_normal.normalize_or_zero()).into()
808 }
809
810 pub fn segment(a: Vector, b: Vector) -> Self {
812 SharedShape::segment(a, b).into()
813 }
814
815 #[cfg(feature = "2d")]
823 pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
824 let mut triangle = parry::shape::Triangle::new(a, b, c);
825
826 if triangle.orientation(1e-8) == parry::shape::TriangleOrientation::Clockwise {
828 triangle.reverse();
829 }
830
831 SharedShape::new(triangle).into()
832 }
833
834 #[cfg(feature = "2d")]
841 pub fn triangle_unchecked(a: Vector, b: Vector, c: Vector) -> Self {
842 SharedShape::triangle(a, b, c).into()
843 }
844
845 #[cfg(feature = "3d")]
847 pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
848 SharedShape::triangle(a, b, c).into()
849 }
850
851 #[cfg(feature = "2d")]
853 pub fn regular_polygon(circumradius: f32, sides: u32) -> Self {
854 RegularPolygon::new(circumradius, sides).collider()
855 }
856
857 pub fn polyline(vertices: Vec<Vector>, indices: Option<Vec<[u32; 2]>>) -> Self {
859 SharedShape::polyline(vertices, indices).into()
860 }
861
862 pub fn trimesh(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
875 Self::try_trimesh(vertices, indices)
876 .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
877 }
878
879 pub fn try_trimesh(
892 vertices: Vec<Vector>,
893 indices: Vec<[u32; 3]>,
894 ) -> Result<Self, TrimeshBuilderError> {
895 SharedShape::trimesh(vertices, indices).map(|trimesh| trimesh.into())
896 }
897
898 pub fn trimesh_with_config(
912 vertices: Vec<Vector>,
913 indices: Vec<[u32; 3]>,
914 flags: TrimeshFlags,
915 ) -> Self {
916 Self::try_trimesh_with_config(vertices, indices, flags)
917 .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
918 }
919
920 pub fn try_trimesh_with_config(
934 vertices: Vec<Vector>,
935 indices: Vec<[u32; 3]>,
936 flags: TrimeshFlags,
937 ) -> Result<Self, TrimeshBuilderError> {
938 SharedShape::trimesh_with_flags(vertices, indices, flags.into())
939 .map(|trimesh| trimesh.into())
940 }
941
942 #[cfg(feature = "2d")]
945 pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 2]>) -> Self {
946 SharedShape::convex_decomposition(&vertices, &indices).into()
947 }
948
949 #[cfg(feature = "3d")]
952 pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
953 SharedShape::convex_decomposition(&vertices, &indices).into()
954 }
955
956 #[cfg(feature = "2d")]
960 pub fn convex_decomposition_with_config(
961 vertices: Vec<Vector>,
962 indices: Vec<[u32; 2]>,
963 params: &VhacdParameters,
964 ) -> Self {
965 SharedShape::convex_decomposition_with_params(&vertices, &indices, ¶ms.clone().into())
966 .into()
967 }
968
969 #[cfg(feature = "3d")]
973 pub fn convex_decomposition_with_config(
974 vertices: Vec<Vector>,
975 indices: Vec<[u32; 3]>,
976 params: VhacdParameters,
977 ) -> Self {
978 SharedShape::convex_decomposition_with_params(&vertices, &indices, ¶ms.clone().into())
979 .into()
980 }
981
982 #[cfg(feature = "2d")]
985 pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
986 SharedShape::convex_hull(&points).map(Into::into)
987 }
988
989 #[cfg(feature = "3d")]
992 pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
993 SharedShape::convex_hull(&points).map(Into::into)
994 }
995
996 #[cfg(feature = "2d")]
1000 pub fn convex_polyline(points: Vec<Vector>) -> Option<Self> {
1001 SharedShape::convex_polyline(points).map(Into::into)
1002 }
1003
1004 pub fn voxels(voxel_size: Vector, grid_coordinates: &[IVector]) -> Self {
1008 #[cfg(all(feature = "2d", feature = "f64"))]
1009 let grid_coordinates = &grid_coordinates
1010 .iter()
1011 .map(|c| c.as_i64vec2())
1012 .collect::<Vec<_>>();
1013 #[cfg(all(feature = "3d", feature = "f64"))]
1014 let grid_coordinates = &grid_coordinates
1015 .iter()
1016 .map(|c| c.as_i64vec3())
1017 .collect::<Vec<_>>();
1018 let shape = Voxels::new(voxel_size, grid_coordinates);
1019 SharedShape::new(shape).into()
1020 }
1021
1022 pub fn voxels_from_points(voxel_size: Vector, points: &[Vector]) -> Self {
1026 SharedShape::voxels_from_points(voxel_size, points).into()
1027 }
1028
1029 #[cfg(feature = "2d")]
1031 pub fn voxelized_polyline(
1032 vertices: &[Vector],
1033 indices: &[[u32; 2]],
1034 voxel_size: Scalar,
1035 fill_mode: FillMode,
1036 ) -> Self {
1037 SharedShape::voxelized_mesh(vertices, indices, voxel_size, fill_mode.into()).into()
1038 }
1039
1040 #[cfg(feature = "3d")]
1042 pub fn voxelized_trimesh(
1043 vertices: &[Vector],
1044 indices: &[[u32; 3]],
1045 voxel_size: Scalar,
1046 fill_mode: FillMode,
1047 ) -> Self {
1048 SharedShape::voxelized_mesh(vertices, indices, voxel_size, fill_mode.into()).into()
1049 }
1050
1051 #[cfg(feature = "collider-from-mesh")]
1055 pub fn voxelized_trimesh_from_mesh(
1056 mesh: &Mesh,
1057 voxel_size: Scalar,
1058 fill_mode: FillMode,
1059 ) -> Option<Self> {
1060 extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1061 SharedShape::voxelized_mesh(&vertices, &indices, voxel_size, fill_mode.into()).into()
1062 })
1063 }
1064
1065 #[cfg_attr(
1066 feature = "2d",
1067 doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1068 )]
1069 #[cfg_attr(
1070 feature = "3d",
1071 doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1072 )]
1073 pub fn voxelized_convex_decomposition(
1074 vertices: &[Vector],
1075 indices: &[[u32; DIM]],
1076 ) -> Vec<Self> {
1077 Self::voxelized_convex_decomposition_with_config(
1078 vertices,
1079 indices,
1080 &VhacdParameters::default(),
1081 )
1082 }
1083
1084 #[cfg_attr(
1085 feature = "2d",
1086 doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1087 )]
1088 #[cfg_attr(
1089 feature = "3d",
1090 doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1091 )]
1092 pub fn voxelized_convex_decomposition_with_config(
1093 vertices: &[Vector],
1094 indices: &[[u32; DIM]],
1095 parameters: &VhacdParameters,
1096 ) -> Vec<Self> {
1097 SharedShape::voxelized_convex_decomposition_with_params(
1098 vertices,
1099 indices,
1100 ¶meters.clone().into(),
1101 )
1102 .into_iter()
1103 .map(|c| c.into())
1104 .collect()
1105 }
1106
1107 #[cfg(feature = "2d")]
1114 pub fn heightfield(heights: Vec<Scalar>, scale: Vector) -> Self {
1115 SharedShape::heightfield(heights, scale).into()
1116 }
1117
1118 #[cfg(feature = "3d")]
1128 pub fn heightfield(heights: Vec<Vec<Scalar>>, scale: Vector) -> Self {
1129 let row_count = heights.len();
1130 let column_count = heights[0].len();
1131 let data: Vec<Scalar> = heights.into_iter().flatten().collect();
1132
1133 assert_eq!(
1134 data.len(),
1135 row_count * column_count,
1136 "Each row in `heights` must have the same amount of points"
1137 );
1138
1139 let heights = parry::utils::Array2::new(row_count, column_count, data);
1140 SharedShape::heightfield(heights, scale).into()
1141 }
1142
1143 #[cfg(feature = "collider-from-mesh")]
1174 pub fn trimesh_from_mesh(mesh: &Mesh) -> Option<Self> {
1175 extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1176 SharedShape::trimesh_with_flags(
1177 vertices,
1178 indices,
1179 TrimeshFlags::MERGE_DUPLICATE_VERTICES.into(),
1180 )
1181 .map(|trimesh| trimesh.into())
1182 .ok()
1183 })
1184 }
1185
1186 #[cfg(feature = "collider-from-mesh")]
1218 pub fn trimesh_from_mesh_with_config(mesh: &Mesh, flags: TrimeshFlags) -> Option<Self> {
1219 extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1220 SharedShape::trimesh_with_flags(vertices, indices, flags.into())
1221 .map(|trimesh| trimesh.into())
1222 .ok()
1223 })
1224 }
1225
1226 #[cfg(feature = "collider-from-mesh")]
1243 pub fn convex_hull_from_mesh(mesh: &Mesh) -> Option<Self> {
1244 extract_mesh_vertices_indices(mesh)
1245 .and_then(|(vertices, _)| SharedShape::convex_hull(&vertices).map(|shape| shape.into()))
1246 }
1247
1248 #[cfg(feature = "collider-from-mesh")]
1265 pub fn convex_decomposition_from_mesh(mesh: &Mesh) -> Option<Self> {
1266 extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1267 SharedShape::convex_decomposition(&vertices, &indices).into()
1268 })
1269 }
1270
1271 #[cfg(feature = "collider-from-mesh")]
1293 pub fn convex_decomposition_from_mesh_with_config(
1294 mesh: &Mesh,
1295 parameters: &VhacdParameters,
1296 ) -> Option<Self> {
1297 extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1298 SharedShape::convex_decomposition_with_params(
1299 &vertices,
1300 &indices,
1301 ¶meters.clone().into(),
1302 )
1303 .into()
1304 })
1305 }
1306
1307 #[cfg_attr(
1312 feature = "collider-from-mesh",
1313 doc = "Returns `None` in the following cases:
1314- The given [`ColliderConstructor`] requires a mesh, but none was provided.
1315- Creating the collider from the given [`ColliderConstructor`] failed."
1316 )]
1317 #[cfg_attr(
1318 not(feature = "collider-from-mesh"),
1319 doc = "Returns `None` if creating the collider from the given [`ColliderConstructor`] failed."
1320 )]
1321 pub fn try_from_constructor(
1322 collider_constructor: ColliderConstructor,
1323 #[cfg(feature = "collider-from-mesh")] mesh: Option<&Mesh>,
1324 ) -> Option<Self> {
1325 match collider_constructor {
1326 #[cfg(feature = "2d")]
1327 ColliderConstructor::Circle { radius } => Some(Self::circle(radius)),
1328 #[cfg(feature = "3d")]
1329 ColliderConstructor::Sphere { radius } => Some(Self::sphere(radius)),
1330 #[cfg(feature = "2d")]
1331 ColliderConstructor::Ellipse {
1332 half_width,
1333 half_height,
1334 } => Some(Self::ellipse(half_width, half_height)),
1335 #[cfg(feature = "2d")]
1336 ColliderConstructor::Rectangle { x_length, y_length } => {
1337 Some(Self::rectangle(x_length, y_length))
1338 }
1339 #[cfg(feature = "3d")]
1340 ColliderConstructor::Cuboid {
1341 x_length,
1342 y_length,
1343 z_length,
1344 } => Some(Self::cuboid(x_length, y_length, z_length)),
1345 #[cfg(feature = "2d")]
1346 ColliderConstructor::RoundRectangle {
1347 x_length,
1348 y_length,
1349 border_radius,
1350 } => Some(Self::round_rectangle(x_length, y_length, border_radius)),
1351 #[cfg(feature = "3d")]
1352 ColliderConstructor::RoundCuboid {
1353 x_length,
1354 y_length,
1355 z_length,
1356 border_radius,
1357 } => Some(Self::round_cuboid(
1358 x_length,
1359 y_length,
1360 z_length,
1361 border_radius,
1362 )),
1363 #[cfg(feature = "3d")]
1364 ColliderConstructor::Cylinder { radius, height } => {
1365 Some(Self::cylinder(radius, height))
1366 }
1367 #[cfg(feature = "3d")]
1368 ColliderConstructor::Cone { radius, height } => Some(Self::cone(radius, height)),
1369 ColliderConstructor::Capsule { radius, height } => Some(Self::capsule(radius, height)),
1370 ColliderConstructor::CapsuleEndpoints { radius, a, b } => {
1371 Some(Self::capsule_endpoints(radius, a, b))
1372 }
1373 ColliderConstructor::HalfSpace { outward_normal } => {
1374 Some(Self::half_space(outward_normal))
1375 }
1376 ColliderConstructor::Segment { a, b } => Some(Self::segment(a, b)),
1377 ColliderConstructor::Triangle { a, b, c } => Some(Self::triangle(a, b, c)),
1378 #[cfg(feature = "2d")]
1379 ColliderConstructor::RegularPolygon {
1380 circumradius,
1381 sides,
1382 } => Some(Self::regular_polygon(circumradius, sides)),
1383 ColliderConstructor::Polyline { vertices, indices } => {
1384 Some(Self::polyline(vertices, indices))
1385 }
1386 ColliderConstructor::Trimesh { vertices, indices } => {
1387 Some(Self::trimesh(vertices, indices))
1388 }
1389 ColliderConstructor::TrimeshWithConfig {
1390 vertices,
1391 indices,
1392 flags,
1393 } => Some(Self::trimesh_with_config(vertices, indices, flags)),
1394 #[cfg(feature = "2d")]
1395 ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1396 Some(Self::convex_decomposition(vertices, indices))
1397 }
1398 #[cfg(feature = "3d")]
1399 ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1400 Some(Self::convex_decomposition(vertices, indices))
1401 }
1402 #[cfg(feature = "2d")]
1403 ColliderConstructor::ConvexDecompositionWithConfig {
1404 vertices,
1405 indices,
1406 params,
1407 } => Some(Self::convex_decomposition_with_config(
1408 vertices, indices, ¶ms,
1409 )),
1410 #[cfg(feature = "3d")]
1411 ColliderConstructor::ConvexDecompositionWithConfig {
1412 vertices,
1413 indices,
1414 params,
1415 } => Some(Self::convex_decomposition_with_config(
1416 vertices, indices, params,
1417 )),
1418 #[cfg(feature = "2d")]
1419 ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1420 #[cfg(feature = "3d")]
1421 ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1422 #[cfg(feature = "2d")]
1423 ColliderConstructor::ConvexPolyline { points } => Self::convex_polyline(points),
1424 ColliderConstructor::Voxels {
1425 voxel_size,
1426 grid_coordinates,
1427 } => Some(Self::voxels(voxel_size, &grid_coordinates)),
1428 #[cfg(feature = "2d")]
1429 ColliderConstructor::VoxelizedPolyline {
1430 vertices,
1431 indices,
1432 voxel_size,
1433 fill_mode,
1434 } => Some(Self::voxelized_polyline(
1435 &vertices, &indices, voxel_size, fill_mode,
1436 )),
1437 #[cfg(feature = "3d")]
1438 ColliderConstructor::VoxelizedTrimesh {
1439 vertices,
1440 indices,
1441 voxel_size,
1442 fill_mode,
1443 } => Some(Self::voxelized_trimesh(
1444 &vertices, &indices, voxel_size, fill_mode,
1445 )),
1446 #[cfg(feature = "2d")]
1447 ColliderConstructor::Heightfield { heights, scale } => {
1448 Some(Self::heightfield(heights, scale))
1449 }
1450 #[cfg(feature = "3d")]
1451 ColliderConstructor::Heightfield { heights, scale } => {
1452 Some(Self::heightfield(heights, scale))
1453 }
1454 #[cfg(feature = "collider-from-mesh")]
1455 ColliderConstructor::TrimeshFromMesh => Self::trimesh_from_mesh(mesh?),
1456 #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1457 ColliderConstructor::TrimeshFromMeshWithConfig(flags) => {
1458 Self::trimesh_from_mesh_with_config(mesh?, flags)
1459 }
1460 #[cfg(feature = "collider-from-mesh")]
1461 ColliderConstructor::ConvexDecompositionFromMesh => {
1462 Self::convex_decomposition_from_mesh(mesh?)
1463 }
1464 #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1465 ColliderConstructor::ConvexDecompositionFromMeshWithConfig(params) => {
1466 Self::convex_decomposition_from_mesh_with_config(mesh?, ¶ms)
1467 }
1468 #[cfg(feature = "collider-from-mesh")]
1469 ColliderConstructor::ConvexHullFromMesh => Self::convex_hull_from_mesh(mesh?),
1470 #[cfg(feature = "collider-from-mesh")]
1471 ColliderConstructor::VoxelizedTrimeshFromMesh {
1472 voxel_size,
1473 fill_mode,
1474 } => Self::voxelized_trimesh_from_mesh(mesh?, voxel_size, fill_mode),
1475 ColliderConstructor::Compound(compound_constructors) => {
1476 let shapes: Vec<_> =
1477 ColliderConstructor::flatten_compound_constructors(compound_constructors)
1478 .into_iter()
1479 .filter_map(|(position, rotation, collider_constructor)| {
1480 Self::try_from_constructor(
1481 collider_constructor,
1482 #[cfg(feature = "collider-from-mesh")]
1483 mesh,
1484 )
1485 .map(|collider| (position, rotation, collider))
1486 })
1487 .collect();
1488
1489 (!shapes.is_empty()).then(|| Self::compound(shapes))
1490 }
1491 }
1492 }
1493}
1494
1495#[cfg(feature = "collider-from-mesh")]
1496type VerticesIndices = (Vec<Vector>, Vec<[u32; 3]>);
1497
1498#[cfg(feature = "collider-from-mesh")]
1499fn extract_mesh_vertices_indices(mesh: &Mesh) -> Option<VerticesIndices> {
1500 let vertices = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?;
1501 let indices = mesh.indices()?;
1502
1503 let vtx: Vec<_> = match vertices {
1504 VertexAttributeValues::Float32(vtx) => Some(
1505 vtx.chunks(3)
1506 .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1507 .collect(),
1508 ),
1509 VertexAttributeValues::Float32x3(vtx) => Some(
1510 vtx.iter()
1511 .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1512 .collect(),
1513 ),
1514 _ => None,
1515 }?;
1516
1517 let idx = match indices {
1518 Indices::U16(idx) => idx
1519 .chunks_exact(3)
1520 .map(|i| [i[0] as u32, i[1] as u32, i[2] as u32])
1521 .collect(),
1522 Indices::U32(idx) => idx.chunks_exact(3).map(|i| [i[0], i[1], i[2]]).collect(),
1523 };
1524
1525 Some((vtx, idx))
1526}
1527
1528fn scale_shape(
1529 shape: &SharedShape,
1530 scale: Vector,
1531 num_subdivisions: u32,
1532) -> Result<SharedShape, UnsupportedShape> {
1533 let scale = scale.abs();
1534 match shape.as_typed_shape() {
1535 TypedShape::Cuboid(s) => Ok(SharedShape::new(s.scaled(scale.abs()))),
1536 TypedShape::RoundCuboid(s) => Ok(SharedShape::new(RoundShape {
1537 border_radius: s.border_radius,
1538 inner_shape: s.inner_shape.scaled(scale.abs()),
1539 })),
1540 TypedShape::Capsule(c) => match c.scaled(scale.abs(), num_subdivisions) {
1541 None => {
1542 log::error!("Failed to apply scale {} to Capsule shape.", scale);
1543 Ok(SharedShape::ball(0.0))
1544 }
1545 Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1546 Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1547 },
1548 TypedShape::Ball(b) => {
1549 #[cfg(feature = "2d")]
1550 {
1551 if scale.x == scale.y {
1552 Ok(SharedShape::ball(b.radius * scale.x.abs()))
1553 } else {
1554 Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1556 half_size: Vec2::splat(b.radius as f32) * scale.f32().abs(),
1557 })))
1558 }
1559 }
1560 #[cfg(feature = "3d")]
1561 match b.scaled(scale.abs(), num_subdivisions) {
1562 None => {
1563 log::error!("Failed to apply scale {} to Ball shape.", scale);
1564 Ok(SharedShape::ball(0.0))
1565 }
1566 Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1567 Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1568 }
1569 }
1570 TypedShape::Segment(s) => Ok(SharedShape::new(s.scaled(scale))),
1571 TypedShape::Triangle(t) => Ok(SharedShape::new(t.scaled(scale))),
1572 TypedShape::RoundTriangle(t) => Ok(SharedShape::new(RoundShape {
1573 border_radius: t.border_radius,
1574 inner_shape: t.inner_shape.scaled(scale),
1575 })),
1576 TypedShape::TriMesh(t) => Ok(SharedShape::new(t.clone().scaled(scale))),
1577 TypedShape::Polyline(p) => Ok(SharedShape::new(p.clone().scaled(scale))),
1578 TypedShape::HalfSpace(h) => match h.scaled(scale) {
1579 None => {
1580 log::error!("Failed to apply scale {} to HalfSpace shape.", scale);
1581 Ok(SharedShape::ball(0.0))
1582 }
1583 Some(scaled) => Ok(SharedShape::new(scaled)),
1584 },
1585 TypedShape::Voxels(v) => Ok(SharedShape::new(v.clone().scaled(scale))),
1586 TypedShape::HeightField(h) => Ok(SharedShape::new(h.clone().scaled(scale))),
1587 #[cfg(feature = "2d")]
1588 TypedShape::ConvexPolygon(cp) => match cp.clone().scaled(scale) {
1589 None => {
1590 log::error!("Failed to apply scale {} to ConvexPolygon shape.", scale);
1591 Ok(SharedShape::ball(0.0))
1592 }
1593 Some(scaled) => Ok(SharedShape::new(scaled)),
1594 },
1595 #[cfg(feature = "2d")]
1596 TypedShape::RoundConvexPolygon(cp) => match cp.inner_shape.clone().scaled(scale) {
1597 None => {
1598 log::error!(
1599 "Failed to apply scale {} to RoundConvexPolygon shape.",
1600 scale
1601 );
1602 Ok(SharedShape::ball(0.0))
1603 }
1604 Some(scaled) => Ok(SharedShape::new(RoundShape {
1605 border_radius: cp.border_radius,
1606 inner_shape: scaled,
1607 })),
1608 },
1609 #[cfg(feature = "3d")]
1610 TypedShape::ConvexPolyhedron(cp) => match cp.clone().scaled(scale) {
1611 None => {
1612 log::error!("Failed to apply scale {} to ConvexPolyhedron shape.", scale);
1613 Ok(SharedShape::ball(0.0))
1614 }
1615 Some(scaled) => Ok(SharedShape::new(scaled)),
1616 },
1617 #[cfg(feature = "3d")]
1618 TypedShape::RoundConvexPolyhedron(cp) => match cp.clone().inner_shape.scaled(scale) {
1619 None => {
1620 log::error!(
1621 "Failed to apply scale {} to RoundConvexPolyhedron shape.",
1622 scale
1623 );
1624 Ok(SharedShape::ball(0.0))
1625 }
1626 Some(scaled) => Ok(SharedShape::new(RoundShape {
1627 border_radius: cp.border_radius,
1628 inner_shape: scaled,
1629 })),
1630 },
1631 #[cfg(feature = "3d")]
1632 TypedShape::Cylinder(c) => match c.scaled(scale.abs(), num_subdivisions) {
1633 None => {
1634 log::error!("Failed to apply scale {} to Cylinder shape.", scale);
1635 Ok(SharedShape::ball(0.0))
1636 }
1637 Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1638 Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1639 },
1640 #[cfg(feature = "3d")]
1641 TypedShape::RoundCylinder(c) => match c.inner_shape.scaled(scale.abs(), num_subdivisions) {
1642 None => {
1643 log::error!("Failed to apply scale {} to RoundCylinder shape.", scale);
1644 Ok(SharedShape::ball(0.0))
1645 }
1646 Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1647 border_radius: c.border_radius,
1648 inner_shape: scaled,
1649 })),
1650 Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1651 border_radius: c.border_radius,
1652 inner_shape: scaled,
1653 })),
1654 },
1655 #[cfg(feature = "3d")]
1656 TypedShape::Cone(c) => match c.scaled(scale, num_subdivisions) {
1657 None => {
1658 log::error!("Failed to apply scale {} to Cone shape.", scale);
1659 Ok(SharedShape::ball(0.0))
1660 }
1661 Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1662 Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1663 },
1664 #[cfg(feature = "3d")]
1665 TypedShape::RoundCone(c) => match c.inner_shape.scaled(scale, num_subdivisions) {
1666 None => {
1667 log::error!("Failed to apply scale {} to RoundCone shape.", scale);
1668 Ok(SharedShape::ball(0.0))
1669 }
1670 Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1671 border_radius: c.border_radius,
1672 inner_shape: scaled,
1673 })),
1674 Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1675 border_radius: c.border_radius,
1676 inner_shape: scaled,
1677 })),
1678 },
1679 TypedShape::Compound(c) => {
1680 let mut scaled = Vec::with_capacity(c.shapes().len());
1681
1682 for (pose, shape) in c.shapes() {
1683 #[cfg(feature = "2d")]
1684 scaled.push((
1685 make_pose(
1686 pose.translation * scale,
1687 Rotation::radians(pose.rotation.angle()),
1688 ),
1689 scale_shape(shape, scale, num_subdivisions)?,
1690 ));
1691 #[cfg(feature = "3d")]
1692 scaled.push((
1693 make_pose(pose.translation * scale, pose.rotation),
1694 scale_shape(shape, scale, num_subdivisions)?,
1695 ));
1696 }
1697 Ok(SharedShape::compound(scaled))
1698 }
1699 TypedShape::Custom(_shape) => {
1700 #[cfg(feature = "2d")]
1701 {
1702 if let Some(ellipse) = _shape.as_shape::<EllipseColliderShape>() {
1703 return Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1704 half_size: ellipse.half_size * scale.f32().abs(),
1705 })));
1706 }
1707 if let Some(polygon) = _shape.as_shape::<RegularPolygonColliderShape>() {
1708 if scale.x == scale.y {
1709 return Ok(SharedShape::new(RegularPolygonColliderShape(
1710 RegularPolygon::new(
1711 polygon.circumradius() * scale.x.abs() as f32,
1712 polygon.sides,
1713 ),
1714 )));
1715 } else {
1716 let vertices = polygon
1717 .vertices(0.0)
1718 .into_iter()
1719 .map(|v| v.adjust_precision())
1720 .collect::<Vec<_>>();
1721
1722 return scale_shape(
1723 &SharedShape::convex_hull(&vertices).unwrap(),
1724 scale,
1725 num_subdivisions,
1726 );
1727 }
1728 }
1729 }
1730 Err(parry::query::Unsupported)
1731 }
1732 }
1733}
1734
1735#[cfg(all(test, feature = "3d"))]
1736mod tests {
1737 use super::*;
1738 use approx::assert_relative_eq;
1739
1740 #[test]
1741 fn test_flatten_compound_constructors() {
1742 let input = vec![
1743 (
1744 Position(Vector::new(10.0, 0.0, 0.0)),
1745 Rotation::default(),
1746 ColliderConstructor::Sphere { radius: 1.0 },
1747 ),
1748 (
1749 Position(Vector::new(5.0, 0.0, 0.0)),
1750 Rotation::from(Quaternion::from_rotation_z(PI / 2.0)),
1751 ColliderConstructor::Compound(vec![
1752 (
1753 Position(Vector::new(2.0, 0.0, 0.0)),
1754 Rotation::from(Quaternion::from_rotation_y(PI)),
1755 ColliderConstructor::Compound(vec![(
1756 Position(Vector::new(1.0, 0.0, 0.0)),
1757 Rotation::default(),
1758 ColliderConstructor::Sphere { radius: 0.5 },
1759 )]),
1760 ),
1761 (
1762 Position(Vector::new(0.0, 3.0, 0.0)),
1763 Rotation::default(),
1764 ColliderConstructor::Sphere { radius: 0.25 },
1765 ),
1766 ]),
1767 ),
1768 ];
1769
1770 let flattened = ColliderConstructor::flatten_compound_constructors(input);
1771 assert_eq!(flattened.len(), 3);
1772
1773 let unchanged_simple_sphere = &flattened[0];
1774 let flattened_grandchild = &flattened[1];
1775 let flattened_sibling = &flattened[2];
1776
1777 assert_eq!(
1779 unchanged_simple_sphere.0,
1780 Position(Vector::new(10.0, 0.0, 0.0))
1781 );
1782 assert_eq!(unchanged_simple_sphere.1, Rotation::default());
1783
1784 let expected_grandchild_world_pos = Vector::new(5.0, 1.0, 0.0);
1790 let actual_grandchild_world_pos = flattened_grandchild.0.0;
1791
1792 assert_relative_eq!(
1793 actual_grandchild_world_pos.x,
1794 expected_grandchild_world_pos.x,
1795 epsilon = 1e-6
1796 );
1797 assert_relative_eq!(
1798 actual_grandchild_world_pos.y,
1799 expected_grandchild_world_pos.y,
1800 epsilon = 1e-6
1801 );
1802 assert_relative_eq!(
1803 actual_grandchild_world_pos.z,
1804 expected_grandchild_world_pos.z,
1805 epsilon = 1e-6
1806 );
1807
1808 let expected_sibling_world_pos = Vector::new(2.0, 0.0, 0.0);
1812 let actual_sibling_world_pos = flattened_sibling.0.0;
1813
1814 assert_relative_eq!(
1815 actual_sibling_world_pos.x,
1816 expected_sibling_world_pos.x,
1817 epsilon = 1e-6
1818 );
1819 assert_relative_eq!(
1820 actual_sibling_world_pos.y,
1821 expected_sibling_world_pos.y,
1822 epsilon = 1e-6
1823 );
1824 assert_relative_eq!(
1825 actual_sibling_world_pos.z,
1826 expected_sibling_world_pos.z,
1827 epsilon = 1e-6
1828 );
1829 }
1830}