Skip to main content

box3d_rust/world/
api.rs

1//! The b3World_* public API from physics_world.c: validity, events, enable
2//! flags, tuning setters, counters, callbacks, explosions, and the static-tree
3//! rebuild. World queries (overlap/cast) live in query.rs.
4//!
5//! SPDX-FileCopyrightText: 2025 Erin Catto
6//! SPDX-License-Identifier: MIT
7
8use super::{
9    default_friction_callback, default_restitution_callback, CustomFilterFcn, PreSolveFcn, Profile,
10    TaskContext, World,
11};
12use crate::body::{get_body_transform_quick, wake_body};
13use crate::constants::{GRAPH_COLOR_COUNT, MAX_WORKERS};
14use crate::distance::{make_proxy, shape_distance, DistanceInput, SimplexCache};
15use crate::events::{BodyMoveEvent, ContactEvents, JointEvent, SensorEvents};
16use crate::math_functions::{
17    aabb_union, add, clamp_float, clamp_int, cross, inv_transform_world_point, is_valid_float,
18    is_valid_position, length_squared, max_int, mul_add, mul_mv, mul_sv, normalize, offset_aabb,
19    rotate_vector, sub, Aabb, Pos, Vec3, TRANSFORM_IDENTITY, VEC3_ZERO,
20};
21use crate::sensor::SensorTaskContext;
22use crate::shape::{get_shape_centroid, get_shape_projected_area, make_shape_proxy};
23use crate::solver_set::{wake_solver_set, AWAKE_SET, FIRST_SLEEPING_SET};
24use crate::types::{
25    BodyType, Capacity, Counters, ExplosionDef, FrictionCallback, RestitutionCallback,
26    BODY_TYPE_COUNT,
27};
28
29/// World id validity. (b3World_IsValid)
30///
31/// C validates the id against the global world registry; the registry-less
32/// Rust port owns the `World`, so a reachable world is valid unless it has
33/// been torn down.
34pub fn world_is_valid(world: &World) -> bool {
35    world.in_use
36}
37
38/// Get the body events for the current time step. The event data is transient.
39/// Do not store a reference to this data. (b3World_GetBodyEvents)
40pub fn world_get_body_events(world: &World) -> &[BodyMoveEvent] {
41    debug_assert!(!world.locked);
42    if world.locked {
43        return &[];
44    }
45
46    &world.body_move_events
47}
48
49/// Get sensor events for the current time step. The event data is transient.
50/// Do not store a reference to this data. (b3World_GetSensorEvents)
51pub fn world_get_sensor_events(world: &World) -> SensorEvents<'_> {
52    debug_assert!(!world.locked);
53    if world.locked {
54        return SensorEvents {
55            begin_events: &[],
56            end_events: &[],
57        };
58    }
59
60    // Careful to use previous buffer
61    let end_event_array_index = 1 - world.end_event_array_index;
62
63    SensorEvents {
64        begin_events: &world.sensor_begin_events,
65        end_events: &world.sensor_end_events[end_event_array_index as usize],
66    }
67}
68
69/// Get contact events for this current time step. The event data is transient.
70/// Do not store a reference to this data. (b3World_GetContactEvents)
71pub fn world_get_contact_events(world: &World) -> ContactEvents<'_> {
72    debug_assert!(!world.locked);
73    if world.locked {
74        return ContactEvents {
75            begin_events: &[],
76            end_events: &[],
77            hit_events: &[],
78        };
79    }
80
81    // Careful to use previous buffer
82    let end_event_array_index = 1 - world.end_event_array_index;
83
84    ContactEvents {
85        begin_events: &world.contact_begin_events,
86        end_events: &world.contact_end_events[end_event_array_index as usize],
87        hit_events: &world.contact_hit_events,
88    }
89}
90
91/// Get the joint events for the current time step. The event data is
92/// transient. Do not store a reference to this data. (b3World_GetJointEvents)
93pub fn world_get_joint_events(world: &World) -> &[JointEvent] {
94    debug_assert!(!world.locked);
95    if world.locked {
96        return &[];
97    }
98
99    &world.joint_events
100}
101
102/// Enable/disable sleep. (b3World_EnableSleeping)
103pub fn world_enable_sleeping(world: &mut World, flag: bool) {
104    debug_assert!(!world.locked);
105    if world.locked {
106        return;
107    }
108
109    if flag == world.enable_sleep {
110        return;
111    }
112
113    crate::recording::capture::rec(world, |rec, wid| {
114        rec.write_world_enable_sleeping(wid, flag);
115    });
116
117    world.enable_sleep = flag;
118
119    if !flag {
120        let set_count = world.solver_sets.len() as i32;
121        for i in FIRST_SLEEPING_SET..set_count {
122            if !world.solver_sets[i as usize].body_sims.is_empty() {
123                wake_solver_set(world, i);
124            }
125        }
126    }
127}
128
129/// Is sleeping enabled? (b3World_IsSleepingEnabled)
130pub fn world_is_sleeping_enabled(world: &World) -> bool {
131    world.enable_sleep
132}
133
134/// Enable/disable constraint warm starting. (b3World_EnableWarmStarting)
135pub fn world_enable_warm_starting(world: &mut World, flag: bool) {
136    debug_assert!(!world.locked);
137    if world.locked {
138        return;
139    }
140
141    crate::recording::capture::rec(world, |rec, wid| {
142        rec.write_world_enable_warm_starting(wid, flag);
143    });
144
145    world.enable_warm_starting = flag;
146}
147
148/// Is constraint warm starting enabled? (b3World_IsWarmStartingEnabled)
149pub fn world_is_warm_starting_enabled(world: &World) -> bool {
150    world.enable_warm_starting
151}
152
153/// Get the number of awake bodies. (b3World_GetAwakeBodyCount)
154pub fn world_get_awake_body_count(world: &World) -> i32 {
155    world.solver_sets[AWAKE_SET as usize].body_sims.len() as i32
156}
157
158/// Enable/disable continuous collision. (b3World_EnableContinuous)
159pub fn world_enable_continuous(world: &mut World, flag: bool) {
160    debug_assert!(!world.locked);
161    if world.locked {
162        return;
163    }
164
165    crate::recording::capture::rec(world, |rec, wid| {
166        rec.write_world_enable_continuous(wid, flag);
167    });
168
169    world.enable_continuous = flag;
170}
171
172/// Is continuous collision enabled? (b3World_IsContinuousEnabled)
173pub fn world_is_continuous_enabled(world: &World) -> bool {
174    world.enable_continuous
175}
176
177/// Enable/disable speculative contacts. (b3World_EnableSpeculative)
178pub fn world_enable_speculative(world: &mut World, flag: bool) {
179    debug_assert!(!world.locked);
180    if world.locked {
181        return;
182    }
183
184    crate::recording::capture::rec(world, |rec, wid| {
185        rec.write_world_enable_speculative(wid, flag);
186    });
187
188    world.enable_speculative = flag;
189}
190
191/// Is speculative contact enabled?
192pub fn world_is_speculative_enabled(world: &World) -> bool {
193    world.enable_speculative
194}
195
196/// Adjust the restitution threshold. (b3World_SetRestitutionThreshold)
197pub fn world_set_restitution_threshold(world: &mut World, value: f32) {
198    debug_assert!(!world.locked);
199    if world.locked {
200        return;
201    }
202
203    crate::recording::capture::rec(world, |rec, wid| {
204        rec.write_world_set_restitution_threshold(wid, value);
205    });
206
207    world.restitution_threshold = clamp_float(value, 0.0, f32::MAX);
208}
209
210/// Get the restitution speed threshold. (b3World_GetRestitutionThreshold)
211pub fn world_get_restitution_threshold(world: &World) -> f32 {
212    world.restitution_threshold
213}
214
215/// Adjust the hit event threshold. (b3World_SetHitEventThreshold)
216pub fn world_set_hit_event_threshold(world: &mut World, value: f32) {
217    debug_assert!(!world.locked);
218    if world.locked {
219        return;
220    }
221
222    crate::recording::capture::rec(world, |rec, wid| {
223        rec.write_world_set_hit_event_threshold(wid, value);
224    });
225
226    world.hit_event_threshold = clamp_float(value, 0.0, f32::MAX);
227}
228
229/// Get the hit event speed threshold. (b3World_GetHitEventThreshold)
230pub fn world_get_hit_event_threshold(world: &World) -> f32 {
231    world.hit_event_threshold
232}
233
234/// Adjust contact tuning parameters. (b3World_SetContactTuning)
235pub fn world_set_contact_tuning(
236    world: &mut World,
237    hertz: f32,
238    damping_ratio: f32,
239    contact_speed: f32,
240) {
241    crate::recording::capture::rec(world, |rec, wid| {
242        rec.write_world_set_contact_tuning(wid, hertz, damping_ratio, contact_speed);
243    });
244
245    debug_assert!(!world.locked);
246    if world.locked {
247        return;
248    }
249
250    world.contact_hertz = clamp_float(hertz, 0.0, f32::MAX);
251    world.contact_damping_ratio = clamp_float(damping_ratio, 0.0, f32::MAX);
252    world.contact_speed = clamp_float(contact_speed, 0.0, f32::MAX);
253}
254
255/// Set the contact recycle distance. (b3World_SetContactRecycleDistance)
256pub fn world_set_contact_recycle_distance(world: &mut World, recycle_distance: f32) {
257    debug_assert!(!world.locked);
258    if world.locked {
259        return;
260    }
261
262    crate::recording::capture::rec(world, |rec, wid| {
263        rec.write_world_set_contact_recycle_distance(wid, recycle_distance);
264    });
265
266    world.contact_recycle_distance = clamp_float(recycle_distance, 0.0, f32::MAX);
267}
268
269/// Get the contact recycle distance. (b3World_GetContactRecycleDistance)
270pub fn world_get_contact_recycle_distance(world: &World) -> f32 {
271    world.contact_recycle_distance
272}
273
274/// Set the maximum linear speed. (b3World_SetMaximumLinearSpeed)
275pub fn world_set_maximum_linear_speed(world: &mut World, maximum_linear_speed: f32) {
276    crate::recording::capture::rec(world, |rec, wid| {
277        rec.write_world_set_maximum_linear_speed(wid, maximum_linear_speed);
278    });
279
280    debug_assert!(is_valid_float(maximum_linear_speed) && maximum_linear_speed > 0.0);
281
282    debug_assert!(!world.locked);
283    if world.locked {
284        return;
285    }
286
287    world.max_linear_speed = maximum_linear_speed;
288}
289
290/// Get the maximum linear speed. (b3World_GetMaximumLinearSpeed)
291pub fn world_get_maximum_linear_speed(world: &World) -> f32 {
292    world.max_linear_speed
293}
294
295/// Get the current world performance profile. (b3World_GetProfile)
296pub fn world_get_profile(world: &World) -> Profile {
297    world.profile
298}
299
300/// Get world counters and sizes. (b3World_GetCounters)
301pub fn world_get_counters(world: &World) -> Counters {
302    let mut s = Counters {
303        body_count: world.body_id_pool.id_count(),
304        shape_count: world.shape_id_pool.id_count(),
305        contact_count: world.contact_id_pool.id_count(),
306        joint_count: world.joint_id_pool.id_count(),
307        island_count: world.island_id_pool.id_count(),
308        sat_call_count: world.sat_call_count,
309        sat_cache_hit_count: world.sat_cache_hit_count,
310        manifold_counts: world.manifold_counts,
311        ..Default::default()
312    };
313
314    let static_tree = &world.broad_phase.trees[BodyType::Static as usize];
315    s.static_tree_height = static_tree.height();
316
317    let dynamic_tree = &world.broad_phase.trees[BodyType::Dynamic as usize];
318    let kinematic_tree = &world.broad_phase.trees[BodyType::Kinematic as usize];
319    s.tree_height = max_int(dynamic_tree.height(), kinematic_tree.height());
320
321    // stack_used, byte_count, arena_capacity, and task_count stay zero.
322
323    for i in 0..world.worker_count as usize {
324        s.recycled_contact_count += world.task_contexts[i].recycled_contact_count;
325        s.distance_iterations = max_int(
326            s.distance_iterations,
327            world.task_contexts[i].distance_iterations,
328        );
329        s.push_back_iterations = max_int(
330            s.push_back_iterations,
331            world.task_contexts[i].push_back_iterations,
332        );
333        s.root_iterations = max_int(s.root_iterations, world.task_contexts[i].root_iterations);
334    }
335
336    for i in 0..GRAPH_COLOR_COUNT as usize {
337        let color = &world.constraint_graph.colors[i];
338        let color_contact_count = (color.convex_contacts.len() + color.contacts.len()) as i32;
339        s.color_counts[i] = color_contact_count + color.joint_sims.len() as i32;
340        s.awake_contact_count += color_contact_count;
341    }
342    s.awake_contact_count += world.solver_sets[AWAKE_SET as usize].contact_indices.len() as i32;
343
344    s
345}
346
347/// Get the maximum capacity the world has reached. (b3World_GetMaxCapacity)
348pub fn world_get_max_capacity(world: &World) -> Capacity {
349    world.max_capacity
350}
351
352/// Set the user data pointer. (b3World_SetUserData)
353pub fn world_set_user_data(world: &mut World, user_data: u64) {
354    world.user_data = user_data;
355}
356
357/// Get the user data pointer. (b3World_GetUserData)
358pub fn world_get_user_data(world: &World) -> u64 {
359    world.user_data
360}
361
362/// Register the friction callback. Passing `None` restores the default.
363/// (b3World_SetFrictionCallback)
364pub fn world_set_friction_callback(world: &mut World, callback: Option<FrictionCallback>) {
365    debug_assert!(!world.locked);
366    if world.locked {
367        return;
368    }
369
370    world.friction_callback = Some(callback.unwrap_or(default_friction_callback));
371}
372
373/// Register the restitution callback. Passing `None` restores the default.
374/// (b3World_SetRestitutionCallback)
375pub fn world_set_restitution_callback(world: &mut World, callback: Option<RestitutionCallback>) {
376    debug_assert!(!world.locked);
377    if world.locked {
378        return;
379    }
380
381    world.restitution_callback = Some(callback.unwrap_or(default_restitution_callback));
382}
383
384/// Register the custom filter callback. (b3World_SetCustomFilterCallback)
385pub fn world_set_custom_filter_callback(
386    world: &mut World,
387    fcn: Option<CustomFilterFcn>,
388    context: u64,
389) {
390    world.custom_filter_fcn = fcn;
391    world.custom_filter_context = context;
392}
393
394/// Register the pre-solve callback. (b3World_SetPreSolveCallback)
395pub fn world_set_pre_solve_callback(world: &mut World, fcn: Option<PreSolveFcn>, context: u64) {
396    world.pre_solve_fcn = fcn;
397    world.pre_solve_context = context;
398}
399
400/// Set the gravity vector. (b3World_SetGravity)
401pub fn world_set_gravity(world: &mut World, gravity: Vec3) {
402    crate::recording::capture::rec(world, |rec, wid| {
403        rec.write_world_set_gravity(wid, gravity);
404    });
405    world.gravity = gravity;
406}
407
408/// Get the gravity vector. (b3World_GetGravity)
409pub fn world_get_gravity(world: &World) -> Vec3 {
410    world.gravity
411}
412
413/// Rebuild the static broad-phase tree. (b3World_RebuildStaticTree)
414pub fn world_rebuild_static_tree(world: &mut World) {
415    debug_assert!(!world.locked);
416    if world.locked {
417        return;
418    }
419
420    crate::recording::capture::rec(world, |rec, wid| {
421        rec.write_world_rebuild_static_tree(wid);
422    });
423
424    world.broad_phase.trees[BodyType::Static as usize].rebuild(true);
425}
426
427/// Apply a radial explosion. (b3World_Explode + static ExplosionCallback)
428pub fn world_explode(world: &mut World, explosion_def: &ExplosionDef) {
429    crate::recording::capture::rec(world, |rec, wid| {
430        rec.write_world_explode(wid, *explosion_def);
431    });
432
433    let mask_bits = explosion_def.mask_bits;
434    let position = explosion_def.position;
435    let radius = explosion_def.radius;
436    let falloff = explosion_def.falloff;
437    let impulse_per_area = explosion_def.impulse_per_area;
438
439    debug_assert!(is_valid_position(position));
440    debug_assert!(is_valid_float(radius) && radius >= 0.0);
441    debug_assert!(is_valid_float(falloff) && falloff >= 0.0);
442    debug_assert!(is_valid_float(impulse_per_area));
443
444    debug_assert!(!world.locked);
445    if world.locked {
446        return;
447    }
448
449    // Locked due to waking
450    world.locked = true;
451
452    let extent = radius + falloff;
453    let local_box = Aabb {
454        lower_bound: Vec3 {
455            x: -extent,
456            y: -extent,
457            z: -extent,
458        },
459        upper_bound: Vec3 {
460            x: extent,
461            y: extent,
462            z: extent,
463        },
464    };
465    let aabb = offset_aabb(local_box, position);
466
467    // C applies impulses inside the tree traversal, but waking needs &mut World
468    // while the tree is borrowed. Collect candidate shape ids first, then apply.
469    let mut shape_ids: Vec<i32> = Vec::new();
470    world.broad_phase.trees[BodyType::Dynamic as usize].query(
471        aabb,
472        mask_bits,
473        false,
474        |_, user_data| {
475            shape_ids.push(user_data as i32);
476            true
477        },
478    );
479
480    for shape_id in shape_ids {
481        explode_shape(world, shape_id, position, radius, falloff, impulse_per_area);
482    }
483
484    world.locked = false;
485}
486
487/// Apply the explosion impulse to one candidate shape. (static ExplosionCallback)
488fn explode_shape(
489    world: &mut World,
490    shape_id: i32,
491    position: Pos,
492    radius: f32,
493    falloff: f32,
494    impulse_per_area: f32,
495) {
496    let shape = &world.shapes[shape_id as usize];
497    if shape.explosion_scale == 0.0 {
498        return;
499    }
500
501    let body_id = shape.body_id;
502    let body = &world.bodies[body_id as usize];
503    debug_assert!(body.type_ == BodyType::Dynamic);
504
505    let xf = get_body_transform_quick(world, body);
506
507    // Re-center the explosion into the shape local frame
508    let local_position = inv_transform_world_point(xf, position);
509
510    let input = DistanceInput {
511        proxy_a: make_shape_proxy(shape),
512        proxy_b: make_proxy(&[local_position], 0.0),
513        transform: TRANSFORM_IDENTITY,
514        use_radii: true,
515    };
516
517    let mut cache = SimplexCache::default();
518    let output = shape_distance(&input, &mut cache, None);
519
520    if output.distance > radius + falloff {
521        return;
522    }
523
524    // Snapshot shape-derived values before waking (wake may reshuffle sims).
525    let mut closest_point = output.point_a;
526    if output.distance == 0.0 {
527        closest_point = get_shape_centroid(shape);
528    }
529
530    let mut direction = sub(closest_point, local_position);
531    if length_squared(direction) > 100.0 * f32::EPSILON * f32::EPSILON {
532        direction = normalize(direction);
533    } else {
534        direction = Vec3 {
535            x: 1.0,
536            y: 0.0,
537            z: 0.0,
538        };
539    }
540
541    let area = get_shape_projected_area(shape, direction);
542    let mut scale = 1.0;
543    if output.distance > radius && falloff > 0.0 {
544        scale = clamp_float((radius + falloff - output.distance) / falloff, 0.0, 1.0);
545    }
546
547    let magnitude = impulse_per_area * area * scale * shape.explosion_scale;
548    let impulse = mul_sv(magnitude, rotate_vector(xf.q, direction));
549    let closest_for_torque = closest_point;
550    let direction_q = xf.q;
551
552    wake_body(world, body_id);
553
554    let body = &world.bodies[body_id as usize];
555    if body.set_index != AWAKE_SET {
556        return;
557    }
558
559    let local_index = body.local_index;
560    let set = &mut world.solver_sets[AWAKE_SET as usize];
561    // Copy inv_mass / local_center / inv_inertia before mutably borrowing body_states.
562    let (inv_mass, local_center, inv_inertia_world) = {
563        let body_sim = &set.body_sims[local_index as usize];
564        (
565            body_sim.inv_mass,
566            body_sim.local_center,
567            body_sim.inv_inertia_world,
568        )
569    };
570    let state = &mut set.body_states[local_index as usize];
571    state.linear_velocity = mul_add(state.linear_velocity, inv_mass, impulse);
572
573    // Lever arm from the center of mass to the closest point, rotated to world
574    let r = rotate_vector(direction_q, sub(closest_for_torque, local_center));
575    state.angular_velocity = add(
576        state.angular_velocity,
577        mul_mv(inv_inertia_world, cross(r, impulse)),
578    );
579}
580
581/// Union of broad-phase tree root bounds. (b3World_GetBounds)
582pub fn world_get_bounds(world: &World) -> Aabb {
583    debug_assert!(!world.locked);
584    if world.locked {
585        return Aabb {
586            lower_bound: VEC3_ZERO,
587            upper_bound: VEC3_ZERO,
588        };
589    }
590
591    let mut world_bounds = Aabb {
592        lower_bound: VEC3_ZERO,
593        upper_bound: VEC3_ZERO,
594    };
595    let mut have_bounds = false;
596
597    for i in 0..BODY_TYPE_COUNT {
598        let tree = &world.broad_phase.trees[i];
599        if tree.proxy_count() == 0 {
600            continue;
601        }
602
603        let bounds = tree.root_bounds();
604        if have_bounds {
605            world_bounds = aabb_union(world_bounds, bounds);
606        } else {
607            world_bounds = bounds;
608            have_bounds = true;
609        }
610    }
611
612    world_bounds
613}
614
615/// Set the stored worker count. Clamped to `[1, MAX_WORKERS]`. The serial port
616/// does not spawn threads but still resizes task contexts like C.
617/// (b3World_SetWorkerCount)
618pub fn world_set_worker_count(world: &mut World, count: i32) {
619    debug_assert!(!world.locked);
620    if world.locked {
621        return;
622    }
623
624    if count == world.worker_count {
625        return;
626    }
627
628    world.worker_count = clamp_int(count, 1, MAX_WORKERS);
629    // Serial equivalent of b3DestroyWorkerContexts + b3CreateWorkerContexts:
630    // resize and zero the per-worker scratch arrays.
631    world.task_contexts = vec![TaskContext::default(); world.worker_count as usize];
632    world.sensor_task_contexts = vec![SensorTaskContext::default(); world.worker_count as usize];
633}
634
635/// Get the stored worker count. (b3World_GetWorkerCount)
636pub fn world_get_worker_count(world: &World) -> i32 {
637    debug_assert!(!world.locked);
638    if world.locked {
639        return 0;
640    }
641    world.worker_count
642}
643
644/// Begin capturing mutations into `recording`. (b3World_StartRecording)
645pub fn world_start_recording(world: &mut World, recording: &mut crate::recording::Recording) {
646    debug_assert!(world.recording.is_none());
647    if world.recording.is_some() {
648        return;
649    }
650    crate::recording::start_recording(world, recording);
651}
652
653/// Finalize the active recording. (b3World_StopRecording)
654pub fn world_stop_recording(world: &mut World) {
655    crate::recording::stop_recording(world);
656}