Skip to main content

parry3d/shape/
compound.rs

1//!
2//! Shape composed from the union of primitives.
3//!
4
5use crate::bounding_volume::{Aabb, BoundingSphere, BoundingVolume};
6use crate::math::Pose;
7use crate::math::{Real, Vector};
8use crate::partitioning::{Bvh, BvhBuildStrategy};
9use crate::query::details::NormalConstraints;
10#[cfg(feature = "dim2")]
11use crate::shape::compound_pseudo_normals::turns_clockwise;
12#[cfg(feature = "dim2")]
13use crate::shape::CompoundEdgeCone;
14#[cfg(feature = "dim3")]
15use crate::shape::CompoundFaceCone;
16use crate::shape::CompoundPseudoNormals;
17use crate::shape::{CompositeShape, Shape, SharedShape, TypedCompositeShape};
18#[cfg(feature = "dim2")]
19use crate::shape::{ConvexPolygon, TriMesh, Triangle};
20#[cfg(feature = "dim2")]
21use crate::transformation::hertel_mehlhorn;
22use crate::utils::hashmap::HashMap;
23use alloc::vec::Vec;
24
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[repr(C)]
27#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
28/// Controls how a [`Compound`] is loaded.
29pub struct CompoundFlags(u8);
30
31bitflags::bitflags! {
32    impl CompoundFlags: u8 {
33        /// If set, the edges (2D) or faces (3D) where two parts meet are treated as interior to
34        /// the union, and contact normals are clamped to the surviving outline. This removes the
35        /// ledge a body catches on when it slides across the cut between two parts of a convex
36        /// decomposition.
37        ///
38        /// This is the [`Compound`] counterpart of `TriMeshFlags::FIX_INTERNAL_EDGES`, and like it
39        /// costs a one-off pass over the parts' edges when set.
40        const FIX_INTERNAL_EDGES = 1;
41    }
42}
43
44/// A compound shape with an aabb bounding volume.
45///
46/// A compound shape is a shape composed of the union of several simpler shape. This is
47/// the main way of creating a concave shape from convex parts. Each parts can have its own
48/// delta transformation to shift or rotate it with regard to the other shapes.
49#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50#[derive(Clone, Debug)]
51pub struct Compound {
52    shapes: Vec<(Pose, SharedShape)>,
53    bvh: Bvh,
54    aabbs: Vec<Aabb>,
55    aabb: Aabb,
56    #[cfg_attr(feature = "serde", serde(default))]
57    flags: CompoundFlags,
58    /// `None` for a part that constrains nothing: one that is not polygonal, or any part at all
59    /// while [`CompoundFlags::FIX_INTERNAL_EDGES`] is unset.
60    #[cfg_attr(feature = "serde", serde(default))]
61    pseudo_normals: Option<Vec<Option<CompoundPseudoNormals>>>,
62}
63
64impl Compound {
65    /// The tolerance [`Compound::set_flags`] welds part corners with, in ULPs.
66    ///
67    /// Two spellings of the same corner drift by about one ULP per rounding, and a corner reaches
68    /// the surface through a rotation and a translation, so a handful of ULPs covers the seams a
69    /// shared grid produces while staying far below any real feature.
70    pub const DEFAULT_WELD_TOLERANCE: Real = 4.0;
71
72    /// Builds a new compound shape from a collection of sub-shapes.
73    ///
74    /// A compound shape combines multiple primitive shapes into a single composite shape,
75    /// each with its own relative position and orientation. This is the primary way to
76    /// create concave shapes from convex primitives. The compound internally builds a
77    /// Bounding Volume Hierarchy (BVH) for efficient collision detection.
78    ///
79    /// # Arguments
80    ///
81    /// * `shapes` - A vector of (position, shape) pairs. Each pair defines:
82    ///   - An [`Pose`] representing the sub-shape's position and orientation relative to the compound's origin
83    ///   - A [`SharedShape`] containing the actual geometry
84    ///
85    /// # Panics
86    ///
87    /// - If the input vector is empty (a compound must contain at least one shape)
88    /// - If any of the provided shapes are themselves composite shapes (nested composites are not allowed)
89    ///
90    /// # Performance
91    ///
92    /// The BVH is built using a binned construction strategy optimized for static scenes.
93    /// For large compounds (100+ shapes), construction may take noticeable time but provides
94    /// excellent query performance.
95    ///
96    /// # Example
97    ///
98    /// ```
99    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
100    /// # use parry3d::shape::{Compound, Ball, Cuboid, SharedShape};
101    /// use parry3d::math::Pose;
102    /// use parry3d::math::Vector;
103    ///
104    /// // Create a compound shape resembling a dumbbell
105    /// let shapes = vec![
106    ///     // Left sphere
107    ///     (
108    ///         Pose::translation(-2.0, 0.0, 0.0),
109    ///         SharedShape::new(Ball::new(0.5))
110    ///     ),
111    ///     // Center bar
112    ///     (
113    ///         Pose::identity(),
114    ///         SharedShape::new(Cuboid::new(Vector::new(2.0, 0.2, 0.2)))
115    ///     ),
116    ///     // Right sphere
117    ///     (
118    ///         Pose::translation(2.0, 0.0, 0.0),
119    ///         SharedShape::new(Ball::new(0.5))
120    ///     ),
121    /// ];
122    ///
123    /// let compound = Compound::new(shapes);
124    ///
125    /// // The compound now contains all three shapes
126    /// assert_eq!(compound.shapes().len(), 3);
127    /// # }
128    /// ```
129    ///
130    /// ```
131    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
132    /// # use parry2d::shape::{Compound, Ball, Cuboid, SharedShape};
133    /// # use parry2d::math::Pose;
134    /// # use parry2d::math::Vector;
135    ///
136    /// // Create an L-shaped compound
137    /// let shapes = vec![
138    ///     // Vertical rectangle
139    ///     (
140    ///         Pose::translation(0.0, 1.0),
141    ///         SharedShape::new(Cuboid::new(Vector::new(0.5, 1.0)))
142    ///     ),
143    ///     // Horizontal rectangle
144    ///     (
145    ///         Pose::translation(1.0, 0.0),
146    ///         SharedShape::new(Cuboid::new(Vector::new(1.0, 0.5)))
147    ///     ),
148    /// ];
149    ///
150    /// let l_shape = Compound::new(shapes);
151    /// assert_eq!(l_shape.shapes().len(), 2);
152    /// # }
153    /// ```
154    pub fn new(shapes: Vec<(Pose, SharedShape)>) -> Compound {
155        assert!(
156            !shapes.is_empty(),
157            "A compound shape must contain at least one shape."
158        );
159        let mut aabbs = Vec::new();
160        let mut leaves = Vec::new();
161        let mut aabb = Aabb::new_invalid();
162
163        for (i, (delta, shape)) in shapes.iter().enumerate() {
164            let bv = shape.compute_aabb(delta);
165
166            aabb.merge(&bv);
167            aabbs.push(bv);
168            leaves.push((i, bv));
169
170            if shape.as_composite_shape().is_some() {
171                panic!("Nested composite shapes are not allowed.");
172            }
173        }
174
175        // NOTE: we apply no dilation factor because we won't
176        // update this tree dynamically.
177        let bvh = Bvh::from_iter(BvhBuildStrategy::Binned, leaves);
178
179        Compound {
180            shapes,
181            bvh,
182            aabbs,
183            aabb,
184            flags: CompoundFlags::empty(),
185            pseudo_normals: None,
186        }
187    }
188
189    /// Builds a new compound shape from a collection of sub-shapes, with the given
190    /// [`CompoundFlags`] already applied.
191    ///
192    /// `weld_tolerance` is [`Compound::set_flags`]'s, and `None` selects
193    /// [`Compound::DEFAULT_WELD_TOLERANCE`].
194    ///
195    /// # Example
196    ///
197    /// ```rust
198    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
199    /// use parry2d::math::{Pose, Vector};
200    /// use parry2d::shape::{Compound, CompoundFlags, Cuboid, SharedShape};
201    ///
202    /// // Two boxes meeting along one edge, as a decomposition or a voxel grid leaves them.
203    /// let box_at = |x: f32| {
204    ///     (
205    ///         Pose::from_parts(Vector::new(x, 0.0), Default::default()),
206    ///         SharedShape::new(Cuboid::new(Vector::new(0.5, 0.5))),
207    ///     )
208    /// };
209    /// let compound = Compound::with_flags(
210    ///     vec![box_at(0.0), box_at(1.0)],
211    ///     CompoundFlags::FIX_INTERNAL_EDGES,
212    ///     None,
213    /// );
214    ///
215    /// // The edge they share is interior to the union, so neither part reports it.
216    /// assert_eq!(compound.part_normal_constraints(0).unwrap().boundary_edges.len(), 3);
217    /// # }
218    /// ```
219    pub fn with_flags(
220        shapes: Vec<(Pose, SharedShape)>,
221        flags: CompoundFlags,
222        weld_tolerance: Option<Real>,
223    ) -> Compound {
224        let mut compound = Self::new(shapes);
225        compound.set_flags(flags, weld_tolerance);
226        compound
227    }
228
229    /// Sets the [`CompoundFlags`], computing or discarding the compound's optional associated data.
230    ///
231    /// `weld_tolerance` says how far apart two part corners may be and still be treated as the same
232    /// point, in ULPs; `None` selects [`Compound::DEFAULT_WELD_TOLERANCE`].
233    ///
234    /// Parts of a decomposition meet on corners each computes for itself, and two spellings of the
235    /// same corner need not round to the same bits: a row of boxes on a shared grid, or any part
236    /// placed through a rotation, routinely leaves its seams a few ULPs apart. Corners that far
237    /// apart are welded so the edge or face between them is still recognized as shared.
238    ///
239    /// The tolerance is relative to each corner's own distance from the origin, not the whole
240    /// compound's, so detail near the origin keeps a tolerance of its own magnitude however far the
241    /// rest of the compound reaches. Zero leaves only corners at the very same coordinates welded.
242    ///
243    /// Only [`CompoundFlags::FIX_INTERNAL_EDGES`] reads the tolerance; without it nothing is
244    /// computed and the value is ignored.
245    pub fn set_flags(&mut self, flags: CompoundFlags, weld_tolerance: Option<Real>) {
246        self.flags = flags;
247
248        if flags.contains(CompoundFlags::FIX_INTERNAL_EDGES) {
249            self.compute_pseudo_normals(weld_tolerance.unwrap_or(Self::DEFAULT_WELD_TOLERANCE));
250        } else {
251            self.pseudo_normals = None;
252        }
253    }
254
255    /// The [`CompoundFlags`] controlling this compound's optional associated data.
256    pub fn flags(&self) -> CompoundFlags {
257        self.flags
258    }
259
260    /// The parts' outlines in the compound's frame, wound counter-clockwise. `None` for a part
261    /// with no straight sides, which therefore shares none.
262    #[cfg(feature = "dim2")]
263    fn part_outlines(&self) -> Vec<Option<Vec<Vector>>> {
264        self.shapes
265            .iter()
266            .map(|(part_pos, part)| {
267                let corners: Vec<Vector> = if let Some(polygon) = part.as_convex_polygon() {
268                    polygon.points().to_vec()
269                } else if let Some(cuboid) = part.as_cuboid() {
270                    cuboid.to_polyline().to_vec()
271                } else {
272                    let triangle = part.as_triangle()?;
273                    alloc::vec![triangle.a, triangle.b, triangle.c]
274                };
275
276                let mut outline: Vec<Vector> = corners
277                    .into_iter()
278                    .map(|corner| part_pos.transform_point(corner))
279                    .collect();
280
281                if signed_area(&outline) < 0.0 {
282                    outline.reverse();
283                }
284
285                Some(outline)
286            })
287            .collect()
288    }
289
290    /// Builds the normal cones every part may report contacts in, from the outline of the union
291    /// rather than from each part's own faces.
292    ///
293    /// An edge two parts share is a cut, so it is dropped, and each surviving edge opens towards
294    /// the edge next to it along the union's outline, which a cut may have left in another part.
295    /// That closes the corner where a surface runs into a cut -- the ledge a body would otherwise
296    /// catch on -- without closing one the cut merely passes through.
297    #[cfg(feature = "dim2")]
298    fn compute_pseudo_normals(&mut self, weld_tolerance: Real) {
299        let outlines = self.part_outlines();
300
301        // Weld every part's corners into one vertex numbering, so two parts meeting along an edge
302        // key it on the same pair of vertices even though each rounded its own corners.
303        let corners: Vec<Vector> = outlines.iter().flatten().flatten().copied().collect();
304        let welded = weld_corners(&corners, weld_tolerance);
305
306        let mut vertex_ids: Vec<Option<Vec<u32>>> = Vec::with_capacity(outlines.len());
307        let mut next = 0;
308        for outline in &outlines {
309            match outline {
310                Some(outline) => {
311                    vertex_ids.push(Some(welded[next..next + outline.len()].to_vec()));
312                    next += outline.len();
313                }
314                None => vertex_ids.push(None),
315            }
316        }
317
318        let edge_key = |a: u32, b: u32| if a <= b { (a, b) } else { (b, a) };
319
320        let mut parts_sharing_edge = HashMap::default();
321        for ids in vertex_ids.iter().flatten() {
322            for (a, b) in outline_edge_ids(ids) {
323                *parts_sharing_edge.entry(edge_key(a, b)).or_insert(0u32) += 1;
324            }
325        }
326
327        // Per part, the outward normal of every outline edge, or `None` where a sibling part covers
328        // it: that edge is a cut, interior to the union.
329        let boundary_normals: Vec<Option<Vec<Option<Vector>>>> = outlines
330            .iter()
331            .zip(vertex_ids.iter())
332            .map(|(outline, ids)| {
333                let (outline, ids) = (outline.as_ref()?, ids.as_ref()?);
334
335                Some(
336                    outline_edges(outline)
337                        .zip(outline_edge_ids(ids))
338                        .map(|((a, b), (id_a, id_b))| {
339                            let shared = parts_sharing_edge
340                                .get(&edge_key(id_a, id_b))
341                                .is_some_and(|parts| *parts > 1);
342
343                            (!shared)
344                                .then(|| crate::utils::ccw_face_normal([a, b]))
345                                .flatten()
346                        })
347                        .collect(),
348                )
349            })
350            .collect();
351
352        // The union's boundary edge arriving at, and the one leaving, each corner. Gathered across
353        // every part, so a corner a cut splits still finds the neighbour that continues the
354        // outline into the sibling part. `None` marks a corner more than one edge claims, where no
355        // single neighbour can be picked.
356        let mut arriving = HashMap::default();
357        let mut leaving = HashMap::default();
358        for (ids, normals) in vertex_ids.iter().zip(boundary_normals.iter()) {
359            let (Some(ids), Some(normals)) = (ids.as_ref(), normals.as_ref()) else {
360                continue;
361            };
362
363            for ((a, b), normal) in outline_edge_ids(ids).zip(normals.iter()) {
364                let Some(normal) = *normal else {
365                    continue;
366                };
367
368                let _ = arriving
369                    .entry(b)
370                    .and_modify(|slot| *slot = None)
371                    .or_insert(Some(normal));
372                let _ = leaving
373                    .entry(a)
374                    .and_modify(|slot| *slot = None)
375                    .or_insert(Some(normal));
376            }
377        }
378
379        let pseudo_normals = self
380            .shapes
381            .iter()
382            .zip(vertex_ids.iter())
383            .zip(boundary_normals.iter())
384            .map(|(((part_pos, _), ids), normals)| {
385                let (ids, normals) = (ids.as_ref()?, normals.as_ref()?);
386
387                let into_part = part_pos.rotation.inverse();
388                let mut boundary_edges = Vec::new();
389
390                for ((a, b), face) in outline_edge_ids(ids).zip(normals.iter()) {
391                    let Some(face) = *face else {
392                        continue;
393                    };
394
395                    // Halfway, so the two edges at a corner split its range and neither cone grows
396                    // past a half turn, which the containment test could not represent.
397                    let halfway_to = |neighbour: Option<Vector>| match neighbour {
398                        Some(neighbour) => (face + neighbour).normalize_or(face),
399                        None => face,
400                    };
401
402                    // A concave corner has no outward range for its two edges to share, so the cone
403                    // stops on this edge's own normal, as it does where the surface runs into a cut.
404                    // Walking counter-clockwise turns the normals the same way, so the edge arriving
405                    // at a corner lies clockwise of the one leaving it.
406                    let clockwise = arriving
407                        .get(&a)
408                        .copied()
409                        .flatten()
410                        .filter(|previous| !turns_clockwise(*previous, face));
411                    let counter_clockwise = leaving
412                        .get(&b)
413                        .copied()
414                        .flatten()
415                        .filter(|next| !turns_clockwise(face, *next));
416
417                    // Cones are stated in the part's frame, which is where the narrow phase applies
418                    // them.
419                    boundary_edges.push(CompoundEdgeCone {
420                        face: into_part * face,
421                        clockwise_limit: into_part * halfway_to(clockwise),
422                        counter_clockwise_limit: into_part * halfway_to(counter_clockwise),
423                    });
424                }
425
426                Some(CompoundPseudoNormals { boundary_edges })
427            })
428            .collect();
429
430        self.pseudo_normals = Some(pseudo_normals);
431    }
432
433    /// The parts' faces in the compound's frame, each an outward normal with its vertex loop wound
434    /// counter-clockwise around it. `None` for a part with no flat faces, which therefore shares
435    /// none.
436    #[cfg(feature = "dim3")]
437    #[allow(clippy::type_complexity)]
438    fn part_faces(&self) -> Vec<Option<Vec<(Vector, Vec<Vector>)>>> {
439        self.shapes
440            .iter()
441            .map(|(part_pos, part)| {
442                let faces = if let Some(polyhedron) = part.as_convex_polyhedron() {
443                    let points = polyhedron.points();
444                    let corner_indices = polyhedron.vertices_adj_to_face();
445
446                    polyhedron
447                        .faces()
448                        .iter()
449                        .map(|face| {
450                            let first = face.first_vertex_or_edge as usize;
451                            let corners = corner_indices
452                                [first..first + face.num_vertices_or_edges as usize]
453                                .iter()
454                                .map(|index| points[*index as usize])
455                                .collect();
456                            (face.normal, corners)
457                        })
458                        .collect()
459                } else {
460                    // A cuboid is a polyhedron the type system does not call one, and a row of them
461                    // is how a voxel grid reaches a compound.
462                    cuboid_faces(part.as_cuboid()?.half_extents)
463                };
464
465                Some(
466                    faces
467                        .into_iter()
468                        .map(|(normal, corners): (Vector, Vec<Vector>)| {
469                            (
470                                part_pos.rotation * normal,
471                                corners
472                                    .into_iter()
473                                    .map(|corner| part_pos.transform_point(corner))
474                                    .collect(),
475                            )
476                        })
477                        .collect(),
478                )
479            })
480            .collect()
481    }
482
483    /// Builds the normal cones every part may report contacts in, from the surface of the union
484    /// rather than from each part's own faces.
485    ///
486    /// A face two parts share is a cut, so it is dropped, and each edge of a surviving face opens
487    /// towards the boundary face across it, which a cut may have left in another part. That closes
488    /// the corner where a surface runs into a cut -- the ledge a body would otherwise catch on --
489    /// without closing one the cut merely passes through.
490    #[cfg(feature = "dim3")]
491    fn compute_pseudo_normals(&mut self, weld_tolerance: Real) {
492        let part_faces = self.part_faces();
493
494        // Weld every part's corners into one vertex numbering, so two parts meeting along a face
495        // key it on the same set of vertices even though each rounded its own corners.
496        let corners: Vec<Vector> = part_faces
497            .iter()
498            .flatten()
499            .flatten()
500            .flat_map(|(_, corners)| corners.iter().copied())
501            .collect();
502        let welded = weld_corners(&corners, weld_tolerance);
503
504        let mut vertex_ids: Vec<Option<Vec<Vec<u32>>>> = Vec::with_capacity(part_faces.len());
505        let mut next = 0;
506        for faces in &part_faces {
507            match faces {
508                Some(faces) => {
509                    let mut per_face = Vec::with_capacity(faces.len());
510                    for (_, corners) in faces {
511                        per_face.push(welded[next..next + corners.len()].to_vec());
512                        next += corners.len();
513                    }
514                    vertex_ids.push(Some(per_face));
515                }
516                None => vertex_ids.push(None),
517            }
518        }
519
520        // A shared face has the same vertices on both sides, in some rotation and possibly reversed,
521        // so the key ignores their order.
522        let face_key = |ids: &[u32]| {
523            let mut key = ids.to_vec();
524            key.sort_unstable();
525            key
526        };
527        let edge_key = |a: u32, b: u32| if a <= b { (a, b) } else { (b, a) };
528
529        let mut parts_sharing_face = HashMap::default();
530        for ids in vertex_ids.iter().flatten() {
531            for face in ids {
532                *parts_sharing_face.entry(face_key(face)).or_insert(0u32) += 1;
533            }
534        }
535
536        // Per part, whether each face lies on the union's outline rather than being a cut two
537        // parts share.
538        let is_boundary: Vec<Option<Vec<bool>>> = vertex_ids
539            .iter()
540            .map(|ids| {
541                Some(
542                    ids.as_ref()?
543                        .iter()
544                        .map(|face| {
545                            parts_sharing_face
546                                .get(&face_key(face))
547                                .is_none_or(|parts| *parts <= 1)
548                        })
549                        .collect(),
550                )
551            })
552            .collect();
553
554        // The boundary faces meeting along each edge. Gathered across every part, so an edge a cut
555        // splits still finds the face continuing the surface into the sibling part.
556        let mut faces_along_edge = HashMap::default();
557        for ((faces, ids), boundary) in part_faces
558            .iter()
559            .zip(vertex_ids.iter())
560            .zip(is_boundary.iter())
561        {
562            let (Some(faces), Some(ids), Some(boundary)) =
563                (faces.as_ref(), ids.as_ref(), boundary.as_ref())
564            else {
565                continue;
566            };
567
568            for (((normal, _), face), boundary) in faces.iter().zip(ids.iter()).zip(boundary.iter())
569            {
570                if !boundary {
571                    continue;
572                }
573
574                for i in 0..face.len() {
575                    faces_along_edge
576                        .entry(edge_key(face[i], face[(i + 1) % face.len()]))
577                        .or_insert_with(Vec::new)
578                        .push(*normal);
579                }
580            }
581        }
582
583        let pseudo_normals = self
584            .shapes
585            .iter()
586            .zip(part_faces.iter())
587            .zip(vertex_ids.iter())
588            .zip(is_boundary.iter())
589            .map(|((((part_pos, _), faces), ids), boundary)| {
590                let (faces, ids, boundary) = (faces.as_ref()?, ids.as_ref()?, boundary.as_ref()?);
591
592                let into_part = part_pos.rotation.inverse();
593                let mut boundary_faces = Vec::new();
594
595                for (((normal, corners), face), on_boundary) in
596                    faces.iter().zip(ids.iter()).zip(boundary.iter())
597                {
598                    if !on_boundary {
599                        continue;
600                    }
601
602                    let edge_pseudo_normals = (0..corners.len())
603                        .map(|i| {
604                            let next = (i + 1) % corners.len();
605                            let (from, to) = (corners[i], corners[next]);
606                            // Halfway to the boundary face across the edge, which may belong to
607                            // another part where a cut splits the surface along it. A concave edge
608                            // has no outward range for its two faces to share, so the cone stops on
609                            // this face's own normal, as it does at an edge whose other face was
610                            // cut away. Faces are wound counter-clockwise around their normal, so
611                            // the edge is convex exactly when the two normals turn that way too.
612                            let across = faces_along_edge
613                                .get(&edge_key(face[i], face[next]))
614                                .and_then(|normals| {
615                                    normals.iter().find(|other| **other != *normal).copied()
616                                })
617                                .filter(|across| normal.cross(*across).dot(to - from) >= 0.0)
618                                .unwrap_or(*normal);
619                            into_part * (*normal + across).normalize_or(*normal)
620                        })
621                        .collect();
622
623                    boundary_faces.push(CompoundFaceCone {
624                        face: into_part * *normal,
625                        edge_pseudo_normals,
626                    });
627                }
628
629                Some(CompoundPseudoNormals { boundary_faces })
630            })
631            .collect();
632
633        self.pseudo_normals = Some(pseudo_normals);
634    }
635
636    /// Returns the [`CompoundPseudoNormals`] of the part with index `i`.
637    ///
638    /// `None` unless this compound was given [`CompoundFlags::FIX_INTERNAL_EDGES`] and the part is
639    /// polygonal -- a part with curved sides has no edge that another part could cover, so it
640    /// constrains nothing.
641    pub fn part_normal_constraints(&self, i: u32) -> Option<&CompoundPseudoNormals> {
642        self.pseudo_normals.as_ref()?[i as usize].as_ref()
643    }
644
645    /// Creates a compound shape by decomposing a triangle mesh into convex polygons.
646    ///
647    /// This 2D-only method takes a [`TriMesh`] and merges adjacent triangles into larger
648    /// convex polygons using the Hertel-Mehlhorn algorithm. This is useful for creating
649    /// efficient collision shapes from arbitrary 2D meshes, as the resulting compound
650    /// has fewer sub-shapes than using individual triangles.
651    ///
652    /// The algorithm works by:
653    /// 1. Starting with all triangles from the input mesh
654    /// 2. Merging adjacent triangles if the result is still convex
655    /// 3. Creating a compound from the resulting convex polygons
656    ///
657    /// # Arguments
658    ///
659    /// * `trimesh` - A reference to the triangle mesh to decompose
660    ///
661    /// # Returns
662    ///
663    /// * `Some(Compound)` - A compound shape containing the convex polygons
664    /// * `None` - If any of the created shapes has zero or near-zero area
665    ///
666    /// # Performance
667    ///
668    /// This decomposition is typically much more efficient than using the raw triangle mesh,
669    /// as it reduces the number of shapes from N triangles to potentially N/2 or fewer polygons.
670    ///
671    /// # Example
672    ///
673    /// ```
674    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
675    /// # use parry2d::shape::{Compound, TriMesh};
676    /// # use parry2d::math::Vector;
677    ///
678    /// // Create a simple square mesh (2 triangles)
679    /// let vertices = vec![
680    ///     Vector::ZERO,
681    ///     Vector::new(1.0, 0.0),
682    ///     Vector::new(1.0, 1.0),
683    ///     Vector::new(0.0, 1.0),
684    /// ];
685    ///
686    /// let indices = vec![
687    ///     [0, 1, 2],  // First triangle
688    ///     [0, 2, 3],  // Second triangle
689    /// ];
690    ///
691    /// let trimesh = TriMesh::new(vertices, indices).unwrap();
692    ///
693    /// // Decompose into convex polygons
694    /// if let Some(compound) = Compound::decompose_trimesh(&trimesh) {
695    ///     // The two triangles should be merged into one or two convex polygons
696    ///     assert!(compound.shapes().len() <= 2);
697    /// }
698    /// # }
699    /// ```
700    ///
701    /// # Example: Complex Shape
702    ///
703    /// ```
704    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
705    /// # use parry2d::shape::{Compound, TriMesh};
706    /// # use parry2d::math::Vector;
707    ///
708    /// // Create an L-shaped mesh
709    /// let vertices = vec![
710    ///     Vector::ZERO,
711    ///     Vector::new(2.0, 0.0),
712    ///     Vector::new(2.0, 1.0),
713    ///     Vector::new(1.0, 1.0),
714    ///     Vector::new(1.0, 2.0),
715    ///     Vector::new(0.0, 2.0),
716    /// ];
717    ///
718    /// let indices = vec![
719    ///     [0, 1, 2],
720    ///     [0, 2, 3],
721    ///     [0, 3, 4],
722    ///     [0, 4, 5],
723    /// ];
724    ///
725    /// let trimesh = TriMesh::new(vertices, indices).unwrap();
726    ///
727    /// // Decompose the L-shape into convex polygons
728    /// if let Some(compound) = Compound::decompose_trimesh(&trimesh) {
729    ///     // The result will have fewer shapes than the original 4 triangles
730    ///     assert!(compound.shapes().len() > 0);
731    ///     println!("Decomposed into {} convex polygons", compound.shapes().len());
732    /// }
733    /// # }
734    /// ```
735    #[cfg(feature = "dim2")]
736    pub fn decompose_trimesh(trimesh: &TriMesh) -> Option<Self> {
737        let polygons = hertel_mehlhorn(trimesh.vertices(), trimesh.indices());
738        let shapes: Option<Vec<_>> = polygons
739            .into_iter()
740            .map(|points| {
741                match points.len() {
742                    3 => {
743                        let triangle = Triangle::new(points[0], points[1], points[2]);
744                        Some(SharedShape::new(triangle))
745                    }
746                    _ => ConvexPolygon::from_convex_polyline(points).map(SharedShape::new),
747                }
748                .map(|shape| (Pose::IDENTITY, shape))
749            })
750            .collect();
751        Some(Self::new(shapes?))
752    }
753}
754
755impl Compound {
756    /// Returns a slice containing all sub-shapes and their positions in this compound.
757    ///
758    /// Each element in the returned slice is a tuple containing:
759    /// - The sub-shape's position and orientation ([`Pose`]) relative to the compound's origin
760    /// - The sub-shape itself ([`SharedShape`])
761    ///
762    /// The order of shapes matches the order they were provided to [`Compound::new`].
763    /// The index of each shape in this slice corresponds to its ID used in other operations
764    /// like BVH traversal.
765    ///
766    /// # Returns
767    ///
768    /// A slice of (isometry, shape) pairs representing all sub-shapes in this compound.
769    ///
770    /// # Example
771    ///
772    /// ```
773    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
774    /// # use parry3d::shape::{Compound, Ball, Cuboid, SharedShape};
775    /// use parry3d::math::Pose;
776    /// use parry3d::math::Vector;
777    ///
778    /// let shapes = vec![
779    ///     (Pose::translation(1.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
780    ///     (Pose::translation(-1.0, 0.0, 0.0), SharedShape::new(Ball::new(0.3))),
781    ///     (Pose::identity(), SharedShape::new(Cuboid::new(Vector::new(0.5, 0.5, 0.5)))),
782    /// ];
783    ///
784    /// let compound = Compound::new(shapes);
785    ///
786    /// // Access all shapes
787    /// assert_eq!(compound.shapes().len(), 3);
788    ///
789    /// // Inspect individual shapes
790    /// for (i, (position, shape)) in compound.shapes().iter().enumerate() {
791    ///     println!("Shape {} at position: {:?}", i, position.translation);
792    ///
793    ///     // Check if it's a ball
794    ///     if let Some(ball) = shape.as_ball() {
795    ///         println!("  Ball with radius: {}", ball.radius);
796    ///     }
797    /// }
798    /// # }
799    /// ```
800    ///
801    /// # Example: Modifying Sub-Shape Positions
802    ///
803    /// ```
804    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
805    /// # use parry3d::shape::{Compound, Ball, SharedShape};
806    /// use parry3d::math::Pose;
807    ///
808    /// let shapes = vec![
809    ///     (Pose::translation(0.0, 0.0, 0.0), SharedShape::new(Ball::new(1.0))),
810    ///     (Pose::translation(2.0, 0.0, 0.0), SharedShape::new(Ball::new(1.0))),
811    /// ];
812    ///
813    /// let compound = Compound::new(shapes);
814    ///
815    /// // Note: To modify positions, you need to create a new compound
816    /// let mut new_shapes: Vec<_> = compound.shapes()
817    ///     .iter()
818    ///     .map(|(pos, shape)| {
819    ///         // Shift all shapes up by 1 unit
820    ///         let new_pos = pos * Pose::translation(0.0, 1.0, 0.0);
821    ///         (new_pos, shape.clone())
822    ///     })
823    ///     .collect();
824    ///
825    /// let shifted_compound = Compound::new(new_shapes);
826    /// assert_eq!(shifted_compound.shapes().len(), 2);
827    /// # }
828    /// ```
829    #[inline]
830    pub fn shapes(&self) -> &[(Pose, SharedShape)] {
831        &self.shapes[..]
832    }
833
834    /// Returns the Axis-Aligned Bounding Box (AABB) of this compound in local space.
835    ///
836    /// The local AABB is the smallest axis-aligned box that contains all sub-shapes
837    /// in the compound, computed in the compound's local coordinate system. This AABB
838    /// is automatically computed when the compound is created and encompasses all
839    /// sub-shapes at their specified positions.
840    ///
841    /// # Returns
842    ///
843    /// A reference to the [`Aabb`] representing the compound's bounding box in local space.
844    ///
845    /// # Use Cases
846    ///
847    /// - **Broad-phase collision detection**: Quick rejection tests before detailed queries
848    /// - **Spatial partitioning**: Organizing compounds in larger spatial structures
849    /// - **Culling**: Determining if the compound is visible or relevant to a query
850    /// - **Size estimation**: Getting approximate dimensions of the compound
851    ///
852    /// # Example
853    ///
854    /// ```
855    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
856    /// # use parry3d::shape::{Compound, Ball, SharedShape};
857    /// use parry3d::math::Pose;
858    /// use parry3d::math::Vector;
859    ///
860    /// let shapes = vec![
861    ///     (Pose::translation(-2.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
862    ///     (Pose::translation(2.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
863    /// ];
864    ///
865    /// let compound = Compound::new(shapes);
866    /// let aabb = compound.local_aabb();
867    ///
868    /// // The AABB should contain both balls
869    /// // Left ball extends from -2.5 to -1.5 on X axis
870    /// // Right ball extends from 1.5 to 2.5 on X axis
871    /// assert!(aabb.mins.x <= -2.5);
872    /// assert!(aabb.maxs.x >= 2.5);
873    ///
874    /// // Check if a point is inside the AABB
875    /// assert!(aabb.contains_local_point(Vector::ZERO));
876    /// assert!(!aabb.contains_local_point(Vector::new(10.0, 0.0, 0.0)));
877    /// # }
878    /// ```
879    ///
880    /// # Example: Computing Compound Dimensions
881    ///
882    /// ```
883    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
884    /// # use parry3d::shape::{Compound, Cuboid, SharedShape};
885    /// use parry3d::math::Pose;
886    /// use parry3d::math::Vector;
887    ///
888    /// let shapes = vec![
889    ///     (Pose::identity(), SharedShape::new(Cuboid::new(Vector::new(1.0, 1.0, 1.0)))),
890    ///     (Pose::translation(3.0, 0.0, 0.0), SharedShape::new(Cuboid::new(Vector::new(0.5, 0.5, 0.5)))),
891    /// ];
892    ///
893    /// let compound = Compound::new(shapes);
894    /// let aabb = compound.local_aabb();
895    ///
896    /// // Calculate the total dimensions
897    /// let dimensions = aabb.maxs - aabb.mins;
898    /// println!("Compound dimensions: {:?}", dimensions);
899    ///
900    /// // The compound extends from -1.0 to 3.5 on the X axis (4.5 units total)
901    /// assert!((dimensions.x - 4.5).abs() < 1e-5);
902    /// # }
903    /// ```
904    #[inline]
905    pub fn local_aabb(&self) -> &Aabb {
906        &self.aabb
907    }
908
909    /// Returns the bounding sphere of this compound in local space.
910    ///
911    /// The bounding sphere is the smallest sphere that contains all sub-shapes in the
912    /// compound. It is computed from the compound's AABB by finding the sphere that
913    /// tightly encloses that box. This provides a simple, rotation-invariant bounding
914    /// volume useful for certain collision detection algorithms.
915    ///
916    /// # Returns
917    ///
918    /// A [`BoundingSphere`] centered in local space that contains the entire compound.
919    ///
920    /// # Performance
921    ///
922    /// This method is very fast as it simply computes the bounding sphere from the
923    /// pre-computed AABB. The bounding sphere is not cached - it's computed on each call.
924    ///
925    /// # Comparison with AABB
926    ///
927    /// - **Bounding Sphere**: Rotation-invariant, simpler intersection tests, but often looser fit
928    /// - **AABB**: Tighter fit for axis-aligned objects, but must be recomputed when rotated
929    ///
930    /// # Example
931    ///
932    /// ```
933    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
934    /// # use parry3d::shape::{Compound, Ball, SharedShape};
935    /// use parry3d::math::Pose;
936    ///
937    /// let shapes = vec![
938    ///     (Pose::translation(-1.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
939    ///     (Pose::translation(1.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
940    /// ];
941    ///
942    /// let compound = Compound::new(shapes);
943    /// let bounding_sphere = compound.local_bounding_sphere();
944    ///
945    /// // The bounding sphere should contain both balls
946    /// println!("Center: {:?}", bounding_sphere.center());
947    /// println!("Radius: {}", bounding_sphere.radius());
948    ///
949    /// // The center should be near the origin
950    /// assert!(bounding_sphere.center().length() < 0.1);
951    ///
952    /// // The radius should be at least 1.5 (distance to ball edge: 1.0 + 0.5)
953    /// assert!(bounding_sphere.radius() >= 1.5);
954    /// # }
955    /// ```
956    ///
957    /// # Example: Using Bounding Sphere for Quick Rejection
958    ///
959    /// ```
960    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
961    /// # use parry3d::shape::{Compound, Cuboid, SharedShape};
962    /// use parry3d::math::{Pose, Vector};
963    ///
964    /// let shapes = vec![
965    ///     (Pose::identity(), SharedShape::new(Cuboid::new(Vector::new(1.0, 1.0, 1.0)))),
966    ///     (Pose::translation(2.0, 0.0, 0.0), SharedShape::new(Cuboid::new(Vector::new(0.5, 0.5, 0.5)))),
967    /// ];
968    ///
969    /// let compound = Compound::new(shapes);
970    /// let sphere = compound.local_bounding_sphere();
971    ///
972    /// // Quick test: is a point potentially inside the compound?
973    /// let test_point = Vector::new(5.0, 5.0, 5.0);
974    /// let distance_to_center = (test_point - sphere.center()).length();
975    ///
976    /// if distance_to_center > sphere.radius() {
977    ///     println!("Vector is definitely outside the compound");
978    ///     assert!(distance_to_center > sphere.radius());
979    /// } else {
980    ///     println!("Vector might be inside - need detailed check");
981    /// }
982    /// # }
983    /// ```
984    #[inline]
985    pub fn local_bounding_sphere(&self) -> BoundingSphere {
986        self.aabb.bounding_sphere()
987    }
988
989    /// Returns a slice of AABBs, one for each sub-shape in this compound.
990    ///
991    /// Each AABB in the returned slice corresponds to the bounding box of a sub-shape,
992    /// transformed to the compound's local coordinate system. The AABBs are stored in
993    /// the same order as the shapes returned by [`Compound::shapes`], so index `i` in
994    /// this slice corresponds to shape `i`.
995    ///
996    /// These AABBs are used internally by the BVH for efficient spatial queries and
997    /// collision detection. They are pre-computed during compound construction.
998    ///
999    /// # Returns
1000    ///
1001    /// A slice of [`Aabb`] representing the local-space bounding boxes of each sub-shape.
1002    ///
1003    /// # Use Cases
1004    ///
1005    /// - Inspecting individual sub-shape bounds without accessing the shapes themselves
1006    /// - Custom spatial queries or culling operations
1007    /// - Debugging and visualization of the compound's structure
1008    /// - Understanding the BVH's leaf nodes
1009    ///
1010    /// # Example
1011    ///
1012    /// ```
1013    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
1014    /// # use parry3d::shape::{Compound, Ball, Cuboid, SharedShape};
1015    /// use parry3d::math::Pose;
1016    /// use parry3d::math::Vector;
1017    ///
1018    /// let shapes = vec![
1019    ///     (Pose::translation(-2.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1020    ///     (Pose::identity(), SharedShape::new(Cuboid::new(Vector::new(1.0, 1.0, 1.0)))),
1021    ///     (Pose::translation(3.0, 0.0, 0.0), SharedShape::new(Ball::new(0.3))),
1022    /// ];
1023    ///
1024    /// let compound = Compound::new(shapes);
1025    ///
1026    /// // Get AABBs for all sub-shapes
1027    /// let aabbs = compound.aabbs();
1028    /// assert_eq!(aabbs.len(), 3);
1029    ///
1030    /// // Inspect each AABB
1031    /// for (i, aabb) in aabbs.iter().enumerate() {
1032    ///     println!("Shape {} AABB:", i);
1033    ///     println!("  Min: {:?}", aabb.mins);
1034    ///     println!("  Max: {:?}", aabb.maxs);
1035    ///
1036    ///     let center = aabb.center();
1037    ///     let extents = aabb.half_extents();
1038    ///     println!("  Center: {:?}", center);
1039    ///     println!("  Half-extents: {:?}", extents);
1040    /// }
1041    /// # }
1042    /// ```
1043    ///
1044    /// # Example: Finding Sub-Shapes in a Region
1045    ///
1046    /// ```
1047    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
1048    /// # use parry3d::shape::{Compound, Ball, SharedShape};
1049    /// use parry3d::math::Pose;
1050    /// use parry3d::bounding_volume::Aabb;
1051    /// use parry3d::math::Vector;
1052    ///
1053    /// let shapes = vec![
1054    ///     (Pose::translation(-5.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1055    ///     (Pose::translation(0.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1056    ///     (Pose::translation(5.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1057    /// ];
1058    ///
1059    /// let compound = Compound::new(shapes);
1060    ///
1061    /// // Define a query point
1062    /// let query_point = Vector::ZERO;
1063    ///
1064    /// // Find which sub-shapes might contain this point
1065    /// let potentially_containing: Vec<usize> = compound.aabbs()
1066    ///     .iter()
1067    ///     .enumerate()
1068    ///     .filter(|(_, aabb)| aabb.contains_local_point(query_point))
1069    ///     .map(|(i, _)| i)
1070    ///     .collect();
1071    ///
1072    /// // Only the middle shape (index 1) should contain the origin
1073    /// assert_eq!(potentially_containing.len(), 1);
1074    /// assert_eq!(potentially_containing[0], 1);
1075    /// # }
1076    /// ```
1077    #[inline]
1078    pub fn aabbs(&self) -> &[Aabb] {
1079        &self.aabbs[..]
1080    }
1081
1082    /// Returns the Bounding Volume Hierarchy (BVH) used for efficient spatial queries.
1083    ///
1084    /// The BVH is an acceleration structure that organizes the sub-shapes hierarchically
1085    /// for fast collision detection and spatial queries. It enables logarithmic-time queries
1086    /// instead of linear searches through all sub-shapes. The BVH is automatically built
1087    /// when the compound is created using a binned construction strategy.
1088    ///
1089    /// # Returns
1090    ///
1091    /// A reference to the [`Bvh`] acceleration structure for this compound.
1092    ///
1093    /// # Use Cases
1094    ///
1095    /// - **Custom spatial queries**: Traverse the BVH for specialized collision detection
1096    /// - **Ray casting**: Efficiently find which sub-shapes intersect a ray
1097    /// - **AABB queries**: Find all sub-shapes intersecting a region
1098    /// - **Debugging**: Inspect the BVH structure and quality
1099    /// - **Performance analysis**: Understand query performance characteristics
1100    ///
1101    /// # Performance
1102    ///
1103    /// The BVH provides O(log n) query performance for most spatial operations, where n
1104    /// is the number of sub-shapes. For compounds with many shapes (100+), the BVH
1105    /// provides dramatic speedups compared to naive linear searches.
1106    ///
1107    /// # Example
1108    ///
1109    /// ```
1110    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
1111    /// # use parry3d::shape::{Compound, Ball, SharedShape};
1112    /// use parry3d::math::Pose;
1113    /// use parry3d::bounding_volume::Aabb;
1114    /// use parry3d::math::Vector;
1115    ///
1116    /// let shapes = vec![
1117    ///     (Pose::translation(-3.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1118    ///     (Pose::translation(0.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1119    ///     (Pose::translation(3.0, 0.0, 0.0), SharedShape::new(Ball::new(0.5))),
1120    /// ];
1121    ///
1122    /// let compound = Compound::new(shapes);
1123    /// let bvh = compound.bvh();
1124    ///
1125    /// // The BVH provides efficient hierarchical organization
1126    /// assert_eq!(bvh.leaf_count(), 3);
1127    /// println!("BVH root AABB: {:?}", bvh.root_aabb());
1128    /// # }
1129    /// ```
1130    ///
1131    /// # Example: Accessing BVH for Custom Queries
1132    ///
1133    /// ```
1134    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
1135    /// # use parry3d::shape::{Compound, Ball, SharedShape};
1136    /// use parry3d::math::Pose;
1137    ///
1138    /// let mut shapes = vec![];
1139    /// for i in 0..10 {
1140    ///     let x = i as f32 * 2.0;
1141    ///     shapes.push((
1142    ///         Pose::translation(x, 0.0, 0.0),
1143    ///         SharedShape::new(Ball::new(0.5))
1144    ///     ));
1145    /// }
1146    ///
1147    /// let compound = Compound::new(shapes);
1148    /// let bvh = compound.bvh();
1149    ///
1150    /// // The BVH organizes 10 shapes hierarchically
1151    /// assert_eq!(bvh.leaf_count(), 10);
1152    /// assert_eq!(compound.shapes().len(), 10);
1153    ///
1154    /// // Access the root AABB which bounds all shapes
1155    /// let root_aabb = bvh.root_aabb();
1156    /// println!("Root AABB spans from {:?} to {:?}", root_aabb.mins, root_aabb.maxs);
1157    /// # }
1158    /// ```
1159    #[inline]
1160    pub fn bvh(&self) -> &Bvh {
1161        &self.bvh
1162    }
1163}
1164
1165impl CompositeShape for Compound {
1166    #[inline]
1167    fn map_part_at(
1168        &self,
1169        shape_id: u32,
1170        f: &mut dyn FnMut(Option<&Pose>, &dyn Shape, Option<&dyn NormalConstraints>),
1171    ) {
1172        if let Some(shape) = self.shapes.get(shape_id as usize) {
1173            let constraints = self
1174                .part_normal_constraints(shape_id)
1175                .map(|c| c as &dyn NormalConstraints);
1176            f(Some(&shape.0), &*shape.1, constraints)
1177        }
1178    }
1179
1180    #[inline]
1181    fn bvh(&self) -> &Bvh {
1182        &self.bvh
1183    }
1184}
1185
1186/// The six faces of a cuboid in its own frame, each an outward normal with its corner loop wound
1187/// counter-clockwise around it.
1188#[cfg(feature = "dim3")]
1189#[allow(clippy::type_complexity)]
1190fn cuboid_faces(half_extents: Vector) -> Vec<(Vector, Vec<Vector>)> {
1191    let (x, y, z) = (half_extents.x, half_extents.y, half_extents.z);
1192    let corner = |sx: Real, sy: Real, sz: Real| Vector::new(sx * x, sy * y, sz * z);
1193
1194    alloc::vec![
1195        (
1196            Vector::X,
1197            alloc::vec![
1198                corner(1.0, -1.0, -1.0),
1199                corner(1.0, 1.0, -1.0),
1200                corner(1.0, 1.0, 1.0),
1201                corner(1.0, -1.0, 1.0),
1202            ]
1203        ),
1204        (
1205            -Vector::X,
1206            alloc::vec![
1207                corner(-1.0, -1.0, -1.0),
1208                corner(-1.0, -1.0, 1.0),
1209                corner(-1.0, 1.0, 1.0),
1210                corner(-1.0, 1.0, -1.0),
1211            ]
1212        ),
1213        (
1214            Vector::Y,
1215            alloc::vec![
1216                corner(-1.0, 1.0, -1.0),
1217                corner(-1.0, 1.0, 1.0),
1218                corner(1.0, 1.0, 1.0),
1219                corner(1.0, 1.0, -1.0),
1220            ]
1221        ),
1222        (
1223            -Vector::Y,
1224            alloc::vec![
1225                corner(-1.0, -1.0, -1.0),
1226                corner(1.0, -1.0, -1.0),
1227                corner(1.0, -1.0, 1.0),
1228                corner(-1.0, -1.0, 1.0),
1229            ]
1230        ),
1231        (
1232            Vector::Z,
1233            alloc::vec![
1234                corner(-1.0, -1.0, 1.0),
1235                corner(1.0, -1.0, 1.0),
1236                corner(1.0, 1.0, 1.0),
1237                corner(-1.0, 1.0, 1.0),
1238            ]
1239        ),
1240        (
1241            -Vector::Z,
1242            alloc::vec![
1243                corner(-1.0, -1.0, -1.0),
1244                corner(-1.0, 1.0, -1.0),
1245                corner(1.0, 1.0, -1.0),
1246                corner(1.0, -1.0, -1.0),
1247            ]
1248        ),
1249    ]
1250}
1251
1252/// Assigns every corner the index of the vertex it welds onto, so parts that meet are keyed on
1253/// the same point even when each computed it for itself.
1254///
1255/// Two corners weld when no coordinate differs by more than `tolerance` ULPs, measured at the
1256/// scale of whichever of the two lies further from the origin. That scale has to come from the
1257/// point rather than the coordinate: a corner reaching the outline through a rotation can land
1258/// near an axis, leaving a coordinate whose own magnitude is pure cancellation and says nothing
1259/// about the error in it, while its siblings still carry the magnitude the rounding happened at.
1260///
1261/// A tolerance of zero leaves only corners at the very same coordinates welded, matching what a
1262/// shared vertex buffer produces and nothing else. The comparison stays numeric there rather than
1263/// dropping to raw bits, so the two signed zeros still meet.
1264fn weld_corners(corners: &[Vector], tolerance: Real) -> Vec<u32> {
1265    let radius = |corner: &Vector| tolerance * Real::EPSILON * corner.abs().max_element();
1266
1267    // Union-find over the corners, so a chain of near-coincident corners collapses to one vertex
1268    // whichever order the pairs turn up in.
1269    let mut parent: Vec<u32> = (0..corners.len() as u32).collect();
1270    fn find(parent: &mut [u32], mut i: u32) -> u32 {
1271        while parent[i as usize] != i {
1272            parent[i as usize] = parent[parent[i as usize] as usize];
1273            i = parent[i as usize];
1274        }
1275        i
1276    }
1277
1278    let leaves: Vec<Aabb> = corners
1279        .iter()
1280        .map(|corner| Aabb::new(*corner, *corner))
1281        .collect();
1282    let bvh = Bvh::from_leaves(BvhBuildStrategy::Binned, &leaves);
1283
1284    for (i, corner) in corners.iter().enumerate() {
1285        // Querying every corner with its own radius finds every pair that should weld: the test is
1286        // symmetric and scaled by the larger of the two, so the larger corner's own query reaches
1287        // the smaller one even when the smaller one's radius would fall short.
1288        let reach = radius(corner);
1289        let query = Aabb::new(
1290            *corner - Vector::splat(reach),
1291            *corner + Vector::splat(reach),
1292        );
1293
1294        for j in bvh.intersect_aabb(&query) {
1295            let other = corners[j as usize];
1296            let scale = corner.abs().max_element().max(other.abs().max_element());
1297
1298            if (*corner - other).abs().max_element() <= tolerance * Real::EPSILON * scale {
1299                let (a, b) = (find(&mut parent, i as u32), find(&mut parent, j));
1300                parent[a as usize] = b;
1301            }
1302        }
1303    }
1304
1305    // Number the roots so the ids are dense.
1306    let mut ids = HashMap::default();
1307    (0..corners.len() as u32)
1308        .map(|i| {
1309            let root = find(&mut parent, i);
1310            let next = ids.len() as u32;
1311            *ids.entry(root).or_insert(next)
1312        })
1313        .collect()
1314}
1315
1316/// The edges of a closed outline, as consecutive vertex-index pairs wrapping back to the start.
1317#[cfg(feature = "dim2")]
1318fn outline_edge_ids(ids: &[u32]) -> impl Iterator<Item = (u32, u32)> + '_ {
1319    ids.iter()
1320        .enumerate()
1321        .map(move |(i, id)| (*id, ids[(i + 1) % ids.len()]))
1322}
1323
1324/// The edges of a closed outline, as consecutive corner pairs wrapping back to the start.
1325#[cfg(feature = "dim2")]
1326fn outline_edges(outline: &[Vector]) -> impl Iterator<Item = (Vector, Vector)> + '_ {
1327    outline
1328        .iter()
1329        .enumerate()
1330        .map(move |(i, corner)| (*corner, outline[(i + 1) % outline.len()]))
1331}
1332
1333/// Twice the area enclosed by a closed outline, negative when it is wound clockwise.
1334#[cfg(feature = "dim2")]
1335fn signed_area(outline: &[Vector]) -> Real {
1336    outline_edges(outline)
1337        .map(|(a, b)| a.x * b.y - a.y * b.x)
1338        .sum()
1339}
1340
1341impl TypedCompositeShape for Compound {
1342    type PartShape = dyn Shape;
1343    type PartNormalConstraints = CompoundPseudoNormals;
1344
1345    #[inline(always)]
1346    fn map_typed_part_at<T>(
1347        &self,
1348        i: u32,
1349        mut f: impl FnMut(Option<&Pose>, &Self::PartShape, Option<&Self::PartNormalConstraints>) -> T,
1350    ) -> Option<T> {
1351        let (part_pos, part) = &self.shapes[i as usize];
1352        Some(f(Some(part_pos), &**part, self.part_normal_constraints(i)))
1353    }
1354
1355    #[inline(always)]
1356    fn map_untyped_part_at<T>(
1357        &self,
1358        i: u32,
1359        mut f: impl FnMut(Option<&Pose>, &Self::PartShape, Option<&dyn NormalConstraints>) -> T,
1360    ) -> Option<T> {
1361        let (part_pos, part) = &self.shapes[i as usize];
1362        let constraints = self
1363            .part_normal_constraints(i)
1364            .map(|c| c as &dyn NormalConstraints);
1365        Some(f(Some(part_pos), &**part, constraints))
1366    }
1367}
1368
1369#[cfg(test)]
1370#[cfg(all(feature = "dim2", feature = "alloc", feature = "std"))]
1371mod test {
1372    use super::{Compound, CompoundFlags};
1373    use crate::math::{Pose, Real, Rotation, Vector};
1374    use crate::query::{ContactManifold, DefaultQueryDispatcher, PersistentQueryDispatcher};
1375    use crate::shape::{ConvexPolygon, Cuboid, SharedShape, Triangle};
1376    use alloc::{vec, vec::Vec};
1377
1378    /// A flat run that turns into a ramp, decomposed the way a convex decomposition would leave it:
1379    /// a big piece below and a wedge above, meeting along a cut that runs right from the ramp foot
1380    /// at the origin. The ramp face and the cut leave the wedge with a tip pointing back along the
1381    /// flat run, which is what a body slides into.
1382    fn ramp_foot() -> Compound {
1383        let part = |points: &[[Real; 2]]| {
1384            let points = points.iter().map(|p| Vector::new(p[0], p[1])).collect();
1385            (
1386                Pose::IDENTITY,
1387                SharedShape::new(
1388                    ConvexPolygon::from_convex_polyline_unmodified(points).expect("convex part"),
1389                ),
1390            )
1391        };
1392
1393        Compound::new(vec![
1394            part(&[
1395                [-3.825, -0.015],
1396                [-11.913, -6.486],
1397                [5.166, -6.486],
1398                [5.165, -0.015],
1399                [0.0, 0.0],
1400            ]),
1401            part(&[[0.0, 0.0], [5.165, -0.015], [3.432, 1.912], [2.135, 1.891]]),
1402        ])
1403    }
1404
1405    /// The normals the wedge reports against a box whose right edge sits `overshoot` past the tip.
1406    fn normals_against_the_wedge(compound: &Compound, overshoot: Real) -> Vec<Vector> {
1407        let slider = Cuboid::new(Vector::new(0.42, 0.20));
1408        let pos12 = Pose::from_parts(Vector::new(overshoot - 0.42, 0.198), Default::default());
1409
1410        let mut manifolds: Vec<ContactManifold<(), ()>> = Vec::new();
1411        let mut workspace = None;
1412        DefaultQueryDispatcher
1413            .contact_manifolds(
1414                &pos12,
1415                compound,
1416                &slider,
1417                0.02,
1418                &mut manifolds,
1419                &mut workspace,
1420            )
1421            .expect("dispatch");
1422
1423        manifolds
1424            .iter()
1425            .filter(|manifold| manifold.subshape1 == 1 && !manifold.points.is_empty())
1426            .map(|manifold| manifold.local_n1)
1427            .collect()
1428    }
1429
1430    #[test]
1431    fn cut_edges_are_dropped_and_close_the_corner_they_end_at() {
1432        let mut compound = ramp_foot();
1433        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1434
1435        // Each part loses exactly the cut it shares with the other.
1436        assert_eq!(
1437            compound
1438                .part_normal_constraints(0)
1439                .unwrap()
1440                .boundary_edges
1441                .len(),
1442            4
1443        );
1444        let wedge = compound.part_normal_constraints(1).unwrap();
1445        assert_eq!(wedge.boundary_edges.len(), 3);
1446
1447        // The ramp face runs into the cut at the tip, so its cone closes onto itself there.
1448        let ramp = wedge
1449            .boundary_edges
1450            .iter()
1451            .find(|cone| cone.face.x < -0.5)
1452            .expect("ramp face");
1453        assert_eq!(ramp.counter_clockwise_limit, ramp.face);
1454    }
1455
1456    /// Nothing guarantees which way a part was wound, and a clockwise one would have every normal
1457    /// pointing into itself.
1458    #[test]
1459    fn a_clockwise_part_still_faces_outwards() {
1460        let clockwise = Triangle::new(
1461            Vector::new(0.0, 0.0),
1462            Vector::new(0.0, 1.0),
1463            Vector::new(1.0, 0.0),
1464        );
1465        let mut compound = Compound::new(vec![(Pose::IDENTITY, SharedShape::new(clockwise))]);
1466        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1467
1468        let cones = compound.part_normal_constraints(0).expect("polygonal part");
1469        assert_eq!(cones.boundary_edges.len(), 3);
1470
1471        // Every face points away from the triangle's interior, which lies towards the origin.
1472        let interior = Vector::new(0.25, 0.25);
1473        for cone in &cones.boundary_edges {
1474            assert!(
1475                cone.face.dot(interior) < 0.0 || cone.face.dot(Vector::new(1.0, 1.0)) > 0.0,
1476                "inward face {:?}",
1477                cone.face
1478            );
1479        }
1480
1481        // The hypotenuse is the only face pointing up and to the right.
1482        let hypotenuse = cones
1483            .boundary_edges
1484            .iter()
1485            .find(|cone| cone.face.x > 0.5 && cone.face.y > 0.5)
1486            .expect("outward hypotenuse");
1487        assert!(hypotenuse.face.dot(Vector::new(1.0, 1.0).normalize()) > 0.99);
1488    }
1489
1490    #[test]
1491    fn a_body_running_into_the_tip_reports_no_contact_with_the_cut() {
1492        let plain = ramp_foot();
1493        let mut fixed = ramp_foot();
1494        fixed.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1495
1496        // Against the bare compound the wedge answers with its tip axis: a wall across the flat run.
1497        let ghost = normals_against_the_wedge(&plain, 0.0);
1498        assert_eq!(ghost.len(), 1);
1499        assert!(
1500            ghost[0].x < -0.99,
1501            "expected a wall normal, got {:?}",
1502            ghost[0]
1503        );
1504
1505        // The cut cannot be touched, so the wedge reports nothing until the ramp itself is reached.
1506        assert!(normals_against_the_wedge(&fixed, 0.0).is_empty());
1507    }
1508
1509    /// An L wound counter-clockwise, split along the diagonal a convex decomposition would cut
1510    /// from its reflex corner. The cut lands on two corners of the union: the convex one at the
1511    /// origin, and the reflex one at `(1, 1)`.
1512    fn cut_ell() -> Compound {
1513        let part = |points: &[[Real; 2]]| {
1514            let points = points.iter().map(|p| Vector::new(p[0], p[1])).collect();
1515            (
1516                Pose::IDENTITY,
1517                SharedShape::new(
1518                    ConvexPolygon::from_convex_polyline_unmodified(points).expect("convex part"),
1519                ),
1520            )
1521        };
1522
1523        let mut compound = Compound::new(vec![
1524            part(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]]),
1525            part(&[[0.0, 0.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]]),
1526        ]);
1527        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1528        compound
1529    }
1530
1531    /// The cone of the boundary edge of `part` whose outward normal is `face`.
1532    fn cone_facing(compound: &Compound, part: u32, face: Vector) -> super::CompoundEdgeCone {
1533        *compound
1534            .part_normal_constraints(part)
1535            .expect("polygonal part")
1536            .boundary_edges
1537            .iter()
1538            .find(|cone| (cone.face - face).length() < 1.0e-5)
1539            .unwrap_or_else(|| panic!("no edge facing {face:?}"))
1540    }
1541
1542    /// A corner a cut splits is still a corner of the union, so the two edges meeting there open
1543    /// towards each other even though they belong to different parts.
1544    #[test]
1545    fn a_convex_corner_split_by_a_cut_opens_across_the_parts() {
1546        let compound = cut_ell();
1547
1548        // Each part keeps its three outline edges and loses the diagonal it shares.
1549        for part in 0..2 {
1550            assert_eq!(
1551                compound
1552                    .part_normal_constraints(part)
1553                    .unwrap()
1554                    .boundary_edges
1555                    .len(),
1556                3,
1557                "part {part}"
1558            );
1559        }
1560
1561        // The union turns 90 degrees at the origin, between the bottom edge of one part and the
1562        // left edge of the other. Both cones reach the bisector, splitting the corner between them.
1563        let bisector = Vector::new(-1.0, -1.0).normalize();
1564        let bottom = cone_facing(&compound, 0, -Vector::Y);
1565        let left = cone_facing(&compound, 1, -Vector::X);
1566
1567        assert!(
1568            (bottom.clockwise_limit - bisector).length() < 1.0e-5,
1569            "bottom stopped at {:?}",
1570            bottom.clockwise_limit
1571        );
1572        assert!(
1573            (left.counter_clockwise_limit - bisector).length() < 1.0e-5,
1574            "left stopped at {:?}",
1575            left.counter_clockwise_limit
1576        );
1577    }
1578
1579    /// A concave corner has no outward range for its edges to share, so each keeps its own normal
1580    /// -- the same thing that happens where a surface runs into a cut.
1581    #[test]
1582    fn a_concave_corner_split_by_a_cut_stays_closed() {
1583        let compound = cut_ell();
1584
1585        // The reflex corner of the L, at `(1, 1)`: the upwards edge of one part meets the
1586        // rightwards edge of the other.
1587        let upwards = cone_facing(&compound, 0, Vector::Y);
1588        let rightwards = cone_facing(&compound, 1, Vector::X);
1589
1590        assert_eq!(upwards.counter_clockwise_limit, upwards.face);
1591        assert_eq!(rightwards.clockwise_limit, rightwards.face);
1592    }
1593
1594    /// A row of boxes on a shared grid, the way a voxel body reaches a compound, optionally
1595    /// rotated as a whole. Every part computes its own corners, so the seams are only found if
1596    /// corners that round differently still weld.
1597    fn voxel_row(n: u32, size: Real, origin: Real, angle: Real) -> Compound {
1598        let half = size / 2.0;
1599        let rotation = Rotation::from_angle(angle);
1600        let parts = (0..n)
1601            .map(|i| {
1602                let center = Vector::new(origin + i as Real * size, 0.0);
1603                (
1604                    Pose::from_parts(rotation * center, rotation),
1605                    SharedShape::new(Cuboid::new(Vector::new(half, half))),
1606                )
1607            })
1608            .collect();
1609
1610        let mut compound = Compound::new(parts);
1611        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1612        compound
1613    }
1614
1615    /// How many boundary edges each part of a row kept. A welded row leaves the two ends with
1616    /// three and every box between them with two.
1617    fn boundary_edge_counts(compound: &Compound, n: u32) -> Vec<usize> {
1618        (0..n)
1619            .map(|i| {
1620                compound
1621                    .part_normal_constraints(i)
1622                    .map_or(99, |cones| cones.boundary_edges.len())
1623            })
1624            .collect()
1625    }
1626
1627    /// Parts of a grid meet on corners each computes for itself, and the two spellings need not
1628    /// round alike. Every row here leaves at least one seam a few ULPs wide.
1629    #[test]
1630    fn a_voxel_row_finds_seams_its_parts_rounded_differently() {
1631        for &(size, origin, angle) in &[
1632            (1.0 as Real, 0.0 as Real, 0.1 as Real),
1633            (1.0, 0.0, 0.5236),
1634            (1.0, 0.0, 0.7854),
1635            (0.1, 0.0, 0.0),
1636            (0.7, 0.0, 0.0),
1637            (1.0, 0.05, 0.0),
1638            (0.1, 1000.0, 0.0),
1639            (1.0, 1000.0, 0.7854),
1640        ] {
1641            assert_eq!(
1642                boundary_edge_counts(&voxel_row(5, size, origin, angle), 5),
1643                alloc::vec![3, 2, 2, 2, 3],
1644                "size {size} origin {origin} angle {angle}"
1645            );
1646        }
1647    }
1648
1649    /// A zero tolerance is the exact comparison, which only recognizes a seam whose two spellings
1650    /// round to the very same bits.
1651    #[test]
1652    fn a_zero_tolerance_welds_only_bit_identical_corners() {
1653        let mut rotated = voxel_row(5, 1.0, 0.0, 0.1);
1654        let welded = boundary_edge_counts(&rotated, 5);
1655        assert_eq!(welded, alloc::vec![3, 2, 2, 2, 3]);
1656
1657        // Dropping the tolerance can only lose cuts, never find new ones, and this rotation puts
1658        // at least one seam beyond an exact comparison.
1659        rotated.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, Some(0.0));
1660        let exact = boundary_edge_counts(&rotated, 5);
1661        assert!(
1662            exact.iter().zip(welded.iter()).all(|(a, b)| a >= b),
1663            "exact found a cut the tolerance missed: {exact:?}"
1664        );
1665        assert!(
1666            exact.iter().sum::<usize>() > welded.iter().sum::<usize>(),
1667            "exact matched every seam: {exact:?}"
1668        );
1669
1670        // A grid whose corners land on exact coordinates never needed the tolerance.
1671        let mut exact = voxel_row(5, 1.0, 0.0, 0.0);
1672        exact.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, Some(0.0));
1673        assert_eq!(boundary_edge_counts(&exact, 5), alloc::vec![3, 2, 2, 2, 3]);
1674    }
1675
1676    /// Every interior seam of a full grid is found, and only the perimeter survives: an `n` by `n`
1677    /// grid of cells encloses exactly `4 * n` boundary edges. Four cells meet at every interior
1678    /// corner, so this also exercises welding more than two coincident corners at once.
1679    #[test]
1680    fn a_voxel_grid_keeps_only_its_perimeter() {
1681        for &(n, size, origin, angle) in &[
1682            (3u32, 1.0 as Real, 0.0 as Real, 0.1 as Real),
1683            (3, 0.1, 0.0, 0.0),
1684            (4, 0.7, 5.0, 0.7854),
1685        ] {
1686            let half = size / 2.0;
1687            let rotation = Rotation::from_angle(angle);
1688            let parts = (0..n)
1689                .flat_map(|i| {
1690                    (0..n).map(move |j| {
1691                        let center =
1692                            Vector::new(origin + i as Real * size, origin + j as Real * size);
1693                        (
1694                            Pose::from_parts(rotation * center, rotation),
1695                            SharedShape::new(Cuboid::new(Vector::new(half, half))),
1696                        )
1697                    })
1698                })
1699                .collect();
1700
1701            let mut compound = Compound::new(parts);
1702            compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1703
1704            let counts = boundary_edge_counts(&compound, n * n);
1705            assert_eq!(
1706                counts.iter().sum::<usize>(),
1707                (4 * n) as usize,
1708                "size {size} origin {origin} angle {angle}: {counts:?}"
1709            );
1710        }
1711    }
1712
1713    /// The two signed zeros are the same point and hash apart, so a corner one part spells `0.0`
1714    /// and another `-0.0` has to weld even where no tolerance is allowed at all.
1715    #[test]
1716    fn signed_zero_corners_weld_at_zero_tolerance() {
1717        assert_ne!(
1718            (0.0 as Real).to_bits(),
1719            (-0.0 as Real).to_bits(),
1720            "the two zeros are meant to differ bitwise"
1721        );
1722
1723        // Two triangles sharing the edge from the origin to `(1, 0)`, the second spelling its end
1724        // of that edge with negative zeros.
1725        let mut compound = Compound::new(vec![
1726            (
1727                Pose::IDENTITY,
1728                SharedShape::new(Triangle::new(
1729                    Vector::new(0.0, 0.0),
1730                    Vector::new(1.0, 0.0),
1731                    Vector::new(0.5, 1.0),
1732                )),
1733            ),
1734            (
1735                Pose::IDENTITY,
1736                SharedShape::new(Triangle::new(
1737                    Vector::new(-0.0, -0.0),
1738                    Vector::new(0.5, -1.0),
1739                    Vector::new(1.0, -0.0),
1740                )),
1741            ),
1742        ]);
1743        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, Some(0.0));
1744
1745        assert_eq!(boundary_edge_counts(&compound, 2), alloc::vec![2, 2]);
1746    }
1747
1748    /// The tolerance follows each corner's own distance from the origin rather than the whole
1749    /// compound's, so a part placed far away does not coarsen detail near it.
1750    #[test]
1751    fn a_distant_part_does_not_coarsen_detail_near_the_origin() {
1752        let small = 1.0e-3;
1753        let box_at = |x: Real, half: Real| {
1754            (
1755                Pose::from_parts(Vector::new(x, 0.0), Default::default()),
1756                SharedShape::new(Cuboid::new(Vector::new(half, half))),
1757            )
1758        };
1759
1760        // Two small boxes with a gap between them, and one part 1e5 away. Scaled to the distant
1761        // part the gap would vanish; scaled to the small boxes themselves it is enormous.
1762        let mut compound = Compound::new(vec![
1763            box_at(0.0, small),
1764            box_at(4.0 * small, small),
1765            box_at(1.0e5, 1.0),
1766        ]);
1767        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1768
1769        for part in 0..2 {
1770            assert_eq!(
1771                compound
1772                    .part_normal_constraints(part)
1773                    .unwrap()
1774                    .boundary_edges
1775                    .len(),
1776                4,
1777                "part {part} lost an edge to the distant part's scale"
1778            );
1779        }
1780    }
1781
1782    #[test]
1783    fn contacts_with_the_ramp_itself_are_left_alone() {
1784        let plain = ramp_foot();
1785        let mut fixed = ramp_foot();
1786        fixed.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1787
1788        let expected = normals_against_the_wedge(&plain, 0.01);
1789        assert_eq!(expected.len(), 1);
1790        assert!(
1791            expected[0].y > 0.7,
1792            "expected the ramp face, got {:?}",
1793            expected[0]
1794        );
1795        assert_eq!(normals_against_the_wedge(&fixed, 0.01), expected);
1796    }
1797}
1798
1799#[cfg(test)]
1800#[cfg(all(feature = "dim3", feature = "alloc", feature = "std"))]
1801mod dim3_test {
1802    use super::{Compound, CompoundFlags};
1803    use crate::math::{Pose, Real, Rotation, Vector};
1804    use crate::shape::{Cuboid, SharedShape};
1805    use alloc::{vec, vec::Vec};
1806
1807    /// A flat run turning into a ramp, split along the diagonal a convex decomposition would cut:
1808    /// a slab and a wedge, extruded, sharing one quad exactly. Built through `convex_hull`, the
1809    /// way the Godot plugin builds every `ConvexPolygonShape3D`.
1810    fn ramp_foot() -> Compound {
1811        let prism = |outline: &[[Real; 2]]| {
1812            let corners: Vec<Vector> = outline
1813                .iter()
1814                .flat_map(|p| [Vector::new(p[0], p[1], -3.0), Vector::new(p[0], p[1], 3.0)])
1815                .collect();
1816            (
1817                Pose::IDENTITY,
1818                SharedShape::convex_hull(&corners).expect("convex prism"),
1819            )
1820        };
1821
1822        Compound::new(vec![
1823            prism(&[[-6.0, 0.0], [5.0, 0.0], [11.0, -6.0], [-6.0, -6.0]]),
1824            prism(&[[5.0, 0.0], [9.0, 3.0], [11.0, 3.0], [11.0, -6.0]]),
1825        ])
1826    }
1827
1828    #[test]
1829    fn cut_faces_are_dropped_from_both_parts() {
1830        let mut compound = ramp_foot();
1831        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1832
1833        // Each prism has 6 faces and loses the shared quad -- which only holds if `convex_hull`
1834        // produced the cut with the same corners on both sides, the premise everything rests on.
1835        for part in 0..2 {
1836            let cones = compound.part_normal_constraints(part).expect("polyhedron");
1837            assert_eq!(cones.boundary_faces.len(), 5, "part {part}");
1838        }
1839    }
1840
1841    /// The 2D cut L, extruded. The cut is a quad, and it reaches the union's surface along two
1842    /// vertical edges: the convex one at the origin, and the reflex one above `(1, 1)`.
1843    fn cut_ell() -> Compound {
1844        let prism = |outline: &[[Real; 2]]| {
1845            let corners: Vec<Vector> = outline
1846                .iter()
1847                .flat_map(|p| [Vector::new(p[0], p[1], -1.0), Vector::new(p[0], p[1], 1.0)])
1848                .collect();
1849            (
1850                Pose::IDENTITY,
1851                SharedShape::convex_hull(&corners).expect("convex prism"),
1852            )
1853        };
1854
1855        let mut compound = Compound::new(vec![
1856            prism(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]]),
1857            prism(&[[0.0, 0.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]]),
1858        ]);
1859        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1860        compound
1861    }
1862
1863    /// The pseudo-normal the cone of `part`'s face pointing along `face` carries at the vertical
1864    /// edge standing over `corner`.
1865    fn pseudo_normal_at_vertical_edge(
1866        compound: &Compound,
1867        part: u32,
1868        face: Vector,
1869        corner: [Real; 2],
1870    ) -> Vector {
1871        let (bottom, top) = (
1872            Vector::new(corner[0], corner[1], -1.0),
1873            Vector::new(corner[0], corner[1], 1.0),
1874        );
1875
1876        let faces = compound.part_faces();
1877        let (_, corners) = faces[part as usize]
1878            .as_ref()
1879            .expect("polyhedron")
1880            .iter()
1881            .find(|(normal, _)| (*normal - face).length() < 1.0e-5)
1882            .unwrap_or_else(|| panic!("no face pointing along {face:?}"));
1883
1884        // `edge_pseudo_normals[i]` describes the edge from `corners[i]` to its successor.
1885        let edge = (0..corners.len())
1886            .find(|i| {
1887                let ends = [corners[*i], corners[(*i + 1) % corners.len()]];
1888                ends.iter().any(|end| (*end - bottom).length() < 1.0e-5)
1889                    && ends.iter().any(|end| (*end - top).length() < 1.0e-5)
1890            })
1891            .unwrap_or_else(|| panic!("no vertical edge over {corner:?}"));
1892
1893        compound
1894            .part_normal_constraints(part)
1895            .expect("polyhedron")
1896            .boundary_faces
1897            .iter()
1898            .find(|cone| (cone.face - face).length() < 1.0e-5)
1899            .expect("cone")
1900            .edge_pseudo_normals[edge]
1901    }
1902
1903    /// An edge a cut splits is still an edge of the union, so the two faces meeting along it open
1904    /// towards each other even though they belong to different parts.
1905    #[test]
1906    fn a_convex_edge_split_by_a_cut_opens_across_the_parts() {
1907        let compound = cut_ell();
1908
1909        // Each prism keeps five faces and loses the cut quad it shares.
1910        for part in 0..2 {
1911            assert_eq!(
1912                compound
1913                    .part_normal_constraints(part)
1914                    .unwrap()
1915                    .boundary_faces
1916                    .len(),
1917                5,
1918                "part {part}"
1919            );
1920        }
1921
1922        // The union turns 90 degrees along the vertical edge over the origin, between the front
1923        // face of one prism and the left face of the other.
1924        let halfway = Vector::new(-1.0, -1.0, 0.0).normalize();
1925        for (part, face) in [(0, -Vector::Y), (1, -Vector::X)] {
1926            let pseudo_normal = pseudo_normal_at_vertical_edge(&compound, part, face, [0.0, 0.0]);
1927            assert!(
1928                (pseudo_normal - halfway).length() < 1.0e-5,
1929                "part {part} stopped at {pseudo_normal:?}"
1930            );
1931        }
1932    }
1933
1934    /// A concave edge has no outward range for its faces to share, so each keeps its own normal --
1935    /// the same thing that happens along an edge whose other face was cut away.
1936    #[test]
1937    fn a_concave_edge_split_by_a_cut_stays_closed() {
1938        let compound = cut_ell();
1939
1940        // The reflex edge over `(1, 1)`, where the step rises out of the lower arm of the L.
1941        for (part, face) in [(0, Vector::Y), (1, Vector::X)] {
1942            let pseudo_normal = pseudo_normal_at_vertical_edge(&compound, part, face, [1.0, 1.0]);
1943            assert!(
1944                (pseudo_normal - face).length() < 1.0e-5,
1945                "part {part} opened to {pseudo_normal:?}"
1946            );
1947        }
1948    }
1949
1950    /// A row of cuboids on a shared grid, the way a voxel body reaches a compound, optionally
1951    /// rotated as a whole.
1952    fn voxel_row(n: u32, size: Real, origin: Real, angle: Real) -> Compound {
1953        let half = size / 2.0;
1954        let rotation = Rotation::from_rotation_z(angle);
1955        let parts = (0..n)
1956            .map(|i| {
1957                let center = Vector::new(origin + i as Real * size, 0.0, 0.0);
1958                (
1959                    Pose::from_parts(rotation * center, rotation),
1960                    SharedShape::new(Cuboid::new(Vector::new(half, half, half))),
1961                )
1962            })
1963            .collect();
1964
1965        let mut compound = Compound::new(parts);
1966        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
1967        compound
1968    }
1969
1970    /// How many boundary faces each part of a row kept. A welded row leaves the two ends with
1971    /// five and every box between them with four.
1972    fn boundary_face_counts(compound: &Compound, n: u32) -> Vec<usize> {
1973        (0..n)
1974            .map(|i| {
1975                compound
1976                    .part_normal_constraints(i)
1977                    .map_or(99, |cones| cones.boundary_faces.len())
1978            })
1979            .collect()
1980    }
1981
1982    /// A cuboid is a polyhedron the type system does not call one, so a voxel body used to get no
1983    /// cones at all; and its parts compute their own corners, which need not round alike.
1984    #[test]
1985    fn a_row_of_cuboids_shares_its_faces() {
1986        for &(size, origin, angle) in &[
1987            (1.0 as Real, 0.0 as Real, 0.0 as Real),
1988            (0.1, 0.0, 0.0),
1989            (0.7, 0.0, 0.0),
1990            (1.0, 0.05, 0.0),
1991            (0.1, 1000.0, 0.0),
1992            (1.0, 0.0, 0.1),
1993            (1.0, 1000.0, 0.7854),
1994        ] {
1995            assert_eq!(
1996                boundary_face_counts(&voxel_row(5, size, origin, angle), 5),
1997                vec![5, 4, 4, 4, 5],
1998                "size {size} origin {origin} angle {angle}"
1999            );
2000        }
2001    }
2002
2003    /// A zero tolerance is the exact comparison, which only recognizes a cut whose corners round
2004    /// to the very same bits on both sides.
2005    #[test]
2006    fn a_zero_tolerance_welds_only_bit_identical_corners() {
2007        let mut rotated = voxel_row(5, 1.0, 0.0, 0.1);
2008        let welded = boundary_face_counts(&rotated, 5);
2009        assert_eq!(welded, vec![5, 4, 4, 4, 5]);
2010
2011        // Dropping the tolerance can only lose cuts, never find new ones, and this rotation puts
2012        // at least one seam beyond an exact comparison.
2013        rotated.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, Some(0.0));
2014        let exact = boundary_face_counts(&rotated, 5);
2015        assert!(
2016            exact.iter().zip(welded.iter()).all(|(a, b)| a >= b),
2017            "exact found a cut the tolerance missed: {exact:?}"
2018        );
2019        assert!(
2020            exact.iter().sum::<usize>() > welded.iter().sum::<usize>(),
2021            "exact matched every seam: {exact:?}"
2022        );
2023
2024        // A grid whose corners land on exact coordinates never needed the tolerance.
2025        let mut exact = voxel_row(5, 1.0, 0.0, 0.0);
2026        exact.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, Some(0.0));
2027        assert_eq!(boundary_face_counts(&exact, 5), vec![5, 4, 4, 4, 5]);
2028    }
2029
2030    /// A body pressed into the wedge tip reports a normal along its own face, not a surface of
2031    /// the union; the cones pull it onto the one face that is.
2032    #[test]
2033    fn a_tip_normal_is_clamped_onto_the_ramp_face() {
2034        use crate::query::details::NormalConstraints;
2035
2036        let mut compound = ramp_foot();
2037        compound.set_flags(CompoundFlags::FIX_INTERNAL_EDGES, None);
2038        let wedge = compound.part_normal_constraints(1).expect("polyhedron");
2039
2040        let ramp = Vector::new(-0.6, 0.8, 0.0);
2041        let clamped = wedge
2042            .project_local_normal(Vector::new(-1.0, 0.0, 0.0))
2043            .expect("tip direction faces the outline");
2044        assert!((clamped - ramp).length() < 1.0e-4, "clamped to {clamped:?}");
2045
2046        // A direction the outline genuinely faces is left alone.
2047        let kept = wedge.project_local_normal(ramp).expect("faces the outline");
2048        assert!((kept - ramp).length() < 1.0e-4, "moved to {kept:?}");
2049    }
2050}