Skip to main content

box2d_rust/world/
query.rs

1// World queries from physics_world.c: overlap tests, ray casts, shape casts,
2// and the mover (character controller) queries.
3//
4// C passes function pointers plus a void* context; the Rust port passes
5// closures, matching the dynamic-tree callback style used across the crate.
6// The tree traversal callbacks (TreeQueryCallback, RayCastCallback, ...) are
7// inlined as closures because their only job in C is to unpack the context
8// struct.
9//
10// Queries take &mut World like C takes a non-const world: an active
11// recording session captures every query with its hits (the C trampolines
12// become a tee inside the callback). The traversal itself only reads, via a
13// scoped immutable reborrow.
14//
15// SPDX-FileCopyrightText: 2023 Erin Catto
16// SPDX-License-Identifier: MIT
17
18use super::World;
19use crate::aabb::offset_aabb;
20use crate::body::get_body_transform_quick;
21use crate::collision::PlaneResult;
22use crate::collision::{Capsule, CastOutput, RayCastInput, ShapeCastInput};
23use crate::constants::linear_slop;
24use crate::distance::{shape_distance, DistanceInput, ShapeProxy, SimplexCache};
25use crate::dynamic_tree::{BoxCastInput, TreeStats};
26use crate::id::ShapeId;
27use crate::math_functions::{
28    is_normalized, is_valid_aabb, is_valid_position, is_valid_vec2, make_aabb, offset_pos, sub_pos,
29    to_relative_transform, to_vec2, Aabb, Pos, Vec2,
30};
31use crate::recording::{
32    rec_w_aabb, rec_w_bool, rec_w_capsule, rec_w_f32, rec_w_planeresult, rec_w_position,
33    rec_w_queryfilter, rec_w_rayresult, rec_w_shapeid, rec_w_shapeproxy, rec_w_vec2,
34    record_query_result, QueryRecorder, OP_QUERY_CAST_MOVER, OP_QUERY_CAST_RAY,
35    OP_QUERY_CAST_RAY_CLOSEST, OP_QUERY_CAST_SHAPE, OP_QUERY_COLLIDE_MOVER, OP_QUERY_OVERLAP_AABB,
36    OP_QUERY_OVERLAP_SHAPE,
37};
38use crate::shape::{
39    collide_mover, make_shape_distance_proxy, ray_cast_shape, shape_cast_shape,
40    should_query_collide, Shape,
41};
42use crate::types::{QueryFilter, BODY_TYPE_COUNT};
43
44/// Make a user-facing shape id for a shape. Queries hand these to callbacks.
45fn query_shape_id(world: &World, shape: &Shape) -> ShapeId {
46    ShapeId {
47        index1: shape.id + 1,
48        world0: world.world_id,
49        generation: shape.generation,
50    }
51}
52
53/// Overlap test for all shapes that potentially overlap the provided AABB.
54/// The callback receives each overlapping shape id and returns false to
55/// terminate the query. (b2World_OverlapAABB + static TreeQueryCallback)
56pub fn world_overlap_aabb(
57    world: &mut World,
58    origin: Pos,
59    aabb: Aabb,
60    filter: QueryFilter,
61    mut fcn: impl FnMut(ShapeId) -> bool,
62) -> TreeStats {
63    let mut tree_stats = TreeStats::default();
64
65    debug_assert!(!world.locked);
66    if world.locked {
67        return tree_stats;
68    }
69
70    debug_assert!(is_valid_position(origin));
71    debug_assert!(is_valid_aabb(aabb));
72
73    let mut q = QueryRecorder::begin(world, |buf| {
74        rec_w_position(buf, origin);
75        rec_w_aabb(buf, aabb);
76        rec_w_queryfilter(buf, filter);
77    });
78
79    {
80        let world: &World = world;
81
82        // Lift to a world float box with outward rounding so the conservative
83        // tree test never misses
84        let world_box = offset_aabb(aabb, origin);
85
86        for i in 0..BODY_TYPE_COUNT {
87            let tree_result =
88                world.broad_phase.trees[i].query(world_box, filter.mask_bits, |_, user_data| {
89                    // (static TreeQueryCallback)
90                    let shape_id = user_data as i32;
91                    let shape = &world.shapes[shape_id as usize];
92
93                    if !should_query_collide(shape.filter, filter) {
94                        return true;
95                    }
96
97                    let id = ShapeId {
98                        index1: shape_id + 1,
99                        world0: world.world_id,
100                        generation: shape.generation,
101                    };
102                    let ret = fcn(id);
103                    if q.active() {
104                        // (b2RecOverlapTrampoline)
105                        rec_w_shapeid(&mut q.buf, id);
106                        rec_w_bool(&mut q.buf, ret);
107                        q.hits += 1;
108                    }
109                    ret
110                });
111
112            tree_stats.node_visits += tree_result.node_visits;
113            tree_stats.leaf_visits += tree_result.leaf_visits;
114        }
115    }
116
117    q.commit(world, OP_QUERY_OVERLAP_AABB, Some(tree_stats));
118    tree_stats
119}
120
121/// Overlap test for all shapes that overlap the provided shape proxy. The
122/// callback returns false to terminate the query.
123/// (b2World_OverlapShape + static TreeOverlapCallback)
124pub fn world_overlap_shape(
125    world: &mut World,
126    origin: Pos,
127    proxy: &ShapeProxy,
128    filter: QueryFilter,
129    mut fcn: impl FnMut(ShapeId) -> bool,
130) -> TreeStats {
131    let mut tree_stats = TreeStats::default();
132
133    debug_assert!(!world.locked);
134    if world.locked {
135        return tree_stats;
136    }
137
138    debug_assert!(is_valid_position(origin));
139
140    let mut q = QueryRecorder::begin(world, |buf| {
141        rec_w_position(buf, origin);
142        rec_w_shapeproxy(buf, proxy);
143        rec_w_queryfilter(buf, filter);
144    });
145
146    {
147        let world: &World = world;
148
149        // Relative box lifted to world float with outward rounding,
150        // conservative for the tree
151        let aabb = offset_aabb(
152            make_aabb(&proxy.points[..proxy.count as usize], proxy.radius),
153            origin,
154        );
155
156        for i in 0..BODY_TYPE_COUNT {
157            let tree_result =
158                world.broad_phase.trees[i].query(aabb, filter.mask_bits, |_, user_data| {
159                    // (static TreeOverlapCallback)
160                    let shape_id = user_data as i32;
161                    let shape = &world.shapes[shape_id as usize];
162
163                    if !should_query_collide(shape.filter, filter) {
164                        return true;
165                    }
166
167                    // Re-center on the query origin so the distance test
168                    // stays in float precision far from the world origin
169                    let body = &world.bodies[shape.body_id as usize];
170                    let transform =
171                        to_relative_transform(get_body_transform_quick(world, body), origin);
172
173                    let input = DistanceInput {
174                        proxy_a: *proxy,
175                        proxy_b: make_shape_distance_proxy(shape),
176                        transform,
177                        use_radii: true,
178                    };
179
180                    let mut cache = SimplexCache::default();
181                    let output = shape_distance(&input, &mut cache, None);
182
183                    let tolerance = 0.1 * linear_slop();
184                    if output.distance > tolerance {
185                        return true;
186                    }
187
188                    let id = query_shape_id(world, shape);
189                    let ret = fcn(id);
190                    if q.active() {
191                        rec_w_shapeid(&mut q.buf, id);
192                        rec_w_bool(&mut q.buf, ret);
193                        q.hits += 1;
194                    }
195                    ret
196                });
197
198            tree_stats.node_visits += tree_result.node_visits;
199            tree_stats.leaf_visits += tree_result.leaf_visits;
200        }
201    }
202
203    q.commit(world, OP_QUERY_OVERLAP_SHAPE, Some(tree_stats));
204    tree_stats
205}
206
207/// The shared per-tree ray cast loop. CastRay records around it; the closest
208/// variant runs it directly and records a self-contained result instead,
209/// matching C where the two entry points duplicate this loop.
210fn cast_ray_impl(
211    world: &World,
212    origin: Pos,
213    translation: Vec2,
214    filter: QueryFilter,
215    mut fcn: impl FnMut(ShapeId, Pos, Vec2, f32) -> f32,
216) -> TreeStats {
217    let mut tree_stats = TreeStats::default();
218
219    // Tree traversal sees the origin truncated to float, displacing the ray
220    // by up to one coordinate ULP, a graze sized miss tolerance at extreme
221    // range. Per-shape casts re-difference against the full precision origin.
222    let mut input = RayCastInput {
223        origin: to_vec2(origin),
224        translation,
225        max_fraction: 1.0,
226    };
227
228    let mut fraction = 1.0f32;
229
230    for i in 0..BODY_TYPE_COUNT {
231        let tree_result = world.broad_phase.trees[i].ray_cast(
232            &input,
233            filter.mask_bits,
234            |tree_input, _, user_data| {
235                // (static RayCastCallback)
236                let shape_id = user_data as i32;
237                let shape = &world.shapes[shape_id as usize];
238
239                if !should_query_collide(shape.filter, filter) {
240                    return tree_input.max_fraction;
241                }
242
243                let body = &world.bodies[shape.body_id as usize];
244                let xf = get_body_transform_quick(world, body);
245
246                // Re-center on the body so the per-shape cast stays in float
247                // precision far from the origin. The tree traversal already
248                // used the truncated origin in input. Here we re-difference in
249                // full precision against the body position.
250                let base = xf.p;
251                let transform = to_relative_transform(xf, base);
252                let mut local_input = *tree_input;
253                local_input.origin = sub_pos(origin, base);
254                let output: CastOutput = ray_cast_shape(&local_input, shape, transform);
255
256                if output.hit {
257                    let id = query_shape_id(world, shape);
258                    let point = offset_pos(base, output.point);
259                    let user_fraction = fcn(id, point, output.normal, output.fraction);
260
261                    // The user may return -1 to skip this shape
262                    if (0.0..=1.0).contains(&user_fraction) {
263                        fraction = user_fraction;
264                    }
265
266                    return user_fraction;
267                }
268
269                tree_input.max_fraction
270            },
271        );
272        tree_stats.node_visits += tree_result.node_visits;
273        tree_stats.leaf_visits += tree_result.leaf_visits;
274
275        if fraction == 0.0 {
276            break;
277        }
278
279        input.max_fraction = fraction;
280    }
281
282    tree_stats
283}
284
285/// Cast a ray into the world to collect shapes in the path of the ray. The
286/// callback receives `(shape_id, point, normal, fraction)` and controls the
287/// continuation like C's b2CastResultFcn: return -1 to ignore the hit, 0 to
288/// terminate, a fraction to clip the ray, or 1 to continue without clipping.
289/// (b2World_CastRay + static RayCastCallback)
290pub fn world_cast_ray(
291    world: &mut World,
292    origin: Pos,
293    translation: Vec2,
294    filter: QueryFilter,
295    mut fcn: impl FnMut(ShapeId, Pos, Vec2, f32) -> f32,
296) -> TreeStats {
297    debug_assert!(!world.locked);
298    if world.locked {
299        return TreeStats::default();
300    }
301
302    debug_assert!(is_valid_position(origin));
303    debug_assert!(is_valid_vec2(translation));
304
305    let mut q = QueryRecorder::begin(world, |buf| {
306        rec_w_position(buf, origin);
307        rec_w_vec2(buf, translation);
308        rec_w_queryfilter(buf, filter);
309    });
310
311    let tree_stats = cast_ray_impl(
312        world,
313        origin,
314        translation,
315        filter,
316        |id, point, normal, fraction| {
317            let ret = fcn(id, point, normal, fraction);
318            if q.active() {
319                // (b2RecCastTrampoline)
320                rec_w_shapeid(&mut q.buf, id);
321                rec_w_position(&mut q.buf, point);
322                rec_w_vec2(&mut q.buf, normal);
323                rec_w_f32(&mut q.buf, fraction);
324                rec_w_f32(&mut q.buf, ret);
325                q.hits += 1;
326            }
327            ret
328        },
329    );
330
331    q.commit(world, OP_QUERY_CAST_RAY, Some(tree_stats));
332    tree_stats
333}
334
335/// Cast a ray into the world to collect the closest hit. This is a
336/// convenience function. Ignores initial overlap.
337/// (b2World_CastRayClosest + static b2RayCastClosestFcn)
338pub fn world_cast_ray_closest(
339    world: &mut World,
340    origin: Pos,
341    translation: Vec2,
342    filter: QueryFilter,
343) -> crate::types::RayResult {
344    let mut result = crate::types::RayResult::default();
345
346    debug_assert!(!world.locked);
347    if world.locked {
348        return result;
349    }
350
351    debug_assert!(is_valid_position(origin));
352    debug_assert!(is_valid_vec2(translation));
353
354    // C runs its own loop with b2RayCastClosestFcn and records one
355    // self-contained result, never per-hit tails.
356    let stats = cast_ray_impl(
357        world,
358        origin,
359        translation,
360        filter,
361        |id, point, normal, fraction| {
362            // Ignore initial overlap
363            if fraction == 0.0 {
364                return -1.0;
365            }
366
367            result.shape_id = id;
368            result.point = point;
369            result.normal = normal;
370            result.fraction = fraction;
371            result.hit = true;
372            fraction
373        },
374    );
375
376    result.node_visits = stats.node_visits;
377    result.leaf_visits = stats.leaf_visits;
378
379    record_query_result(
380        world,
381        OP_QUERY_CAST_RAY_CLOSEST,
382        |buf| {
383            rec_w_position(buf, origin);
384            rec_w_vec2(buf, translation);
385            rec_w_queryfilter(buf, filter);
386        },
387        |buf| rec_w_rayresult(buf, &result),
388    );
389
390    result
391}
392
393/// Cast a shape through the world. Similar to a cast ray except that a shape
394/// is cast instead of a point. The callback contract matches
395/// [`world_cast_ray`]. (b2World_CastShape + static ShapeCastCallback)
396pub fn world_cast_shape(
397    world: &mut World,
398    origin: Pos,
399    proxy: &ShapeProxy,
400    translation: Vec2,
401    filter: QueryFilter,
402    mut fcn: impl FnMut(ShapeId, Pos, Vec2, f32) -> f32,
403) -> TreeStats {
404    let mut tree_stats = TreeStats::default();
405
406    debug_assert!(!world.locked);
407    if world.locked {
408        return tree_stats;
409    }
410
411    debug_assert!(is_valid_position(origin));
412    debug_assert!(is_valid_vec2(translation));
413
414    let mut q = QueryRecorder::begin(world, |buf| {
415        rec_w_position(buf, origin);
416        rec_w_shapeproxy(buf, proxy);
417        rec_w_vec2(buf, translation);
418        rec_w_queryfilter(buf, filter);
419    });
420
421    {
422        let world: &World = world;
423
424        // Origin relative input carried on the context in C
425        // (WorldShapeCastContext)
426        let cast_input = ShapeCastInput {
427            proxy: *proxy,
428            translation,
429            max_fraction: 1.0,
430            can_encroach: false,
431        };
432
433        let mut fraction = 1.0f32;
434
435        // Bound the proxy in origin relative space then lift to a
436        // conservative world float box. The tree node boxes use the same
437        // directed rounding, so the swept box never clips a shape far from
438        // the origin. Per shape casts re-difference at full precision against
439        // the carried origin.
440        let local_box = make_aabb(&proxy.points[..proxy.count as usize], proxy.radius);
441        let box_ = offset_aabb(local_box, origin);
442        let mut tree_input = BoxCastInput {
443            box_,
444            translation,
445            max_fraction: 1.0,
446        };
447
448        for i in 0..BODY_TYPE_COUNT {
449            let tree_result = world.broad_phase.trees[i].box_cast(
450                &tree_input,
451                filter.mask_bits,
452                |box_input, _, user_data| {
453                    // (static ShapeCastCallback)
454                    let shape_id = user_data as i32;
455                    let shape = &world.shapes[shape_id as usize];
456
457                    if !should_query_collide(shape.filter, filter) {
458                        return box_input.max_fraction;
459                    }
460
461                    // Rebuild from the origin relative input, taking only the
462                    // advancing fraction from the tree. The tree input is
463                    // world float and would lose the cast far from the origin.
464                    let mut local_input = cast_input;
465                    local_input.max_fraction = box_input.max_fraction;
466
467                    let body = &world.bodies[shape.body_id as usize];
468                    let transform = get_body_transform_quick(world, body);
469                    let local_transform = to_relative_transform(transform, origin);
470
471                    let output = shape_cast_shape(&local_input, shape, local_transform);
472
473                    if output.hit {
474                        let id = query_shape_id(world, shape);
475                        let point = offset_pos(origin, output.point);
476                        let user_fraction = fcn(id, point, output.normal, output.fraction);
477                        if q.active() {
478                            rec_w_shapeid(&mut q.buf, id);
479                            rec_w_position(&mut q.buf, point);
480                            rec_w_vec2(&mut q.buf, output.normal);
481                            rec_w_f32(&mut q.buf, output.fraction);
482                            rec_w_f32(&mut q.buf, user_fraction);
483                            q.hits += 1;
484                        }
485
486                        // The user may return -1 to skip this shape
487                        if (0.0..=1.0).contains(&user_fraction) {
488                            fraction = user_fraction;
489                        }
490
491                        return user_fraction;
492                    }
493
494                    box_input.max_fraction
495                },
496            );
497            tree_stats.node_visits += tree_result.node_visits;
498            tree_stats.leaf_visits += tree_result.leaf_visits;
499
500            if fraction == 0.0 {
501                break;
502            }
503
504            tree_input.max_fraction = fraction;
505        }
506    }
507
508    q.commit(world, OP_QUERY_CAST_SHAPE, Some(tree_stats));
509    tree_stats
510}
511
512/// Cast a capsule mover through the world. This is a special shape cast that
513/// handles sliding along other shapes while reducing clipping. Returns the
514/// fraction of the translation that can be performed without a hit.
515/// (b2World_CastMover + static MoverCastCallback)
516pub fn world_cast_mover(
517    world: &mut World,
518    origin: Pos,
519    mover: &Capsule,
520    translation: Vec2,
521    filter: QueryFilter,
522) -> f32 {
523    debug_assert!(is_valid_position(origin));
524    debug_assert!(is_valid_vec2(translation));
525    debug_assert!(mover.radius > 2.0 * linear_slop());
526
527    debug_assert!(!world.locked);
528    if world.locked {
529        return 1.0;
530    }
531
532    let mut fraction = 1.0f32;
533
534    {
535        let world: &World = world;
536
537        // Origin relative input carried on the context in C
538        // (WorldMoverCastContext)
539        let mut cast_input = ShapeCastInput::default();
540        cast_input.proxy.points[0] = mover.center1;
541        cast_input.proxy.points[1] = mover.center2;
542        cast_input.proxy.count = 2;
543        cast_input.proxy.radius = mover.radius;
544        cast_input.translation = translation;
545        cast_input.max_fraction = 1.0;
546        cast_input.can_encroach = true;
547
548        // Bound the capsule in origin relative space then lift to a
549        // conservative world float box
550        let centers = [mover.center1, mover.center2];
551        let box_ = offset_aabb(make_aabb(&centers, mover.radius), origin);
552        let mut tree_input = BoxCastInput {
553            box_,
554            translation,
555            max_fraction: 1.0,
556        };
557
558        for i in 0..BODY_TYPE_COUNT {
559            world.broad_phase.trees[i].box_cast(
560                &tree_input,
561                filter.mask_bits,
562                |box_input, _, user_data| {
563                    // (static MoverCastCallback)
564                    let shape_id = user_data as i32;
565                    let shape = &world.shapes[shape_id as usize];
566
567                    if !should_query_collide(shape.filter, filter) {
568                        return fraction;
569                    }
570
571                    // Rebuild from the origin relative input, taking only the
572                    // advancing fraction from the tree
573                    let mut local_input = cast_input;
574                    local_input.max_fraction = box_input.max_fraction;
575
576                    let body = &world.bodies[shape.body_id as usize];
577                    let transform =
578                        to_relative_transform(get_body_transform_quick(world, body), origin);
579
580                    let output = shape_cast_shape(&local_input, shape, transform);
581                    if output.fraction == 0.0 {
582                        // Ignore overlapping shapes
583                        return fraction;
584                    }
585
586                    fraction = output.fraction;
587                    output.fraction
588                },
589            );
590
591            if fraction == 0.0 {
592                break;
593            }
594
595            tree_input.max_fraction = fraction;
596        }
597    }
598
599    record_query_result(
600        world,
601        OP_QUERY_CAST_MOVER,
602        |buf| {
603            rec_w_position(buf, origin);
604            rec_w_capsule(buf, *mover);
605            rec_w_vec2(buf, translation);
606            rec_w_queryfilter(buf, filter);
607        },
608        |buf| rec_w_f32(buf, fraction),
609    );
610
611    fraction
612}
613
614/// Collide a capsule mover with the world, gathering collision planes that
615/// can be fed to `solve_planes`. Useful for character controllers. The
616/// callback returns false to terminate the query.
617/// (b2World_CollideMover + static TreeCollideCallback)
618pub fn world_collide_mover(
619    world: &mut World,
620    origin: Pos,
621    mover: &Capsule,
622    filter: QueryFilter,
623    mut fcn: impl FnMut(ShapeId, &PlaneResult) -> bool,
624) {
625    debug_assert!(!world.locked);
626    if world.locked {
627        return;
628    }
629
630    debug_assert!(is_valid_position(origin));
631
632    let mut q = QueryRecorder::begin(world, |buf| {
633        rec_w_position(buf, origin);
634        rec_w_capsule(buf, *mover);
635        rec_w_queryfilter(buf, filter);
636    });
637
638    {
639        let world: &World = world;
640
641        let r = Vec2 {
642            x: mover.radius,
643            y: mover.radius,
644        };
645
646        // Relative box lifted to world float with outward rounding,
647        // conservative for the tree
648        let rel_box = Aabb {
649            lower_bound: crate::math_functions::sub(
650                crate::math_functions::min(mover.center1, mover.center2),
651                r,
652            ),
653            upper_bound: crate::math_functions::add(
654                crate::math_functions::max(mover.center1, mover.center2),
655                r,
656            ),
657        };
658        let aabb = offset_aabb(rel_box, origin);
659
660        for i in 0..BODY_TYPE_COUNT {
661            world.broad_phase.trees[i].query(aabb, filter.mask_bits, |_, user_data| {
662                // (static TreeCollideCallback)
663                let shape_id = user_data as i32;
664                let shape = &world.shapes[shape_id as usize];
665
666                if !should_query_collide(shape.filter, filter) {
667                    return true;
668                }
669
670                // Re-center on the query origin, the mover and the resulting
671                // planes are origin relative
672                let body = &world.bodies[shape.body_id as usize];
673                let transform =
674                    to_relative_transform(get_body_transform_quick(world, body), origin);
675
676                let result = collide_mover(mover, shape, transform);
677
678                // todo handle deep overlap
679                if result.hit && is_normalized(result.plane.normal) {
680                    let id = query_shape_id(world, shape);
681                    let ret = fcn(id, &result);
682                    if q.active() {
683                        // (b2RecPlaneTrampoline)
684                        rec_w_shapeid(&mut q.buf, id);
685                        rec_w_planeresult(&mut q.buf, &result);
686                        rec_w_bool(&mut q.buf, ret);
687                        q.hits += 1;
688                    }
689                    return ret;
690                }
691
692                true
693            });
694        }
695    }
696
697    // CollideMover has no TREESTATS tail (returns void)
698    q.commit(world, OP_QUERY_COLLIDE_MOVER, None);
699}