Skip to main content

box3d_rust/shape/
mod.rs

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