Skip to main content

box3d_rust/shape/
mod.rs

1//! Collision shapes attached to bodies (sphere, capsule, hull, mesh, …).
2//!
3//! Create shapes with [`create_sphere_shape`], [`create_capsule_shape`],
4//! [`create_hull_shape`], and the mesh/height-field/compound helpers, using a
5//! [`crate::types::ShapeDef`] from [`crate::types::default_shape_def`]. Geometry
6//! primitives live in [`crate::geometry`] and [`crate::hull`].
7//!
8//! Port of `shape.h` / `shape.c`. This file holds the data model and filter
9//! predicates; create/destroy and mutators are re-exported from submodules.
10//!
11//! SPDX-FileCopyrightText: 2025 Erin Catto
12//! SPDX-License-Identifier: MIT
13
14use crate::compound::CompoundData;
15use crate::core::NULL_INDEX;
16use crate::geometry::{Capsule, ShapeType, Sphere, SurfaceMaterial};
17use crate::height_field::HeightFieldData;
18use crate::hull::HullData;
19use crate::math_functions::{Aabb, Vec3, BOUNDS3_EMPTY, VEC3_ONE, VEC3_ZERO};
20use crate::mesh::MeshData;
21use crate::types::{Filter, QueryFilter};
22use std::rc::Rc;
23
24/// Shape flag bits. (enum b3ShapeFlags)
25pub mod shape_flags {
26    pub const ENABLE_SENSOR_EVENTS: u8 = 0x01;
27    pub const ENABLE_CONTACT_EVENTS: u8 = 0x02;
28    pub const ENABLE_CUSTOM_FILTERING: u8 = 0x04;
29    pub const ENABLE_HIT_EVENTS: u8 = 0x08;
30    pub const ENABLE_PRE_SOLVE_EVENTS: u8 = 0x10;
31    pub const ENLARGED_AABB: u8 = 0x20;
32}
33
34/// Concrete shape geometry. Maps to C's `type` tag + anonymous union.
35#[derive(Debug, Clone)]
36pub enum ShapeGeometry {
37    Capsule(Capsule),
38    Sphere(Sphere),
39    /// Shared hull from the world hull database. (C: `const b3HullData* hull`)
40    Hull(Rc<HullData>),
41    /// Mesh data plus per-instance scale (C: `b3Mesh { data*, scale }`).
42    Mesh {
43        data: MeshData,
44        scale: Vec3,
45    },
46    HeightField(HeightFieldData),
47    Compound(CompoundData),
48}
49
50impl ShapeGeometry {
51    /// The shape type tag. (C: shape->type)
52    pub fn shape_type(&self) -> ShapeType {
53        match self {
54            ShapeGeometry::Capsule(_) => ShapeType::Capsule,
55            ShapeGeometry::Sphere(_) => ShapeType::Sphere,
56            ShapeGeometry::Hull(_) => ShapeType::Hull,
57            ShapeGeometry::Mesh { .. } => ShapeType::Mesh,
58            ShapeGeometry::HeightField(_) => ShapeType::Height,
59            ShapeGeometry::Compound(_) => ShapeType::Compound,
60        }
61    }
62}
63
64impl Default for ShapeGeometry {
65    fn default() -> Self {
66        ShapeGeometry::Capsule(Capsule::default())
67    }
68}
69
70/// Internal shape. (b3Shape)
71///
72/// A single-material shape keeps its material inline. Multi-material meshes and
73/// compounds own a heap array in [`Self::materials`]. Reach materials the same
74/// way for both via [`Shape::shape_materials`].
75#[derive(Debug, Clone)]
76pub struct Shape {
77    pub id: i32,
78    pub body_id: i32,
79    pub prev_shape_id: i32,
80    pub next_shape_id: i32,
81    pub sensor_index: i32,
82    pub proxy_key: i32,
83    pub density: f32,
84    pub explosion_scale: f32,
85    pub aabb_margin: f32,
86
87    pub aabb: Aabb,
88    pub fat_aabb: Aabb,
89    pub local_centroid: Vec3,
90
91    /// Inline material for single-material shapes.
92    pub material: SurfaceMaterial,
93    /// Heap materials for multi-material meshes/compounds. Empty means use
94    /// [`Self::material`] as a one-element array (C: `materials == NULL`).
95    pub materials: Vec<SurfaceMaterial>,
96
97    pub filter: Filter,
98    pub user_data: u64,
99    /// Application user shape pointer (C: `void* userShape`).
100    pub user_shape: u64,
101
102    pub name_id: u32,
103    pub generation: u16,
104
105    /// shape_flags bits
106    pub flags: u8,
107
108    /// The shape geometry (C: type tag + union).
109    pub geometry: ShapeGeometry,
110}
111
112impl Shape {
113    /// The shape type tag. (C: shape->type)
114    pub fn shape_type(&self) -> ShapeType {
115        self.geometry.shape_type()
116    }
117
118    /// Material count. Single-material shapes report 1. (C: materialCount)
119    pub fn material_count(&self) -> i32 {
120        if self.materials.is_empty() {
121            1
122        } else {
123            self.materials.len() as i32
124        }
125    }
126
127    /// Materials slice. Single-material shapes present the inline material as a
128    /// one-element view via this helper's return of a temporary isn't possible
129    /// without an enum — callers should use [`Self::get_material`] or check
130    /// `materials` emptiness. (b3GetShapeMaterials)
131    pub fn shape_materials(&self) -> &[SurfaceMaterial] {
132        if self.materials.is_empty() {
133            core::slice::from_ref(&self.material)
134        } else {
135            &self.materials
136        }
137    }
138
139    /// Material at index. (index into the materials array)
140    pub fn get_material(&self, index: i32) -> &SurfaceMaterial {
141        let mats = self.shape_materials();
142        &mats[index as usize]
143    }
144
145    /// Mutable materials slice. (b3GetShapeMaterials writable)
146    pub fn shape_materials_mut(&mut self) -> &mut [SurfaceMaterial] {
147        if self.materials.is_empty() {
148            core::slice::from_mut(&mut self.material)
149        } else {
150            &mut self.materials
151        }
152    }
153
154    /// Mutable material at index.
155    pub fn get_material_mut(&mut self, index: i32) -> &mut SurfaceMaterial {
156        &mut self.shape_materials_mut()[index as usize]
157    }
158
159    /// User material id for a child/triangle of this shape.
160    /// (b3GetShapeUserMaterialId)
161    ///
162    /// C early-returns 0 when materialCount == 0; the Rust shape always
163    /// presents at least the inline material, so that guard has no
164    /// equivalent here.
165    pub fn get_shape_user_material_id(&self, child_index: i32, triangle_index: i32) -> u64 {
166        use crate::compound::{get_compound_child, ChildGeometry, MAX_COMPOUND_MESH_MATERIALS};
167        use crate::height_field::get_height_field_material;
168        use crate::math_functions::clamp_int;
169        use crate::mesh::get_mesh_material_indices;
170
171        let mut material_index = 0i32;
172        match &self.geometry {
173            ShapeGeometry::Mesh { data, .. } => {
174                let indices = get_mesh_material_indices(data);
175                if !indices.is_empty() {
176                    material_index = indices[triangle_index as usize] as i32;
177                }
178            }
179            ShapeGeometry::HeightField(height_field) => {
180                material_index = get_height_field_material(height_field, triangle_index);
181            }
182            ShapeGeometry::Compound(compound) => {
183                let child = get_compound_child(compound, child_index);
184                if let ChildGeometry::Mesh(mesh) = child.geometry {
185                    let indices = get_mesh_material_indices(mesh.data);
186                    let mesh_material_index = if !indices.is_empty() {
187                        indices[triangle_index as usize] as i32
188                    } else {
189                        0
190                    };
191                    let mesh_material_index = clamp_int(
192                        mesh_material_index,
193                        0,
194                        MAX_COMPOUND_MESH_MATERIALS as i32 - 1,
195                    );
196                    material_index = child.material_indices[mesh_material_index as usize];
197                } else {
198                    material_index = child.material_indices[0];
199                }
200            }
201            _ => {}
202        }
203
204        let material_index = clamp_int(material_index, 0, self.material_count() - 1);
205        self.shape_materials()[material_index as usize].user_material_id
206    }
207}
208
209impl Default for Shape {
210    fn default() -> Self {
211        Shape {
212            id: NULL_INDEX,
213            body_id: NULL_INDEX,
214            prev_shape_id: NULL_INDEX,
215            next_shape_id: NULL_INDEX,
216            sensor_index: NULL_INDEX,
217            proxy_key: NULL_INDEX,
218            density: 0.0,
219            explosion_scale: 1.0,
220            aabb_margin: 0.0,
221            aabb: BOUNDS3_EMPTY,
222            fat_aabb: BOUNDS3_EMPTY,
223            local_centroid: VEC3_ZERO,
224            material: SurfaceMaterial::default(),
225            materials: Vec::new(),
226            filter: Filter::default(),
227            user_data: 0,
228            user_shape: 0,
229            name_id: 0,
230            generation: 0,
231            flags: 0,
232            geometry: ShapeGeometry::default(),
233        }
234    }
235}
236
237/// (static inline b3ShouldShapesCollide)
238pub fn should_shapes_collide(filter_a: Filter, filter_b: Filter) -> bool {
239    if filter_a.group_index == filter_b.group_index && filter_a.group_index != 0 {
240        return filter_a.group_index > 0;
241    }
242
243    (filter_a.mask_bits & filter_b.category_bits) != 0
244        && (filter_a.category_bits & filter_b.mask_bits) != 0
245}
246
247/// (static inline b3ShouldQueryCollide)
248pub fn should_query_collide(shape_filter: &Filter, query_filter: &QueryFilter) -> bool {
249    (shape_filter.category_bits & query_filter.mask_bits) != 0
250        && (shape_filter.mask_bits & query_filter.category_bits) != 0
251}
252
253/// Convenience: unit-scale mesh geometry. (b3Mesh with identity scale)
254pub fn mesh_geometry(data: MeshData) -> ShapeGeometry {
255    ShapeGeometry::Mesh {
256        data,
257        scale: VEC3_ONE,
258    }
259}
260
261mod accessors;
262mod api;
263mod dispatch;
264mod geometry_set;
265pub(crate) mod lifecycle;
266mod mutators;
267mod query;
268mod toi;
269mod wind;
270
271pub use accessors::*;
272pub use api::*;
273pub use dispatch::*;
274pub use geometry_set::*;
275pub use mutators::*;
276pub use query::*;
277pub use toi::*;
278pub use wind::*;