Skip to main content

box3d_rust/shape/
lifecycle.rs

1// Shape creation from shape.c.
2//
3// SPDX-FileCopyrightText: 2025 Erin Catto
4// SPDX-License-Identifier: MIT
5
6use super::{
7    create_shape_proxy, destroy_shape_proxy, get_shape_centroid, shape_flags, Shape, ShapeGeometry,
8};
9use crate::compound::{get_compound_materials, CompoundData};
10use crate::constants::{linear_slop, max_aabb_margin, AABB_MARGIN_FRACTION, MAX_SHAPES};
11use crate::core::{NULL_INDEX, SECRET_COOKIE};
12use crate::geometry::{Capsule, ShapeType, Sphere};
13use crate::height_field::HeightFieldData;
14use crate::hull::{get_hull_points, HullData};
15use crate::id::{ShapeId, NULL_SHAPE_ID};
16use crate::math_functions::{
17    distance, distance_squared, is_valid_float, lerp, min_float, safe_scale, Aabb, Vec3,
18    WorldTransform, VEC3_ZERO,
19};
20use crate::mesh::{is_valid_mesh, MeshData};
21use crate::solver_set::DISABLED_SET;
22use crate::types::{BodyType, ShapeDef};
23use crate::world::World;
24
25/// AABB margin for the broad phase fat AABB, limited by shape size.
26/// (static b3ComputeShapeMargin)
27pub fn compute_shape_margin(shape: &Shape) -> f32 {
28    let margin = match &shape.geometry {
29        ShapeGeometry::Sphere(sphere) => sphere.radius,
30        ShapeGeometry::Capsule(capsule) => {
31            0.5 * distance(capsule.center2, capsule.center1) + capsule.radius
32        }
33        ShapeGeometry::Hull(hull) => {
34            let points = get_hull_points(hull);
35            let mut max_extent_sqr = 0.0f32;
36            for point in points {
37                let dist_sqr = distance_squared(*point, hull.center);
38                max_extent_sqr = crate::math_functions::max_float(max_extent_sqr, dist_sqr);
39            }
40            max_extent_sqr.sqrt()
41        }
42        ShapeGeometry::Mesh { .. } | ShapeGeometry::HeightField(_) | ShapeGeometry::Compound(_) => {
43            // Static-only shapes: return the cap so incidental use is generous.
44            return max_aabb_margin();
45        }
46    };
47
48    min_float(max_aabb_margin(), AABB_MARGIN_FRACTION * margin)
49}
50
51/// Create a shape on a body. Returns the raw shape id, or NULL_INDEX on failure.
52/// (static b3CreateShapeInternal)
53pub(crate) fn create_shape_internal(
54    world: &mut World,
55    body_index: i32,
56    body_transform: WorldTransform,
57    def: &ShapeDef,
58    geometry: ShapeGeometry,
59) -> i32 {
60    let shape_type = geometry.shape_type();
61
62    let shape_id = world.shape_id_pool.alloc_id();
63
64    if shape_id == world.shapes.len() as i32 {
65        world.shapes.push(Shape::default());
66    } else {
67        debug_assert!(world.shapes[shape_id as usize].id == NULL_INDEX);
68    }
69
70    let (body_raw_id, body_set_index, body_type, head_shape_id) = {
71        let body = &world.bodies[body_index as usize];
72        (body.id, body.set_index, body.type_, body.head_shape_id)
73    };
74
75    // Truncate like b3StrCpy into B3_SHAPE_NAME_LENGTH (+ NameCache in Rust).
76    let truncated = if def.name.len() > crate::constants::SHAPE_NAME_LENGTH {
77        let mut end = crate::constants::SHAPE_NAME_LENGTH;
78        while end > 0 && !def.name.is_char_boundary(end) {
79            end -= 1;
80        }
81        &def.name[..end]
82    } else {
83        def.name.as_str()
84    };
85    let name_id = world.names.add_name(truncated);
86
87    {
88        let shape = &mut world.shapes[shape_id as usize];
89        shape.geometry = geometry;
90
91        shape.id = shape_id;
92        shape.body_id = body_raw_id;
93        shape.density = def.density;
94        shape.explosion_scale = def.explosion_scale;
95        shape.filter = def.filter;
96        shape.user_data = def.user_data;
97        shape.user_shape = 0;
98        shape.flags = 0;
99        if def.enable_sensor_events {
100            shape.flags |= shape_flags::ENABLE_SENSOR_EVENTS;
101        }
102        if def.enable_contact_events {
103            shape.flags |= shape_flags::ENABLE_CONTACT_EVENTS;
104        }
105        if def.enable_custom_filtering {
106            shape.flags |= shape_flags::ENABLE_CUSTOM_FILTERING;
107        }
108        if def.enable_hit_events {
109            shape.flags |= shape_flags::ENABLE_HIT_EVENTS;
110        }
111        if def.enable_pre_solve_events {
112            shape.flags |= shape_flags::ENABLE_PRE_SOLVE_EVENTS;
113        }
114        shape.proxy_key = NULL_INDEX;
115        shape.local_centroid = get_shape_centroid(shape);
116        shape.aabb_margin = compute_shape_margin(shape);
117        shape.aabb = Aabb {
118            lower_bound: VEC3_ZERO,
119            upper_bound: VEC3_ZERO,
120        };
121        shape.fat_aabb = Aabb {
122            lower_bound: VEC3_ZERO,
123            upper_bound: VEC3_ZERO,
124        };
125        shape.name_id = name_id;
126        shape.generation = shape.generation.wrapping_add(1);
127
128        if shape_type == ShapeType::Compound {
129            // Own a copy of the compound materials so every shape frees its array
130            // the same way. Compounds are few, so the copy is cheap and avoids
131            // aliasing the geometry blob. (C: b3CreateShapeInternal)
132            let mats = match &shape.geometry {
133                ShapeGeometry::Compound(compound) => get_compound_materials(compound).to_vec(),
134                _ => unreachable!(),
135            };
136            if mats.is_empty() {
137                shape.material = def.base_material;
138                shape.materials.clear();
139            } else if mats.len() == 1 {
140                shape.material = mats[0];
141                shape.materials.clear();
142            } else {
143                shape.material = def.base_material;
144                shape.materials = mats;
145            }
146        } else if def.materials.len() > 1 {
147            shape.materials = def.materials.clone();
148            shape.material = def.base_material;
149        } else if def.materials.len() == 1 {
150            shape.material = def.materials[0];
151            shape.materials.clear();
152        } else {
153            shape.material = def.base_material;
154            shape.materials.clear();
155        }
156    }
157
158    if body_set_index != DISABLED_SET {
159        let force_pair_creation = def.invoke_contact_creation && shape_type != ShapeType::Compound;
160        let (shapes, broad_phase) = (&mut world.shapes, &mut world.broad_phase);
161        create_shape_proxy(
162            &mut shapes[shape_id as usize],
163            broad_phase,
164            body_type,
165            body_transform,
166            force_pair_creation,
167        );
168    }
169
170    // Add to shape doubly linked list
171    if head_shape_id != NULL_INDEX {
172        world.shapes[head_shape_id as usize].prev_shape_id = shape_id;
173    }
174
175    world.shapes[shape_id as usize].prev_shape_id = NULL_INDEX;
176    world.shapes[shape_id as usize].next_shape_id = head_shape_id;
177    world.bodies[body_index as usize].head_shape_id = shape_id;
178    world.bodies[body_index as usize].shape_count += 1;
179
180    if def.is_sensor {
181        world.shapes[shape_id as usize].sensor_index = world.sensors.len() as i32;
182        world.sensors.push(crate::sensor::Sensor::new(shape_id));
183    } else {
184        world.shapes[shape_id as usize].sensor_index = NULL_INDEX;
185    }
186
187    world.validate_solver_sets();
188
189    shape_id
190}
191
192/// (static b3CreateShape)
193fn create_shape(
194    world: &mut World,
195    body_id: crate::id::BodyId,
196    def: &ShapeDef,
197    geometry: ShapeGeometry,
198) -> ShapeId {
199    use crate::body::{
200        body_flags, get_body_full_id, get_body_transform_quick, sync_body_flags,
201        update_body_mass_data,
202    };
203
204    debug_assert!(def.internal_value == SECRET_COOKIE);
205    debug_assert!(is_valid_float(def.density) && def.density >= 0.0);
206    debug_assert!(is_valid_float(def.base_material.friction) && def.base_material.friction >= 0.0);
207    debug_assert!(
208        is_valid_float(def.base_material.restitution) && def.base_material.restitution >= 0.0
209    );
210
211    debug_assert!(!world.locked);
212    if world.locked {
213        return NULL_SHAPE_ID;
214    }
215
216    if world.shapes.len() as i32 == MAX_SHAPES && world.shape_id_pool.free_array.is_empty() {
217        debug_assert!(false);
218        return NULL_SHAPE_ID;
219    }
220
221    let body_index = get_body_full_id(world, body_id);
222    let shape_type = geometry.shape_type();
223
224    if world.bodies[body_index as usize].type_ != BodyType::Static
225        && (shape_type == ShapeType::Compound || shape_type == ShapeType::Height)
226    {
227        // Compound and height shapes must be on static bodies.
228        return NULL_SHAPE_ID;
229    }
230
231    world.locked = true;
232
233    let body_transform = get_body_transform_quick(world, &world.bodies[body_index as usize]);
234
235    let shape_id = create_shape_internal(world, body_index, body_transform, def, geometry);
236
237    if shape_id == NULL_INDEX {
238        world.locked = false;
239        return NULL_SHAPE_ID;
240    }
241
242    if def.update_body_mass {
243        update_body_mass_data(world, body_index);
244    } else if world.bodies[body_index as usize].flags & body_flags::DIRTY_MASS == 0 {
245        world.bodies[body_index as usize].flags |= body_flags::DIRTY_MASS;
246        sync_body_flags(world, body_index);
247    }
248
249    world.validate_solver_sets();
250
251    let id = ShapeId {
252        index1: shape_id + 1,
253        world0: body_id.world0,
254        generation: world.shapes[shape_id as usize].generation,
255    };
256
257    world.locked = false;
258
259    id
260}
261
262/// (b3CreateSphereShape)
263pub fn create_sphere_shape(
264    world: &mut World,
265    body_id: crate::id::BodyId,
266    def: &ShapeDef,
267    sphere: &Sphere,
268) -> ShapeId {
269    let shape_id = create_shape(world, body_id, def, ShapeGeometry::Sphere(*sphere));
270    if shape_id.index1 != 0 {
271        crate::recording::with_recording(world, |rec| {
272            rec.write_create_sphere_shape(body_id, def, *sphere, shape_id);
273        });
274    }
275    shape_id
276}
277
278/// (b3CreateCapsuleShape)
279pub fn create_capsule_shape(
280    world: &mut World,
281    body_id: crate::id::BodyId,
282    def: &ShapeDef,
283    capsule: &Capsule,
284) -> ShapeId {
285    let length_sqr = distance_squared(capsule.center1, capsule.center2);
286    let shape_id = if length_sqr <= linear_slop() * linear_slop() {
287        let sphere = Sphere {
288            center: lerp(capsule.center1, capsule.center2, 0.5),
289            radius: capsule.radius,
290        };
291        create_shape(world, body_id, def, ShapeGeometry::Sphere(sphere))
292    } else {
293        create_shape(world, body_id, def, ShapeGeometry::Capsule(*capsule))
294    };
295    if shape_id.index1 != 0 {
296        crate::recording::with_recording(world, |rec| {
297            rec.write_create_capsule_shape(body_id, def, *capsule, shape_id);
298        });
299    }
300    shape_id
301}
302
303/// (b3CreateHullShape)
304pub fn create_hull_shape(
305    world: &mut World,
306    body_id: crate::id::BodyId,
307    def: &ShapeDef,
308    hull: &HullData,
309) -> ShapeId {
310    debug_assert!(crate::hull::is_valid_hull(hull));
311    debug_assert!(hull.hash != 0);
312    let shared = world.hull_database.add(hull);
313    let shape_id = create_shape(world, body_id, def, ShapeGeometry::Hull(shared));
314    if shape_id.index1 != 0 {
315        crate::recording::with_recording(world, |rec| {
316            let geometry_id = rec.registry.intern_hull(hull);
317            rec.write_create_hull_shape(body_id, def, geometry_id, shape_id);
318        });
319    }
320    shape_id
321}
322
323/// (b3CreateMeshShape)
324///
325/// The shape stores an owned clone of `mesh` (C keeps a borrowed pointer). Per-instance
326/// `scale` is sanitized via [`safe_scale`].
327pub fn create_mesh_shape(
328    world: &mut World,
329    body_id: crate::id::BodyId,
330    def: &ShapeDef,
331    mesh: &MeshData,
332    scale: Vec3,
333) -> ShapeId {
334    debug_assert!(is_valid_mesh(Some(mesh)));
335    debug_assert!(mesh.hash != 0);
336    let shape_id = create_shape(
337        world,
338        body_id,
339        def,
340        ShapeGeometry::Mesh {
341            data: mesh.clone(),
342            scale: safe_scale(scale),
343        },
344    );
345    if shape_id.index1 != 0 {
346        crate::recording::with_recording(world, |rec| {
347            let geometry_id = rec.registry.intern_mesh(mesh);
348            rec.write_create_mesh_shape(body_id, def, geometry_id, safe_scale(scale), shape_id);
349        });
350    }
351    shape_id
352}
353
354/// (b3CreateHeightFieldShape)
355///
356/// Height fields must be on static bodies (enforced in [`create_shape`]).
357pub fn create_height_field_shape(
358    world: &mut World,
359    body_id: crate::id::BodyId,
360    def: &ShapeDef,
361    height_field: &HeightFieldData,
362) -> ShapeId {
363    debug_assert!(height_field.hash != 0);
364    let shape_id = create_shape(
365        world,
366        body_id,
367        def,
368        ShapeGeometry::HeightField(height_field.clone()),
369    );
370    if shape_id.index1 != 0 {
371        crate::recording::with_recording(world, |rec| {
372            let geometry_id = rec.registry.intern_height_field(height_field);
373            rec.write_create_height_field_shape(body_id, def, geometry_id, shape_id);
374        });
375    }
376    shape_id
377}
378
379/// (b3CreateCompoundShape)
380///
381/// Compounds must be on static non-sensor bodies. Materials are copied from the
382/// compound geometry into the shape (see [`create_shape_internal`]).
383pub fn create_compound_shape(
384    world: &mut World,
385    body_id: crate::id::BodyId,
386    def: &ShapeDef,
387    compound: &CompoundData,
388) -> ShapeId {
389    debug_assert!(!def.is_sensor);
390    let shape_id = create_shape(
391        world,
392        body_id,
393        def,
394        ShapeGeometry::Compound(compound.clone()),
395    );
396    if shape_id.index1 != 0 {
397        crate::recording::with_recording(world, |rec| {
398            let geometry_id = rec.registry.intern_compound(compound);
399            rec.write_create_compound_shape(body_id, def, geometry_id, shape_id);
400        });
401    }
402    shape_id
403}
404
405/// Resolve a ShapeId to the shape index. (b3GetShape)
406pub fn get_shape(world: &World, shape_id: ShapeId) -> i32 {
407    let id = shape_id.index1 - 1;
408    let shape = &world.shapes[id as usize];
409    debug_assert!(shape.id == id && shape.generation == shape_id.generation);
410    id
411}
412
413/// Shape identifier validation. (b3Shape_IsValid)
414pub fn shape_is_valid(world: &World, id: ShapeId) -> bool {
415    if id.index1 < 1 || (world.shapes.len() as i32) < id.index1 {
416        return false;
417    }
418    let shape = &world.shapes[(id.index1 - 1) as usize];
419    if shape.id == NULL_INDEX {
420        return false;
421    }
422    shape.generation == id.generation
423}
424
425/// (b3Shape_GetHull) — returns None for non-hull shapes.
426pub fn shape_get_hull(world: &World, shape_id: ShapeId) -> Option<&HullData> {
427    let index = get_shape(world, shape_id);
428    match &world.shapes[index as usize].geometry {
429        ShapeGeometry::Hull(hull) => Some(hull.as_ref()),
430        _ => {
431            debug_assert!(false, "shape is not a hull");
432            None
433        }
434    }
435}
436
437/// Release hull-database ownership when geometry is about to change.
438/// Does not clear materials. (`b3DestroyShapeAllocationForShapeChange`)
439pub(crate) fn destroy_shape_allocation_for_shape_change(world: &mut World, shape_index: i32) {
440    if matches!(
441        world.shapes[shape_index as usize].geometry,
442        ShapeGeometry::Hull(_)
443    ) {
444        let geometry = std::mem::replace(
445            &mut world.shapes[shape_index as usize].geometry,
446            ShapeGeometry::default(),
447        );
448        if let ShapeGeometry::Hull(rc) = &geometry {
449            world.hull_database.release(rc);
450        }
451    }
452
453    let user_shape = world.shapes[shape_index as usize].user_shape;
454    if user_shape != 0 {
455        if let Some(destroy) = world.destroy_debug_shape {
456            destroy(user_shape, world.user_debug_shape_context);
457        }
458        world.shapes[shape_index as usize].user_shape = 0;
459    }
460}
461
462/// Free hull-database and material allocations for a shape.
463/// (b3DestroyShapeAllocations)
464fn destroy_shape_allocations(world: &mut World, shape_index: i32) {
465    destroy_shape_allocation_for_shape_change(world, shape_index);
466    world.shapes[shape_index as usize].materials.clear();
467}
468
469/// Destroy a shape on a body. (static b3DestroyShapeInternal)
470pub(crate) fn destroy_shape_internal(
471    world: &mut World,
472    shape_index: i32,
473    body_index: i32,
474    wake_bodies: bool,
475) {
476    let _ = wake_bodies; // contact destroy uses this; contacts land next
477
478    let (prev_shape_id, next_shape_id, sensor_index) = {
479        let shape = &world.shapes[shape_index as usize];
480        (shape.prev_shape_id, shape.next_shape_id, shape.sensor_index)
481    };
482
483    // Remove the shape from the body's doubly linked list.
484    if prev_shape_id != NULL_INDEX {
485        world.shapes[prev_shape_id as usize].next_shape_id = next_shape_id;
486    }
487    if next_shape_id != NULL_INDEX {
488        world.shapes[next_shape_id as usize].prev_shape_id = prev_shape_id;
489    }
490    if shape_index == world.bodies[body_index as usize].head_shape_id {
491        world.bodies[body_index as usize].head_shape_id = next_shape_id;
492    }
493    world.bodies[body_index as usize].shape_count -= 1;
494
495    // Remove from broad-phase.
496    {
497        let (shapes, broad_phase) = (&mut world.shapes, &mut world.broad_phase);
498        destroy_shape_proxy(&mut shapes[shape_index as usize], broad_phase);
499    }
500
501    // Destroy any contacts associated with the shape.
502    let mut contact_key = world.bodies[body_index as usize].head_contact_key;
503    while contact_key != NULL_INDEX {
504        let contact_id = contact_key >> 1;
505        let edge_index = contact_key & 1;
506        let next_key = world.contacts[contact_id as usize].edges[edge_index as usize].next_key;
507        let shape_id_a = world.contacts[contact_id as usize].shape_id_a;
508        let shape_id_b = world.contacts[contact_id as usize].shape_id_b;
509        contact_key = next_key;
510
511        if shape_id_a == shape_index || shape_id_b == shape_index {
512            crate::contact::destroy_contact(world, contact_id, wake_bodies);
513        }
514    }
515
516    if sensor_index != NULL_INDEX {
517        crate::sensor::destroy_sensor(world, shape_index);
518    }
519
520    destroy_shape_allocations(world, shape_index);
521
522    world.shape_id_pool.free_id(shape_index);
523    world.shapes[shape_index as usize].id = NULL_INDEX;
524
525    world.validate_solver_sets();
526}
527
528/// Destroy a shape. (b3DestroyShape)
529pub fn destroy_shape(world: &mut World, shape_id: ShapeId, update_body_mass: bool) {
530    debug_assert!(!world.locked);
531    if world.locked {
532        return;
533    }
534
535    crate::recording::with_recording(world, |rec| {
536        rec.write_destroy_shape(shape_id, update_body_mass);
537    });
538
539    world.locked = true;
540
541    let shape_index = get_shape(world, shape_id);
542    let body_index = world.shapes[shape_index as usize].body_id;
543
544    destroy_shape_internal(world, shape_index, body_index, true);
545
546    if update_body_mass {
547        crate::body::update_body_mass_data(world, body_index);
548    }
549
550    world.locked = false;
551}