Skip to main content

box2d_rust/shape/
mod.rs

1// Port of the shape module from box2d-cpp-reference/src/shape.h + shape.c.
2//
3// Split to satisfy the 800-line file limit:
4// - dispatch.rs  — per-shape-type dispatch helpers (AABBs, mass, casts, mover)
5// - lifecycle.rs — shape creation (margin, create_shape_internal, typed wrappers)
6//
7// This file holds the data model and filter predicates. The remaining
8// shape.c (destruction, chains, world API accessors) lands in later slices.
9//
10// SPDX-FileCopyrightText: 2023 Erin Catto
11// SPDX-License-Identifier: MIT
12
13use crate::collision::{Circle, ShapeGeometry, ShapeType};
14use crate::core::NULL_INDEX;
15use crate::math_functions::{Aabb, Vec2, VEC2_ZERO};
16use crate::types::{Filter, QueryFilter, SurfaceMaterial};
17
18/// Internal shape. (b2Shape)
19///
20/// The C struct stores a `b2ShapeType type` tag plus a union of the concrete
21/// geometries; the Rust port stores the tagged [`ShapeGeometry`] enum, and
22/// [`Shape::shape_type`] recovers the tag.
23#[derive(Debug, Clone)]
24pub struct Shape {
25    pub id: i32,
26    pub body_id: i32,
27    pub prev_shape_id: i32,
28    pub next_shape_id: i32,
29    pub sensor_index: i32,
30    pub material: SurfaceMaterial,
31    pub density: f32,
32    pub aabb_margin: f32,
33    pub aabb: Aabb,
34    pub fat_aabb: Aabb,
35    pub local_centroid: Vec2,
36    pub proxy_key: i32,
37
38    pub filter: Filter,
39    pub user_data: u64,
40
41    /// The shape geometry (C: type tag + union).
42    pub geometry: ShapeGeometry,
43
44    pub generation: u16,
45    pub enable_sensor_events: bool,
46    pub enable_contact_events: bool,
47    pub enable_custom_filtering: bool,
48    pub enable_hit_events: bool,
49    pub enable_pre_solve_events: bool,
50    pub enlarged_aabb: bool,
51}
52
53impl Shape {
54    /// The shape type tag. (C: shape->type)
55    pub fn shape_type(&self) -> ShapeType {
56        self.geometry.shape_type()
57    }
58
59    /// (static inline b2GetShapeRadius)
60    pub fn radius(&self) -> f32 {
61        match &self.geometry {
62            ShapeGeometry::Capsule(capsule) => capsule.radius,
63            ShapeGeometry::Circle(circle) => circle.radius,
64            ShapeGeometry::Polygon(polygon) => polygon.radius,
65            _ => 0.0,
66        }
67    }
68}
69
70impl Default for Shape {
71    fn default() -> Self {
72        Shape {
73            id: NULL_INDEX,
74            body_id: NULL_INDEX,
75            prev_shape_id: NULL_INDEX,
76            next_shape_id: NULL_INDEX,
77            sensor_index: NULL_INDEX,
78            material: SurfaceMaterial::default(),
79            density: 0.0,
80            aabb_margin: 0.0,
81            aabb: Aabb::default(),
82            fat_aabb: Aabb::default(),
83            local_centroid: VEC2_ZERO,
84            proxy_key: NULL_INDEX,
85            filter: Filter::default(),
86            user_data: 0,
87            geometry: ShapeGeometry::Circle(Circle::default()),
88            generation: 0,
89            enable_sensor_events: false,
90            enable_contact_events: false,
91            enable_custom_filtering: false,
92            enable_hit_events: false,
93            enable_pre_solve_events: false,
94            enlarged_aabb: false,
95        }
96    }
97}
98
99/// Internal chain shape. (b2ChainShape)
100///
101/// The C `int* shapeIndices` / `b2SurfaceMaterial* materials` pointer+count
102/// pairs are owned Vecs.
103#[derive(Debug, Clone, Default)]
104pub struct ChainShape {
105    pub id: i32,
106    pub body_id: i32,
107    pub next_chain_id: i32,
108    pub shape_indices: Vec<i32>,
109    pub materials: Vec<SurfaceMaterial>,
110    pub generation: u16,
111}
112
113/// (b2ShapeExtent)
114#[derive(Debug, Clone, Copy, PartialEq, Default)]
115pub struct ShapeExtent {
116    pub min_extent: f32,
117    pub max_extent: f32,
118}
119
120/// (static inline b2ShouldShapesCollide)
121pub fn should_shapes_collide(filter_a: Filter, filter_b: Filter) -> bool {
122    if filter_a.group_index == filter_b.group_index && filter_a.group_index != 0 {
123        return filter_a.group_index > 0;
124    }
125
126    (filter_a.mask_bits & filter_b.category_bits) != 0
127        && (filter_a.category_bits & filter_b.mask_bits) != 0
128}
129
130/// (static inline b2ShouldQueryCollide)
131pub fn should_query_collide(shape_filter: Filter, query_filter: QueryFilter) -> bool {
132    (shape_filter.category_bits & query_filter.mask_bits) != 0
133        && (shape_filter.mask_bits & query_filter.category_bits) != 0
134}
135
136mod dispatch;
137mod lifecycle;
138
139pub use dispatch::*;
140pub use lifecycle::*;
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::types::default_filter;
146
147    #[test]
148    fn filter_logic() {
149        // Same positive group always collides, same negative group never.
150        let mut a = default_filter();
151        let mut b = default_filter();
152        a.group_index = 3;
153        b.group_index = 3;
154        assert!(should_shapes_collide(a, b));
155        a.group_index = -3;
156        b.group_index = -3;
157        assert!(!should_shapes_collide(a, b));
158
159        // Zero group falls back to category/mask.
160        a.group_index = 0;
161        b.group_index = 0;
162        a.category_bits = 0x2;
163        a.mask_bits = 0x4;
164        b.category_bits = 0x4;
165        b.mask_bits = 0x2;
166        assert!(should_shapes_collide(a, b));
167        b.mask_bits = 0x8;
168        assert!(!should_shapes_collide(a, b));
169
170        // Query filtering is symmetric category/mask with no groups.
171        let shape_filter = default_filter();
172        let query = crate::types::default_query_filter();
173        assert!(should_query_collide(shape_filter, query));
174    }
175
176    #[test]
177    fn shape_dispatch_matches_geometry() {
178        use crate::collision::Circle;
179        use crate::geometry::{compute_circle_mass, make_box};
180        use crate::math_functions::{make_world_transform, Vec2, PI, TRANSFORM_IDENTITY};
181
182        let circle = Circle {
183            center: Vec2 { x: 1.0, y: 0.0 },
184            radius: 1.0,
185        };
186        let shape = Shape {
187            density: 2.0,
188            geometry: ShapeGeometry::Circle(circle),
189            ..Default::default()
190        };
191
192        // Mass dispatch matches the direct geometry call.
193        let md = compute_shape_mass(&shape);
194        assert_eq!(md, compute_circle_mass(&circle, 2.0));
195
196        // Centroid, perimeter, extents.
197        assert_eq!(get_shape_centroid(&shape), circle.center);
198        assert_eq!(get_shape_perimeter(&shape), 2.0 * PI);
199        let extent = compute_shape_extent(&shape, Vec2 { x: 0.0, y: 0.0 });
200        assert_eq!(extent.min_extent, 1.0);
201        assert_eq!(extent.max_extent, 2.0);
202
203        // AABB through the world transform.
204        let aabb = compute_shape_aabb(&shape, make_world_transform(TRANSFORM_IDENTITY));
205        assert!((aabb.lower_bound.x - 0.0).abs() < 1e-6);
206        assert!((aabb.upper_bound.x - 2.0).abs() < 1e-6);
207
208        // Distance proxy carries the radius for round shapes.
209        let proxy = make_shape_distance_proxy(&shape);
210        assert_eq!(proxy.count, 1);
211        assert_eq!(proxy.radius, 1.0);
212
213        // Polygon dispatch: projected perimeter of a unit box on the x axis.
214        let box_shape = Shape {
215            geometry: ShapeGeometry::Polygon(make_box(1.0, 1.0)),
216            ..Default::default()
217        };
218        let proj = get_shape_projected_perimeter(&box_shape, Vec2 { x: 1.0, y: 0.0 });
219        assert_eq!(proj, 2.0);
220    }
221
222    #[test]
223    fn ray_cast_shape_respects_transform() {
224        use crate::collision::{Circle, RayCastInput};
225        use crate::math_functions::{Transform, Vec2, ROT_IDENTITY};
226
227        // A unit circle at local origin, shifted to x = 3 by the transform.
228        let shape = Shape {
229            geometry: ShapeGeometry::Circle(Circle {
230                center: Vec2 { x: 0.0, y: 0.0 },
231                radius: 1.0,
232            }),
233            ..Default::default()
234        };
235        let transform = Transform {
236            p: Vec2 { x: 3.0, y: 0.0 },
237            q: ROT_IDENTITY,
238        };
239
240        let input = RayCastInput {
241            origin: Vec2 { x: 0.0, y: 0.0 },
242            translation: Vec2 { x: 8.0, y: 0.0 },
243            max_fraction: 1.0,
244        };
245        let output = ray_cast_shape(&input, &shape, transform);
246        assert!(output.hit);
247        // Hit at x = 2 in the input frame.
248        assert!((output.point.x - 2.0).abs() < 1e-5);
249        assert!((output.fraction - 0.25).abs() < 1e-6);
250        assert!((output.normal.x + 1.0).abs() < 1e-6);
251    }
252
253    #[test]
254    fn create_shapes_on_body_updates_mass() {
255        use crate::body::{create_body, get_body_full_id};
256        use crate::collision::Circle;
257        use crate::geometry::{compute_polygon_mass, make_box};
258        use crate::math_functions::Vec2;
259        use crate::solver_set::AWAKE_SET;
260        use crate::types::{default_body_def, default_shape_def, default_world_def, BodyType};
261        use crate::world::World;
262
263        let mut world = World::new(&default_world_def());
264
265        let mut body_def = default_body_def();
266        body_def.type_ = BodyType::Dynamic;
267        let body = create_body(&mut world, &body_def);
268        let body_index = get_body_full_id(&world, body);
269
270        // A 1x1 half-extent box with density 1: mass = 4, matching geometry.
271        let box_poly = make_box(1.0, 1.0);
272        let shape_def = default_shape_def();
273        let shape_id = create_polygon_shape(&mut world, body, &shape_def, &box_poly);
274        assert!(shape_id.index1 >= 1);
275
276        let expected = compute_polygon_mass(&box_poly, 1.0);
277        {
278            let b = &world.bodies[body_index as usize];
279            assert_eq!(b.shape_count, 1);
280            assert_eq!(b.mass, expected.mass);
281            assert_eq!(b.inertia, expected.rotational_inertia);
282        }
283        {
284            let sim = &world.solver_sets[AWAKE_SET as usize].body_sims[0];
285            assert_eq!(sim.inv_mass, 1.0 / expected.mass);
286            assert!(sim.max_extent > 1.4 && sim.max_extent < 1.5);
287        }
288
289        // The shape has a live broad-phase proxy in the dynamic tree.
290        let raw_shape = (shape_id.index1 - 1) as usize;
291        let proxy_key = world.shapes[raw_shape].proxy_key;
292        assert!(proxy_key != NULL_INDEX);
293        assert_eq!(world.broad_phase.shape_index(proxy_key), raw_shape as i32);
294
295        // A second shape accumulates mass; deferring the update sets dirty.
296        let circle = Circle {
297            center: Vec2 { x: 2.0, y: 0.0 },
298            radius: 0.5,
299        };
300        let mut lazy_def = default_shape_def();
301        lazy_def.update_body_mass = false;
302        let _s2 = create_circle_shape(&mut world, body, &lazy_def, &circle);
303        {
304            let b = &world.bodies[body_index as usize];
305            assert_eq!(b.shape_count, 2);
306            // Mass unchanged, flagged dirty.
307            assert_eq!(b.mass, expected.mass);
308            assert!(b.flags & crate::body::body_flags::DIRTY_MASS != 0);
309        }
310        crate::body::update_body_mass_data(&mut world, body_index);
311        assert!(world.bodies[body_index as usize].mass > expected.mass);
312
313        // Sensor shapes register in the sensor array.
314        let mut sensor_def = default_shape_def();
315        sensor_def.is_sensor = true;
316        let s3 = create_circle_shape(&mut world, body, &sensor_def, &circle);
317        let raw_s3 = (s3.index1 - 1) as usize;
318        assert_eq!(world.shapes[raw_s3].sensor_index, 0);
319        assert_eq!(world.sensors.len(), 1);
320        assert_eq!(world.sensors[0].shape_id, raw_s3 as i32);
321
322        world.validate_solver_sets();
323    }
324
325    #[test]
326    fn proxy_lifecycle_updates_broad_phase() {
327        use crate::broad_phase::BroadPhase;
328        use crate::math_functions::{make_world_transform, TRANSFORM_IDENTITY};
329        use crate::types::{BodyType, Capacity};
330
331        let mut bp = BroadPhase::new(&Capacity::default());
332        let mut shape = Shape {
333            id: 5,
334            geometry: ShapeGeometry::Circle(crate::collision::Circle {
335                center: crate::math_functions::Vec2 { x: 0.0, y: 0.0 },
336                radius: 0.5,
337            }),
338            aabb_margin: crate::constants::max_aabb_margin(),
339            ..Default::default()
340        };
341
342        create_shape_proxy(
343            &mut shape,
344            &mut bp,
345            BodyType::Dynamic,
346            make_world_transform(TRANSFORM_IDENTITY),
347            false,
348        );
349        assert!(shape.proxy_key != NULL_INDEX);
350        // Fat AABB got both speculative and movement margins.
351        assert!(shape.fat_aabb.upper_bound.x > shape.aabb.upper_bound.x);
352        assert!(shape.aabb.upper_bound.x > 0.5);
353        assert_eq!(bp.shape_index(shape.proxy_key), 5);
354
355        destroy_shape_proxy(&mut shape, &mut bp);
356        assert_eq!(shape.proxy_key, NULL_INDEX);
357    }
358}