Skip to main content

box2d_rust/body/
mod.rs

1// Port of the body module from box2d-cpp-reference/src/body.h + body.c.
2//
3// Split to satisfy the 800-line file limit:
4// - plumbing.rs  — storage accessors, island glue, sim removal
5// - lifecycle.rs — create_body and update_body_mass_data
6//
7// This file holds the data model. Destruction and the public accessors
8// that need joints/contacts/sensors land in later slices.
9//
10// SPDX-FileCopyrightText: 2023 Erin Catto
11// SPDX-License-Identifier: MIT
12
13use crate::core::NULL_INDEX;
14use crate::distance::Sweep;
15use crate::math_functions::{
16    sub_pos, Pos, Rot, Vec2, WorldTransform, POS_ZERO, ROT_IDENTITY, VEC2_ZERO,
17    WORLD_TRANSFORM_IDENTITY,
18};
19use crate::types::BodyType;
20
21// enum b2BodyFlags
22pub mod body_flags {
23    /// This body has fixed translation along the x-axis
24    pub const LOCK_LINEAR_X: u32 = 0x00000001;
25    /// This body has fixed translation along the y-axis
26    pub const LOCK_LINEAR_Y: u32 = 0x00000002;
27    /// This body has fixed rotation
28    pub const LOCK_ANGULAR_Z: u32 = 0x00000004;
29    /// This flag is used for debug draw
30    pub const IS_FAST: u32 = 0x00000008;
31    /// This dynamic body does a final CCD pass against all body types, but not other bullets
32    pub const IS_BULLET: u32 = 0x00000010;
33    /// This body was speed capped in the current time step
34    pub const IS_SPEED_CAPPED: u32 = 0x00000020;
35    /// This body had a time of impact event in the current time step
36    pub const HAD_TIME_OF_IMPACT: u32 = 0x00000040;
37    /// This body has no limit on angular velocity
38    pub const ALLOW_FAST_ROTATION: u32 = 0x00000080;
39    /// This body needs to have its AABB increased
40    pub const ENLARGE_BOUNDS: u32 = 0x00000100;
41    /// This body is dynamic so the solver should write to it.
42    pub const DYNAMIC_FLAG: u32 = 0x00000200;
43    /// The user deferred mass computation but b2Body_ApplyMassFromShapes was
44    /// not called before the world step.
45    pub const DIRTY_MASS: u32 = 0x00000400;
46    pub const ENABLE_SLEEP: u32 = 0x00000800;
47    pub const BODY_ENABLE_CONTACT_RECYCLING: u32 = 0x00001000;
48
49    /// All lock flags
50    pub const ALL_LOCKS: u32 = LOCK_ANGULAR_Z | LOCK_LINEAR_X | LOCK_LINEAR_Y;
51    /// If this flag is set then the body has fixed rotation
52    pub const FIXED_ROTATION: u32 = LOCK_ANGULAR_Z;
53    /// These flags are transient per time step. These may be different across
54    /// Body, BodySim, and BodyState.
55    pub const BODY_TRANSIENT_FLAGS: u32 = IS_FAST | IS_SPEED_CAPPED | HAD_TIME_OF_IMPACT;
56}
57
58/// Body organizational details that are not used in the solver. (b2Body)
59#[derive(Debug, Clone)]
60pub struct Body {
61    pub user_data: u64,
62
63    /// index of solver set stored in World. May be NULL_INDEX.
64    pub set_index: i32,
65
66    /// body sim and state index within set. May be NULL_INDEX.
67    pub local_index: i32,
68
69    /// [31 : contactId | 1 : edgeIndex]
70    pub head_contact_key: i32,
71    pub contact_count: i32,
72
73    pub head_shape_id: i32,
74    pub shape_count: i32,
75
76    pub head_chain_id: i32,
77
78    /// [31 : jointId | 1 : edgeIndex]
79    pub head_joint_key: i32,
80    pub joint_count: i32,
81
82    /// All enabled dynamic and kinematic bodies are in an island.
83    pub island_id: i32,
84
85    /// Need this island index for faster union-find
86    pub island_index: i32,
87
88    pub mass: f32,
89
90    /// Rotational inertia about the center of mass.
91    pub inertia: f32,
92
93    pub sleep_threshold: f32,
94    pub sleep_time: f32,
95
96    /// this is used to adjust the fellAsleep flag in the body move array
97    pub body_move_index: i32,
98
99    pub id: i32,
100
101    /// body_flags bits
102    pub flags: u32,
103
104    pub type_: BodyType,
105
106    /// Monotonically advanced when a body is allocated in this slot.
107    /// Used to check for invalid BodyId.
108    pub generation: u16,
109
110    /// Body name for debugging (C: char[B2_NAME_LENGTH + 1]).
111    pub name: String,
112}
113
114impl Default for Body {
115    fn default() -> Self {
116        Body {
117            user_data: 0,
118            set_index: NULL_INDEX,
119            local_index: NULL_INDEX,
120            head_contact_key: NULL_INDEX,
121            contact_count: 0,
122            head_shape_id: NULL_INDEX,
123            shape_count: 0,
124            head_chain_id: NULL_INDEX,
125            head_joint_key: NULL_INDEX,
126            joint_count: 0,
127            island_id: NULL_INDEX,
128            island_index: NULL_INDEX,
129            mass: 0.0,
130            inertia: 0.0,
131            sleep_threshold: 0.0,
132            sleep_time: 0.0,
133            body_move_index: NULL_INDEX,
134            id: NULL_INDEX,
135            flags: 0,
136            type_: BodyType::Static,
137            generation: 0,
138            name: String::new(),
139        }
140    }
141}
142
143/// Body state, designed for fast conversion to and from SIMD via
144/// scatter-gather. Only awake dynamic and kinematic bodies have a body state.
145/// Used in the performance critical constraint solver. (b2BodyState, 32 bytes)
146///
147/// The solver operates on the body state. The body state array does not hold
148/// static bodies; their delta rotation is identity via a dummy state.
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct BodyState {
151    pub linear_velocity: Vec2,
152    pub angular_velocity: f32,
153    /// body_flags bits. Important flags: locking, dynamic.
154    pub flags: u32,
155    /// Using delta position reduces round-off error far from the origin
156    pub delta_position: Vec2,
157    /// Using delta rotation because the solver cannot access the full rotation
158    /// on static bodies and must use zero delta rotation (c,s) = (1,0)
159    pub delta_rotation: Rot,
160}
161
162/// Identity body state, notice the delta_rotation is {1, 0}.
163/// (b2_identityBodyState)
164pub const IDENTITY_BODY_STATE: BodyState = BodyState {
165    linear_velocity: VEC2_ZERO,
166    angular_velocity: 0.0,
167    flags: 0,
168    delta_position: VEC2_ZERO,
169    delta_rotation: ROT_IDENTITY,
170};
171
172impl Default for BodyState {
173    fn default() -> Self {
174        IDENTITY_BODY_STATE
175    }
176}
177
178/// Body simulation data used for integration of position and velocity.
179/// Transform data used for collision and solver preparation. (b2BodySim)
180#[derive(Debug, Clone, Copy, PartialEq)]
181pub struct BodySim {
182    /// transform for body origin, double translation in large world mode
183    pub transform: WorldTransform,
184
185    /// center of mass position in world space
186    pub center: Pos,
187
188    /// previous rotation and COM for TOI
189    pub rotation0: Rot,
190    pub center0: Pos,
191
192    /// location of center of mass relative to the body origin
193    pub local_center: Vec2,
194
195    pub force: Vec2,
196    pub torque: f32,
197
198    /// inverse mass and inertia
199    pub inv_mass: f32,
200    pub inv_inertia: f32,
201
202    pub min_extent: f32,
203    pub max_extent: f32,
204    pub linear_damping: f32,
205    pub angular_damping: f32,
206    pub gravity_scale: f32,
207
208    /// Index of Body
209    pub body_id: i32,
210
211    /// body_flags bits
212    pub flags: u32,
213}
214
215impl Default for BodySim {
216    fn default() -> Self {
217        BodySim {
218            transform: WORLD_TRANSFORM_IDENTITY,
219            center: POS_ZERO,
220            rotation0: ROT_IDENTITY,
221            center0: POS_ZERO,
222            local_center: VEC2_ZERO,
223            force: VEC2_ZERO,
224            torque: 0.0,
225            inv_mass: 0.0,
226            inv_inertia: 0.0,
227            min_extent: 0.0,
228            max_extent: 0.0,
229            linear_damping: 0.0,
230            angular_damping: 0.0,
231            gravity_scale: 0.0,
232            body_id: NULL_INDEX,
233            flags: 0,
234        }
235    }
236}
237
238/// Build a sweep relative to a base position so continuous collision keeps
239/// float precision far from the origin. The base cancels out of the relative
240/// motion the TOI actually solves. (b2MakeRelativeSweep)
241pub fn make_relative_sweep(body_sim: &BodySim, base: Pos) -> Sweep {
242    Sweep {
243        c1: sub_pos(body_sim.center0, base),
244        c2: sub_pos(body_sim.center, base),
245        q1: body_sim.rotation0,
246        q2: body_sim.transform.q,
247        local_center: body_sim.local_center,
248    }
249}
250
251mod lifecycle;
252mod plumbing;
253
254pub use lifecycle::*;
255pub use plumbing::*;
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::math_functions::{make_rot, to_pos, Vec2};
261    use crate::solver_set::{AWAKE_SET, DISABLED_SET, STATIC_SET};
262    use crate::types::{default_body_def, default_world_def};
263    use crate::world::World;
264
265    #[test]
266    fn create_static_dynamic_and_sleeping_bodies() {
267        let mut world = World::new(&default_world_def());
268
269        // Static body goes in the static set with no island.
270        let s = create_body(&mut world, &default_body_def());
271        let s_index = get_body_full_id(&world, s);
272        assert_eq!(world.bodies[s_index as usize].set_index, STATIC_SET);
273        assert_eq!(world.bodies[s_index as usize].island_id, NULL_INDEX);
274        assert!(world.solver_sets[STATIC_SET as usize]
275            .body_states
276            .is_empty());
277
278        // Dynamic awake body: awake set, body state, island.
279        let mut def = default_body_def();
280        def.type_ = BodyType::Dynamic;
281        def.position = to_pos(Vec2 { x: 2.0, y: 3.0 });
282        def.rotation = make_rot(0.5);
283        def.linear_velocity = Vec2 { x: 1.0, y: -1.0 };
284        def.name = "crate".into();
285        let d = create_body(&mut world, &def);
286        let d_index = get_body_full_id(&world, d);
287        {
288            let body = &world.bodies[d_index as usize];
289            assert_eq!(body.set_index, AWAKE_SET);
290            assert_eq!(body.type_, BodyType::Dynamic);
291            assert!(body.island_id != NULL_INDEX);
292            assert_eq!(body.name, "crate");
293            assert!(body.flags & body_flags::DYNAMIC_FLAG != 0);
294        }
295        let xf = get_body_transform(&world, d_index);
296        assert_eq!(crate::math_functions::to_vec2(xf.p).x, 2.0);
297        let awake = &world.solver_sets[AWAKE_SET as usize];
298        assert_eq!(awake.body_sims.len(), 1);
299        assert_eq!(awake.body_states.len(), 1);
300        assert_eq!(awake.body_states[0].linear_velocity.x, 1.0);
301        assert_eq!(awake.island_sims.len(), 1);
302
303        // Sleeping dynamic body gets its own new sleeping set + island.
304        let mut sleep_def = default_body_def();
305        sleep_def.type_ = BodyType::Dynamic;
306        sleep_def.is_awake = false;
307        let z = create_body(&mut world, &sleep_def);
308        let z_index = get_body_full_id(&world, z);
309        let z_set = world.bodies[z_index as usize].set_index;
310        assert!(z_set >= crate::solver_set::FIRST_SLEEPING_SET);
311        assert_eq!(world.solver_sets[z_set as usize].body_sims.len(), 1);
312        assert!(world.solver_sets[z_set as usize].body_states.is_empty());
313        assert!(world.bodies[z_index as usize].island_id != NULL_INDEX);
314
315        // Disabled body: disabled set, no island.
316        let mut disabled_def = default_body_def();
317        disabled_def.type_ = BodyType::Dynamic;
318        disabled_def.is_enabled = false;
319        let x = create_body(&mut world, &disabled_def);
320        let x_index = get_body_full_id(&world, x);
321        assert_eq!(world.bodies[x_index as usize].set_index, DISABLED_SET);
322        assert_eq!(world.bodies[x_index as usize].island_id, NULL_INDEX);
323
324        // Ids are 1-based with generations.
325        assert_eq!(s.index1, 1);
326        assert_eq!(d.index1, 2);
327        assert!(d.generation >= 1);
328
329        world.validate_solver_sets();
330    }
331
332    #[test]
333    fn island_lifecycle_via_bodies() {
334        let mut world = World::new(&default_world_def());
335
336        let mut def = default_body_def();
337        def.type_ = BodyType::Dynamic;
338        let a = create_body(&mut world, &def);
339        let b = create_body(&mut world, &def);
340        let a_index = get_body_full_id(&world, a);
341        let b_index = get_body_full_id(&world, b);
342
343        assert_eq!(world.island_id_pool.id_count(), 2);
344
345        // Remove body A from its island; the island becomes empty and dies.
346        let a_island = world.bodies[a_index as usize].island_id;
347        remove_body_from_island(&mut world, a_index);
348        assert_eq!(world.bodies[a_index as usize].island_id, NULL_INDEX);
349        assert_eq!(world.islands[a_island as usize].set_index, NULL_INDEX);
350        assert_eq!(world.island_id_pool.id_count(), 1);
351
352        // B's island is intact.
353        crate::island::validate_island(&world, world.bodies[b_index as usize].island_id);
354
355        // Name truncation to NAME_LENGTH.
356        let mut long_name = default_body_def();
357        long_name.name = "abcdefghijklmnop".into();
358        let c = create_body(&mut world, &long_name);
359        let c_index = get_body_full_id(&world, c);
360        assert_eq!(
361            world.bodies[c_index as usize].name.len(),
362            crate::constants::NAME_LENGTH as usize
363        );
364    }
365
366    // Slice-5 test: destroy_shape removes the contact it participates in;
367    // destroy_body tears down shapes, islands, and solver set slots.
368    #[test]
369    fn destroy_shape_and_body_lifecycle() {
370        use crate::broad_phase::update_broad_phase_pairs;
371        use crate::geometry::make_box;
372        use crate::shape::{create_polygon_shape, destroy_shape};
373        use crate::table::shape_pair_key;
374        use crate::types::default_shape_def;
375
376        let mut world = World::new(&default_world_def());
377
378        let mut def = default_body_def();
379        def.type_ = BodyType::Dynamic;
380        let body_a = create_body(&mut world, &def);
381        let body_b = create_body(&mut world, &def);
382        let a_index = get_body_full_id(&world, body_a);
383        let b_index = get_body_full_id(&world, body_b);
384
385        let box_poly = make_box(0.5, 0.5);
386        let shape_def = default_shape_def();
387        let sa = create_polygon_shape(&mut world, body_a, &shape_def, &box_poly);
388        let sb = create_polygon_shape(&mut world, body_b, &shape_def, &box_poly);
389
390        update_broad_phase_pairs(&mut world);
391        assert_eq!(world.contact_id_pool.id_count(), 1);
392
393        // Destroying shape B destroys the contact and unlinks both bodies.
394        destroy_shape(&mut world, sb, true);
395        assert_eq!(world.contact_id_pool.id_count(), 0);
396        assert_eq!(world.shape_id_pool.id_count(), 1);
397        assert_eq!(world.bodies[a_index as usize].contact_count, 0);
398        assert_eq!(world.bodies[b_index as usize].contact_count, 0);
399        assert_eq!(world.bodies[b_index as usize].shape_count, 0);
400        assert_eq!(world.bodies[b_index as usize].head_shape_id, NULL_INDEX);
401        assert_eq!(world.shapes[(sb.index1 - 1) as usize].id, NULL_INDEX);
402        assert!(!world
403            .broad_phase
404            .pair_set
405            .contains_key(shape_pair_key(sa.index1 - 1, sb.index1 - 1)));
406
407        // Destroying body A destroys its shape and empties its island.
408        let island_a = world.bodies[a_index as usize].island_id;
409        assert!(island_a != NULL_INDEX);
410        destroy_body(&mut world, body_a);
411        assert_eq!(world.body_id_pool.id_count(), 1);
412        assert_eq!(world.shape_id_pool.id_count(), 0);
413        assert_eq!(world.bodies[a_index as usize].id, NULL_INDEX);
414        assert_eq!(world.bodies[a_index as usize].set_index, NULL_INDEX);
415        assert_eq!(world.islands[island_a as usize].island_id, NULL_INDEX);
416        assert_eq!(
417            world.solver_sets[AWAKE_SET as usize].body_sims.len(),
418            1,
419            "only body B remains awake"
420        );
421        // The remaining sim belongs to body B with a fixed-up local index.
422        assert_eq!(
423            world.solver_sets[AWAKE_SET as usize].body_sims[0].body_id,
424            b_index
425        );
426        assert_eq!(world.bodies[b_index as usize].local_index, 0);
427
428        destroy_body(&mut world, body_b);
429        assert_eq!(world.body_id_pool.id_count(), 0);
430        assert!(world.solver_sets[AWAKE_SET as usize].body_sims.is_empty());
431        assert!(world.solver_sets[AWAKE_SET as usize].island_sims.is_empty());
432
433        world.validate_solver_sets();
434
435        // Slot reuse: a new body recycles the freed slot with a bumped
436        // generation.
437        let reborn = create_body(&mut world, &def);
438        assert_eq!(get_body_full_id(&world, reborn), 1);
439        assert!(reborn.generation > body_b.generation);
440    }
441}