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 api;
137mod chain;
138mod dispatch;
139mod geometry_api;
140mod lifecycle;
141
142pub use api::*;
143pub use chain::*;
144pub use dispatch::*;
145pub use geometry_api::*;
146pub use lifecycle::*;
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::types::default_filter;
152
153    #[test]
154    fn filter_logic() {
155        // Same positive group always collides, same negative group never.
156        let mut a = default_filter();
157        let mut b = default_filter();
158        a.group_index = 3;
159        b.group_index = 3;
160        assert!(should_shapes_collide(a, b));
161        a.group_index = -3;
162        b.group_index = -3;
163        assert!(!should_shapes_collide(a, b));
164
165        // Zero group falls back to category/mask.
166        a.group_index = 0;
167        b.group_index = 0;
168        a.category_bits = 0x2;
169        a.mask_bits = 0x4;
170        b.category_bits = 0x4;
171        b.mask_bits = 0x2;
172        assert!(should_shapes_collide(a, b));
173        b.mask_bits = 0x8;
174        assert!(!should_shapes_collide(a, b));
175
176        // Query filtering is symmetric category/mask with no groups.
177        let shape_filter = default_filter();
178        let query = crate::types::default_query_filter();
179        assert!(should_query_collide(shape_filter, query));
180    }
181
182    #[test]
183    fn shape_dispatch_matches_geometry() {
184        use crate::collision::Circle;
185        use crate::geometry::{compute_circle_mass, make_box};
186        use crate::math_functions::{make_world_transform, Vec2, PI, TRANSFORM_IDENTITY};
187
188        let circle = Circle {
189            center: Vec2 { x: 1.0, y: 0.0 },
190            radius: 1.0,
191        };
192        let shape = Shape {
193            density: 2.0,
194            geometry: ShapeGeometry::Circle(circle),
195            ..Default::default()
196        };
197
198        // Mass dispatch matches the direct geometry call.
199        let md = compute_shape_mass(&shape);
200        assert_eq!(md, compute_circle_mass(&circle, 2.0));
201
202        // Centroid, perimeter, extents.
203        assert_eq!(get_shape_centroid(&shape), circle.center);
204        assert_eq!(get_shape_perimeter(&shape), 2.0 * PI);
205        let extent = compute_shape_extent(&shape, Vec2 { x: 0.0, y: 0.0 });
206        assert_eq!(extent.min_extent, 1.0);
207        assert_eq!(extent.max_extent, 2.0);
208
209        // AABB through the world transform.
210        let aabb = compute_shape_aabb(&shape, make_world_transform(TRANSFORM_IDENTITY));
211        assert!((aabb.lower_bound.x - 0.0).abs() < 1e-6);
212        assert!((aabb.upper_bound.x - 2.0).abs() < 1e-6);
213
214        // Distance proxy carries the radius for round shapes.
215        let proxy = make_shape_distance_proxy(&shape);
216        assert_eq!(proxy.count, 1);
217        assert_eq!(proxy.radius, 1.0);
218
219        // Polygon dispatch: projected perimeter of a unit box on the x axis.
220        let box_shape = Shape {
221            geometry: ShapeGeometry::Polygon(make_box(1.0, 1.0)),
222            ..Default::default()
223        };
224        let proj = get_shape_projected_perimeter(&box_shape, Vec2 { x: 1.0, y: 0.0 });
225        assert_eq!(proj, 2.0);
226    }
227
228    #[test]
229    fn ray_cast_shape_respects_transform() {
230        use crate::collision::{Circle, RayCastInput};
231        use crate::math_functions::{Transform, Vec2, ROT_IDENTITY};
232
233        // A unit circle at local origin, shifted to x = 3 by the transform.
234        let shape = Shape {
235            geometry: ShapeGeometry::Circle(Circle {
236                center: Vec2 { x: 0.0, y: 0.0 },
237                radius: 1.0,
238            }),
239            ..Default::default()
240        };
241        let transform = Transform {
242            p: Vec2 { x: 3.0, y: 0.0 },
243            q: ROT_IDENTITY,
244        };
245
246        let input = RayCastInput {
247            origin: Vec2 { x: 0.0, y: 0.0 },
248            translation: Vec2 { x: 8.0, y: 0.0 },
249            max_fraction: 1.0,
250        };
251        let output = ray_cast_shape(&input, &shape, transform);
252        assert!(output.hit);
253        // Hit at x = 2 in the input frame.
254        assert!((output.point.x - 2.0).abs() < 1e-5);
255        assert!((output.fraction - 0.25).abs() < 1e-6);
256        assert!((output.normal.x + 1.0).abs() < 1e-6);
257    }
258
259    #[test]
260    fn create_shapes_on_body_updates_mass() {
261        use crate::body::{create_body, get_body_full_id};
262        use crate::collision::Circle;
263        use crate::geometry::{compute_polygon_mass, make_box};
264        use crate::math_functions::Vec2;
265        use crate::solver_set::AWAKE_SET;
266        use crate::types::{default_body_def, default_shape_def, default_world_def, BodyType};
267        use crate::world::World;
268
269        let mut world = World::new(&default_world_def());
270
271        let mut body_def = default_body_def();
272        body_def.type_ = BodyType::Dynamic;
273        let body = create_body(&mut world, &body_def);
274        let body_index = get_body_full_id(&world, body);
275
276        // A 1x1 half-extent box with density 1: mass = 4, matching geometry.
277        let box_poly = make_box(1.0, 1.0);
278        let shape_def = default_shape_def();
279        let shape_id = create_polygon_shape(&mut world, body, &shape_def, &box_poly);
280        assert!(shape_id.index1 >= 1);
281
282        let expected = compute_polygon_mass(&box_poly, 1.0);
283        {
284            let b = &world.bodies[body_index as usize];
285            assert_eq!(b.shape_count, 1);
286            assert_eq!(b.mass, expected.mass);
287            assert_eq!(b.inertia, expected.rotational_inertia);
288        }
289        {
290            let sim = &world.solver_sets[AWAKE_SET as usize].body_sims[0];
291            assert_eq!(sim.inv_mass, 1.0 / expected.mass);
292            assert!(sim.max_extent > 1.4 && sim.max_extent < 1.5);
293        }
294
295        // The shape has a live broad-phase proxy in the dynamic tree.
296        let raw_shape = (shape_id.index1 - 1) as usize;
297        let proxy_key = world.shapes[raw_shape].proxy_key;
298        assert!(proxy_key != NULL_INDEX);
299        assert_eq!(world.broad_phase.shape_index(proxy_key), raw_shape as i32);
300
301        // A second shape accumulates mass; deferring the update sets dirty.
302        let circle = Circle {
303            center: Vec2 { x: 2.0, y: 0.0 },
304            radius: 0.5,
305        };
306        let mut lazy_def = default_shape_def();
307        lazy_def.update_body_mass = false;
308        let _s2 = create_circle_shape(&mut world, body, &lazy_def, &circle);
309        {
310            let b = &world.bodies[body_index as usize];
311            assert_eq!(b.shape_count, 2);
312            // Mass unchanged, flagged dirty.
313            assert_eq!(b.mass, expected.mass);
314            assert!(b.flags & crate::body::body_flags::DIRTY_MASS != 0);
315        }
316        crate::body::update_body_mass_data(&mut world, body_index);
317        assert!(world.bodies[body_index as usize].mass > expected.mass);
318
319        // Sensor shapes register in the sensor array.
320        let mut sensor_def = default_shape_def();
321        sensor_def.is_sensor = true;
322        let s3 = create_circle_shape(&mut world, body, &sensor_def, &circle);
323        let raw_s3 = (s3.index1 - 1) as usize;
324        assert_eq!(world.shapes[raw_s3].sensor_index, 0);
325        assert_eq!(world.sensors.len(), 1);
326        assert_eq!(world.sensors[0].shape_id, raw_s3 as i32);
327
328        world.validate_solver_sets();
329    }
330
331    #[test]
332    fn proxy_lifecycle_updates_broad_phase() {
333        use crate::broad_phase::BroadPhase;
334        use crate::math_functions::{make_world_transform, TRANSFORM_IDENTITY};
335        use crate::types::{BodyType, Capacity};
336
337        let mut bp = BroadPhase::new(&Capacity::default());
338        let mut shape = Shape {
339            id: 5,
340            geometry: ShapeGeometry::Circle(crate::collision::Circle {
341                center: crate::math_functions::Vec2 { x: 0.0, y: 0.0 },
342                radius: 0.5,
343            }),
344            aabb_margin: crate::constants::max_aabb_margin(),
345            ..Default::default()
346        };
347
348        create_shape_proxy(
349            &mut shape,
350            &mut bp,
351            BodyType::Dynamic,
352            make_world_transform(TRANSFORM_IDENTITY),
353            false,
354        );
355        assert!(shape.proxy_key != NULL_INDEX);
356        // Fat AABB got both speculative and movement margins.
357        assert!(shape.fat_aabb.upper_bound.x > shape.aabb.upper_bound.x);
358        assert!(shape.aabb.upper_bound.x > 0.5);
359        assert_eq!(bp.shape_index(shape.proxy_key), 5);
360
361        destroy_shape_proxy(&mut shape, &mut bp);
362        assert_eq!(shape.proxy_key, NULL_INDEX);
363    }
364}