Skip to main content

box3d_rust/world/
query.rs

1//! World overlap and cast queries from physics_world.c (with recording capture).
2//!
3//! C passes function pointers plus a void* context; the Rust port passes
4//! closures, matching the dynamic-tree callback style used across the crate.
5//!
6//! SPDX-FileCopyrightText: 2025 Erin Catto
7//! SPDX-License-Identifier: MIT
8
9use super::World;
10use crate::body::get_body_transform_quick;
11use crate::distance::{make_proxy, ShapeProxy};
12use crate::dynamic_tree::{BoxCastInput, TreeStats};
13use crate::geometry::{Capsule, PlaneResult, RayCastInput, ShapeCastInput};
14use crate::id::ShapeId;
15use crate::math_functions::{
16    add, clamp_int, is_valid_aabb, is_valid_position, is_valid_vec3, make_aabb, max, min,
17    offset_aabb, offset_pos, sub, to_relative_transform, to_vec3, Aabb, Pos, Vec3, VEC3_ZERO,
18};
19use crate::shape::{
20    collide_mover, overlap_shape, ray_cast_shape, shape_cast_shape, should_query_collide, Shape,
21};
22use crate::types::{QueryFilter, RayResult, BODY_TYPE_COUNT};
23
24/// Make a user-facing shape id for a shape. Queries hand these to callbacks.
25fn query_shape_id(world: &World, shape: &Shape) -> ShapeId {
26    ShapeId {
27        index1: shape.id + 1,
28        world0: world.world_id,
29        generation: shape.generation,
30    }
31}
32
33/// Overlap test for all shapes that potentially overlap the provided AABB.
34/// The callback receives each overlapping shape id and returns false to
35/// terminate the query. (b3World_OverlapAABB + static TreeQueryCallback)
36pub fn world_overlap_aabb(
37    world: &World,
38    aabb: Aabb,
39    filter: &QueryFilter,
40    mut fcn: impl FnMut(ShapeId) -> bool,
41) -> TreeStats {
42    let mut tree_stats = TreeStats::default();
43
44    debug_assert!(!world.locked);
45    if world.locked {
46        return tree_stats;
47    }
48
49    debug_assert!(is_valid_aabb(aabb));
50
51    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
52    if let Some(w) = rec.as_mut() {
53        w.write_overlap_aabb_header(
54            crate::recording::query_capture::world_id(world),
55            aabb,
56            filter,
57        );
58    }
59
60    for i in 0..BODY_TYPE_COUNT {
61        let tree_result =
62            world.broad_phase.trees[i].query(aabb, filter.mask_bits, false, |_, user_data| {
63                let shape_id = user_data as i32;
64                let shape = &world.shapes[shape_id as usize];
65
66                if !should_query_collide(&shape.filter, filter) {
67                    return true;
68                }
69
70                let id = ShapeId {
71                    index1: shape_id + 1,
72                    world0: world.world_id,
73                    generation: shape.generation,
74                };
75                let ret = fcn(id);
76                if let Some(w) = rec.as_mut() {
77                    w.append_overlap_hit(id, ret);
78                }
79                ret
80            });
81
82        tree_stats.node_visits += tree_result.node_visits;
83        tree_stats.leaf_visits += tree_result.leaf_visits;
84    }
85
86    if let Some(mut w) = rec {
87        w.finish_counted();
88        w.append_tree_stats(tree_stats);
89        crate::recording::query_capture::commit(
90            world,
91            w,
92            crate::recording::ops::RecOp::QueryOverlapAABB,
93        );
94    }
95
96    tree_stats
97}
98
99/// Overlap test for all shapes that overlap the provided shape proxy.
100/// (b3World_OverlapShape + static TreeOverlapCallback)
101pub fn world_overlap_shape(
102    world: &World,
103    origin: Pos,
104    proxy: &ShapeProxy,
105    filter: &QueryFilter,
106    mut fcn: impl FnMut(ShapeId) -> bool,
107) -> TreeStats {
108    let mut tree_stats = TreeStats::default();
109
110    debug_assert!(!world.locked);
111    if world.locked {
112        return tree_stats;
113    }
114
115    debug_assert!(is_valid_position(origin));
116
117    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
118    if let Some(w) = rec.as_mut() {
119        w.write_overlap_shape_header(
120            crate::recording::query_capture::world_id(world),
121            origin,
122            proxy,
123            filter,
124        );
125    }
126
127    // Bound the proxy in origin relative space then lift to a conservative world float box
128    let aabb = offset_aabb(
129        make_aabb(&proxy.points[..proxy.count as usize], proxy.radius),
130        origin,
131    );
132
133    for i in 0..BODY_TYPE_COUNT {
134        let tree_result =
135            world.broad_phase.trees[i].query(aabb, filter.mask_bits, false, |_, user_data| {
136                let shape_id = user_data as i32;
137                let shape = &world.shapes[shape_id as usize];
138
139                if !should_query_collide(&shape.filter, filter) {
140                    return true;
141                }
142
143                // Re-center on the query origin so the overlap test stays in float precision
144                let body = &world.bodies[shape.body_id as usize];
145                let transform =
146                    to_relative_transform(get_body_transform_quick(world, body), origin);
147
148                if !overlap_shape(shape, transform, proxy) {
149                    return true;
150                }
151
152                let id = query_shape_id(world, shape);
153                let ret = fcn(id);
154                if let Some(w) = rec.as_mut() {
155                    w.append_overlap_hit(id, ret);
156                }
157                ret
158            });
159
160        tree_stats.node_visits += tree_result.node_visits;
161        tree_stats.leaf_visits += tree_result.leaf_visits;
162    }
163
164    if let Some(mut w) = rec {
165        w.finish_counted();
166        w.append_tree_stats(tree_stats);
167        crate::recording::query_capture::commit(
168            world,
169            w,
170            crate::recording::ops::RecOp::QueryOverlapShape,
171        );
172    }
173
174    tree_stats
175}
176
177/// Cast a ray into the world. The callback receives
178/// `(shape_id, point, normal, fraction, user_material_id, triangle_index, child_index)`
179/// and controls continuation like C's b3CastResultFcn: return -1 to ignore,
180/// 0 to terminate, a fraction in 0..=1 to clip, or >1 to continue without clipping.
181/// (b3World_CastRay + static RayCastCallback)
182pub fn world_cast_ray(
183    world: &World,
184    origin: Pos,
185    translation: Vec3,
186    filter: &QueryFilter,
187    mut fcn: impl FnMut(ShapeId, Pos, Vec3, f32, u64, i32, i32) -> f32,
188) -> TreeStats {
189    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
190    if let Some(w) = rec.as_mut() {
191        w.write_cast_ray_header(
192            crate::recording::query_capture::world_id(world),
193            origin,
194            translation,
195            filter,
196        );
197    }
198
199    let tree_stats = cast_ray_impl(
200        world,
201        origin,
202        translation,
203        filter,
204        |id, point, normal, fraction, mid, tri, child| {
205            let user_fraction = fcn(id, point, normal, fraction, mid, tri, child);
206            if let Some(w) = rec.as_mut() {
207                w.append_cast_hit(id, point, normal, fraction, mid, tri, child, user_fraction);
208            }
209            user_fraction
210        },
211    );
212
213    if let Some(mut w) = rec {
214        w.finish_counted();
215        w.append_tree_stats(tree_stats);
216        crate::recording::query_capture::commit(
217            world,
218            w,
219            crate::recording::ops::RecOp::QueryCastRay,
220        );
221    }
222
223    tree_stats
224}
225
226fn cast_ray_impl(
227    world: &World,
228    origin: Pos,
229    translation: Vec3,
230    filter: &QueryFilter,
231    mut fcn: impl FnMut(ShapeId, Pos, Vec3, f32, u64, i32, i32) -> f32,
232) -> TreeStats {
233    let mut tree_stats = TreeStats::default();
234
235    debug_assert!(!world.locked);
236    if world.locked {
237        return tree_stats;
238    }
239
240    debug_assert!(is_valid_position(origin));
241    debug_assert!(is_valid_vec3(translation));
242
243    let mut input = RayCastInput {
244        origin: to_vec3(origin),
245        translation,
246        max_fraction: 1.0,
247    };
248
249    let mut fraction = 1.0f32;
250
251    for i in 0..BODY_TYPE_COUNT {
252        let tree_result = world.broad_phase.trees[i].ray_cast(
253            &input,
254            filter.mask_bits,
255            false,
256            |ray_input, _, user_data| {
257                let shape_id = user_data as i32;
258                let shape = &world.shapes[shape_id as usize];
259
260                if !should_query_collide(&shape.filter, filter) {
261                    return ray_input.max_fraction;
262                }
263
264                let body = &world.bodies[shape.body_id as usize];
265                let body_transform = get_body_transform_quick(world, body);
266                let transform = to_relative_transform(body_transform, origin);
267
268                let local_input = RayCastInput {
269                    origin: VEC3_ZERO,
270                    translation: ray_input.translation,
271                    max_fraction: ray_input.max_fraction,
272                };
273                let output = ray_cast_shape(shape, transform, &local_input);
274
275                if output.hit {
276                    debug_assert!(output.fraction <= ray_input.max_fraction);
277
278                    let id = ShapeId {
279                        index1: shape_id + 1,
280                        world0: world.world_id,
281                        generation: shape.generation,
282                    };
283                    let point = offset_pos(origin, output.point);
284                    let material_index =
285                        clamp_int(output.material_index, 0, shape.material_count() - 1);
286                    let user_material_id =
287                        shape.shape_materials()[material_index as usize].user_material_id;
288
289                    let user_fraction = fcn(
290                        id,
291                        point,
292                        output.normal,
293                        output.fraction,
294                        user_material_id,
295                        output.triangle_index,
296                        output.child_index,
297                    );
298
299                    // The user may return -1 to skip this shape
300                    if (0.0..=1.0).contains(&user_fraction) {
301                        fraction = user_fraction;
302                    }
303
304                    return user_fraction;
305                }
306
307                ray_input.max_fraction
308            },
309        );
310
311        tree_stats.node_visits += tree_result.node_visits;
312        tree_stats.leaf_visits += tree_result.leaf_visits;
313
314        if fraction == 0.0 {
315            break;
316        }
317
318        input.max_fraction = fraction;
319    }
320
321    tree_stats
322}
323
324/// Cast a ray into the world to collect the closest hit. Ignores initial
325/// overlap (fraction == 0 returns -1). (b3World_CastRayClosest)
326pub fn world_cast_ray_closest(
327    world: &World,
328    origin: Pos,
329    translation: Vec3,
330    filter: &QueryFilter,
331) -> RayResult {
332    let mut result = RayResult::default();
333
334    debug_assert!(!world.locked);
335    if world.locked {
336        return result;
337    }
338
339    debug_assert!(is_valid_position(origin));
340    debug_assert!(is_valid_vec3(translation));
341
342    // Use the unrecorded path so CastRayClosest records a single RayResult, matching C.
343    let stats = cast_ray_impl(
344        world,
345        origin,
346        translation,
347        filter,
348        |id, point, normal, fraction, user_material_id, triangle_index, child_index| {
349            // Ignore initial overlap
350            if fraction == 0.0 {
351                return -1.0;
352            }
353
354            result.shape_id = id;
355            result.point = point;
356            result.normal = normal;
357            result.fraction = fraction;
358            result.user_material_id = user_material_id;
359            result.triangle_index = triangle_index;
360            result.child_index = child_index;
361            result.hit = true;
362            fraction
363        },
364    );
365
366    result.node_visits = stats.node_visits;
367    result.leaf_visits = stats.leaf_visits;
368
369    if let Some(mut w) = crate::recording::query_capture::maybe_begin(world, filter) {
370        w.write_cast_ray_closest_header(
371            crate::recording::query_capture::world_id(world),
372            origin,
373            translation,
374            filter,
375        );
376        w.append_ray_result(&result);
377        crate::recording::query_capture::commit(
378            world,
379            w,
380            crate::recording::ops::RecOp::QueryCastRayClosest,
381        );
382    }
383
384    result
385}
386
387/// Cast a shape through the world. Callback contract matches [`world_cast_ray`].
388/// (b3World_CastShape + static ShapeCastCallback)
389pub fn world_cast_shape(
390    world: &World,
391    origin: Pos,
392    proxy: &ShapeProxy,
393    translation: Vec3,
394    filter: &QueryFilter,
395    mut fcn: impl FnMut(ShapeId, Pos, Vec3, f32, u64, i32, i32) -> f32,
396) -> TreeStats {
397    let mut tree_stats = TreeStats::default();
398
399    debug_assert!(!world.locked);
400    if world.locked {
401        return tree_stats;
402    }
403
404    debug_assert!(is_valid_position(origin));
405    debug_assert!(is_valid_vec3(translation));
406
407    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
408    if let Some(w) = rec.as_mut() {
409        w.write_cast_shape_header(
410            crate::recording::query_capture::world_id(world),
411            origin,
412            proxy,
413            translation,
414            filter,
415        );
416    }
417
418    let cast_input = ShapeCastInput {
419        proxy: *proxy,
420        translation,
421        max_fraction: 1.0,
422        can_encroach: false,
423    };
424
425    let mut fraction = 1.0f32;
426
427    // Bound the proxy in origin relative space then lift to a conservative world float box
428    let local_box = make_aabb(&proxy.points[..proxy.count as usize], proxy.radius);
429    let mut tree_input = BoxCastInput {
430        box_: offset_aabb(local_box, origin),
431        translation,
432        max_fraction: 1.0,
433    };
434
435    for i in 0..BODY_TYPE_COUNT {
436        let tree_result = world.broad_phase.trees[i].box_cast(
437            &tree_input,
438            filter.mask_bits,
439            false,
440            |box_input, _, user_data| {
441                let shape_id = user_data as i32;
442                let shape = &world.shapes[shape_id as usize];
443
444                if !should_query_collide(&shape.filter, filter) {
445                    return box_input.max_fraction;
446                }
447
448                // Rebuild from the origin relative input, taking only the advancing
449                // fraction from the tree.
450                let mut local_input = cast_input;
451                local_input.max_fraction = box_input.max_fraction;
452
453                let body = &world.bodies[shape.body_id as usize];
454                let transform =
455                    to_relative_transform(get_body_transform_quick(world, body), origin);
456
457                let output = shape_cast_shape(shape, transform, &local_input);
458
459                if output.hit {
460                    let id = ShapeId {
461                        index1: shape_id + 1,
462                        world0: world.world_id,
463                        generation: shape.generation,
464                    };
465                    let material_index =
466                        clamp_int(output.material_index, 0, shape.material_count() - 1);
467                    let user_material_id =
468                        shape.shape_materials()[material_index as usize].user_material_id;
469
470                    let point = offset_pos(origin, output.point);
471                    let user_fraction = fcn(
472                        id,
473                        point,
474                        output.normal,
475                        output.fraction,
476                        user_material_id,
477                        output.triangle_index,
478                        output.child_index,
479                    );
480
481                    if let Some(w) = rec.as_mut() {
482                        w.append_cast_hit(
483                            id,
484                            point,
485                            output.normal,
486                            output.fraction,
487                            user_material_id,
488                            output.triangle_index,
489                            output.child_index,
490                            user_fraction,
491                        );
492                    }
493
494                    if (0.0..=1.0).contains(&user_fraction) {
495                        fraction = user_fraction;
496                    }
497
498                    return user_fraction;
499                }
500
501                box_input.max_fraction
502            },
503        );
504
505        tree_stats.node_visits += tree_result.node_visits;
506        tree_stats.leaf_visits += tree_result.leaf_visits;
507
508        if fraction == 0.0 {
509            break;
510        }
511
512        tree_input.max_fraction = fraction;
513    }
514
515    if let Some(mut w) = rec {
516        w.finish_counted();
517        w.append_tree_stats(tree_stats);
518        crate::recording::query_capture::commit(
519            world,
520            w,
521            crate::recording::ops::RecOp::QueryCastShape,
522        );
523    }
524
525    tree_stats
526}
527
528/// Collide a capsule mover with the world, gathering collision planes via callback.
529/// The callback receives `(shape_id, planes)` and returns `false` to stop.
530/// (b3World_CollideMover + static TreeCollideCallback)
531pub fn world_collide_mover(
532    world: &World,
533    origin: Pos,
534    mover: &Capsule,
535    filter: &QueryFilter,
536    mut fcn: impl FnMut(ShapeId, &[PlaneResult]) -> bool,
537) {
538    debug_assert!(!world.locked);
539    if world.locked {
540        return;
541    }
542
543    debug_assert!(is_valid_position(origin));
544
545    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
546    if let Some(w) = rec.as_mut() {
547        w.write_collide_mover_header(
548            crate::recording::query_capture::world_id(world),
549            origin,
550            *mover,
551            filter,
552        );
553    }
554
555    let r = Vec3 {
556        x: mover.radius,
557        y: mover.radius,
558        z: mover.radius,
559    };
560
561    // Relative box lifted to world float with outward rounding, conservative for the tree
562    let rel_box = Aabb {
563        lower_bound: sub(min(mover.center1, mover.center2), r),
564        upper_bound: add(max(mover.center1, mover.center2), r),
565    };
566    let aabb = offset_aabb(rel_box, origin);
567
568    for i in 0..BODY_TYPE_COUNT {
569        world.broad_phase.trees[i].query(aabb, filter.mask_bits, false, |_, user_data| {
570            let shape_id = user_data as i32;
571            let shape = &world.shapes[shape_id as usize];
572
573            if !should_query_collide(&shape.filter, filter) {
574                return true;
575            }
576
577            // Re-center on the query origin, the mover and the resulting planes are origin relative
578            let body = &world.bodies[shape.body_id as usize];
579            let transform = to_relative_transform(get_body_transform_quick(world, body), origin);
580
581            let mut buffer = [PlaneResult::default(); 64];
582            let count = collide_mover(&mut buffer, shape, transform, mover);
583
584            if count > 0 {
585                let id = query_shape_id(world, shape);
586                let planes = &buffer[..count as usize];
587                let ret = fcn(id, planes);
588                if let Some(w) = rec.as_mut() {
589                    w.append_plane_hit(id, planes, ret);
590                }
591                return ret;
592            }
593
594            true
595        });
596    }
597
598    if let Some(mut w) = rec {
599        w.finish_counted();
600        crate::recording::query_capture::commit(
601            world,
602            w,
603            crate::recording::ops::RecOp::QueryCollideMover,
604        );
605    }
606}
607
608/// Cast a capsule mover through the world. Returns the earliest hit fraction in
609/// `[0, 1]`. Overlapping shapes (fraction == 0) are ignored. An optional filter
610/// callback can reject individual shapes. (b3World_CastMover + static MoverCastCallback)
611pub fn world_cast_mover(
612    world: &World,
613    origin: Pos,
614    mover: &Capsule,
615    translation: Vec3,
616    filter: &QueryFilter,
617    mut filter_fcn: Option<&mut dyn FnMut(ShapeId) -> bool>,
618) -> f32 {
619    debug_assert!(is_valid_position(origin));
620    debug_assert!(is_valid_vec3(translation));
621
622    debug_assert!(!world.locked);
623    if world.locked {
624        return 1.0;
625    }
626
627    let mut rec = crate::recording::query_capture::maybe_begin(world, filter);
628    if let Some(w) = rec.as_mut() {
629        w.write_cast_mover_header(
630            crate::recording::query_capture::world_id(world),
631            origin,
632            *mover,
633            translation,
634            filter,
635        );
636    }
637
638    let cast_input = ShapeCastInput {
639        proxy: make_proxy(&[mover.center1, mover.center2], mover.radius),
640        translation,
641        max_fraction: 1.0,
642        can_encroach: mover.radius > 0.0,
643    };
644
645    let mut fraction = 1.0f32;
646
647    // Bound the capsule in origin relative space then lift to a conservative world float box
648    let centers = [mover.center1, mover.center2];
649    let mut tree_input = BoxCastInput {
650        box_: offset_aabb(make_aabb(&centers, mover.radius), origin),
651        translation,
652        max_fraction: 1.0,
653    };
654
655    for i in 0..BODY_TYPE_COUNT {
656        world.broad_phase.trees[i].box_cast(
657            &tree_input,
658            filter.mask_bits,
659            false,
660            |box_input, _, user_data| {
661                let shape_id = user_data as i32;
662                let shape = &world.shapes[shape_id as usize];
663
664                if !should_query_collide(&shape.filter, filter) {
665                    return fraction;
666                }
667
668                let id = ShapeId {
669                    index1: shape_id + 1,
670                    world0: world.world_id,
671                    generation: shape.generation,
672                };
673
674                // When recording, always record the filter decision (accept-all if no user filter),
675                // matching C's overlap trampoline installed even for NULL filters.
676                let should_collide = if let Some(ref mut fcn) = filter_fcn {
677                    fcn(id)
678                } else {
679                    true
680                };
681                if rec.is_some() || filter_fcn.is_some() {
682                    if let Some(w) = rec.as_mut() {
683                        w.append_overlap_hit(id, should_collide);
684                    }
685                    if !should_collide {
686                        return fraction;
687                    }
688                }
689
690                // Rebuild from the origin relative input, taking only the advancing fraction from the tree
691                let mut local_input = cast_input;
692                local_input.max_fraction = box_input.max_fraction;
693
694                // Re-center on the query origin so the per-shape cast stays in float precision far from the origin
695                let body = &world.bodies[shape.body_id as usize];
696                let transform =
697                    to_relative_transform(get_body_transform_quick(world, body), origin);
698
699                let output = shape_cast_shape(shape, transform, &local_input);
700                if output.fraction == 0.0 {
701                    // Ignore overlapping shapes
702                    return fraction;
703                }
704
705                fraction = output.fraction;
706                output.fraction
707            },
708        );
709
710        if fraction == 0.0 {
711            break;
712        }
713
714        tree_input.max_fraction = fraction;
715    }
716
717    if let Some(mut w) = rec {
718        w.finish_counted();
719        w.append_f32(fraction);
720        crate::recording::query_capture::commit(
721            world,
722            w,
723            crate::recording::ops::RecOp::QueryCastMover,
724        );
725    }
726
727    fraction
728}