Skip to main content

box2d_rust/recording/
player_queries.rs

1// Player-side query store: the per-frame stash of recorded spatial queries
2// and the debug drawing of them (b2RecDrawQuery / b2RecRecordedHit /
3// b2RecPlayer_DrawFrameQueries and the public query inspection API).
4//
5// SPDX-FileCopyrightText: 2026 Erin Catto
6// SPDX-License-Identifier: MIT
7
8use crate::collision::{Capsule, PlaneResult, WorldCastOutput};
9use crate::debug_draw::{DebugDraw, HexColor};
10use crate::distance::ShapeProxy;
11use crate::id::ShapeId;
12use crate::math_functions::{mul_sv, offset_pos, Aabb, Pos, Vec2, WorldTransform, ROT_IDENTITY};
13use crate::shape::{shape_get_aabb, shape_is_valid};
14use crate::types::QueryFilter;
15use crate::world::World;
16
17/// The kind of a recorded spatial query, matching the public query and cast
18/// functions. (b2RecQueryType / b2RecQueryKind — the C internal and public
19/// enums are value-identical, so the Rust port keeps one.)
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum RecQueryType {
22    #[default]
23    OverlapAabb,
24    OverlapShape,
25    CastRay,
26    CastShape,
27    CollideMover,
28    CastRayClosest,
29    CastMover,
30    ShapeTestPoint,
31    ShapeRayCast,
32}
33
34/// A single recorded callback hit, used both as reader scratch and as the
35/// per-frame draw store. (b2RecRecordedHit)
36#[derive(Debug, Clone, Copy, Default)]
37pub(crate) struct RecordedHit {
38    pub id: ShapeId,
39    pub point: Pos,
40    pub normal: Vec2,
41    pub fraction: f32,
42    pub plane: PlaneResult,
43    #[allow(dead_code)] // parity with the C struct; the draw path reads only the fields above
44    pub user_return_f: f32,
45    #[allow(dead_code)]
46    pub user_return_b: bool,
47}
48
49/// Per-frame draw record for one query call. (b2RecDrawQuery)
50#[derive(Debug, Clone, Default)]
51pub(crate) struct DrawQuery {
52    pub kind: RecQueryType,
53    pub filter: QueryFilter,
54    pub aabb: Aabb,
55    pub proxy: ShapeProxy,
56    pub mover: Capsule,
57    pub origin: Pos,
58    pub translation: Vec2,
59    pub bool_result: bool,
60    #[allow(dead_code)] // parity with C; shown by viewers, not the draw path
61    pub cast_fraction: f32,
62    pub cast_out: WorldCastOutput,
63    pub shape: ShapeId,
64    pub hit_start: usize,
65    pub hit_count: usize,
66}
67
68/// The per-frame query store, reset at the top of each StepFrame. Hits for
69/// all queries pool into one array like C's frameHits, addressed by each
70/// query's hit_start/hit_count window.
71#[derive(Default)]
72pub(crate) struct QueryStash {
73    pub queries: Vec<DrawQuery>,
74    pub hits: Vec<RecordedHit>,
75}
76
77impl QueryStash {
78    pub fn clear(&mut self) {
79        self.queries.clear();
80        self.hits.clear();
81    }
82
83    /// Append a query with its pooled hits and return it for arg fill-in.
84    /// (b2RecStashQueryBegin)
85    pub fn begin(&mut self, kind: RecQueryType, hits: &[RecordedHit]) -> &mut DrawQuery {
86        let hit_start = self.hits.len();
87        self.hits.extend_from_slice(hits);
88        self.queries.push(DrawQuery {
89            kind,
90            hit_start,
91            hit_count: hits.len(),
92            ..DrawQuery::default()
93        });
94        self.queries.last_mut().unwrap()
95    }
96}
97
98/// A spatial query recorded during a replayed frame, exposed for inspection.
99/// (b2RecQueryInfo)
100#[derive(Debug, Clone, Copy, Default)]
101pub struct RecQueryInfo {
102    pub type_: RecQueryType,
103    /// Zeroed for the shape local query types.
104    pub filter: QueryFilter,
105    /// Overlap AABB, relative to origin.
106    pub aabb: Aabb,
107    /// Query origin.
108    pub origin: Pos,
109    /// Ray and cast translation.
110    pub translation: Vec2,
111    /// Target shape for the shape local query types.
112    pub shape: ShapeId,
113    /// Number of recorded results.
114    pub hit_count: i32,
115}
116
117/// One result of a recorded spatial query. (b2RecQueryHit)
118#[derive(Debug, Clone, Copy, Default)]
119pub struct RecQueryHit {
120    pub shape: ShapeId,
121    pub point: Pos,
122    pub normal: Vec2,
123    pub fraction: f32,
124}
125
126/// Highlight each reported overlap shape by its AABB. Skip any destroyed
127/// since the query, per the shape_get_aabb contract that overlap results may
128/// contain stale shapes. (b2RecDrawHitAABBs)
129fn draw_hit_aabbs(world: &World, stash: &QueryStash, q: &DrawQuery, draw: &mut dyn DebugDraw) {
130    for hit in &stash.hits[q.hit_start..q.hit_start + q.hit_count] {
131        if !shape_is_valid(world, hit.id) {
132            continue;
133        }
134        let b = shape_get_aabb(world, hit.id);
135        let lower = b.lower_bound;
136        let upper = b.upper_bound;
137        let vs = [
138            lower,
139            Vec2 {
140                x: upper.x,
141                y: lower.y,
142            },
143            upper,
144            Vec2 {
145                x: lower.x,
146                y: upper.y,
147            },
148        ];
149        draw.draw_polygon(
150            crate::math_functions::WORLD_TRANSFORM_IDENTITY,
151            &vs,
152            HexColor::MAGENTA,
153        );
154    }
155}
156
157/// Draw spatial queries recorded during the most recently replayed frame.
158/// `query_index` < 0 draws all queries, otherwise just the one selected in
159/// the viewer. The C NULL-callback skips map to the trait's default no-op
160/// methods. (b2RecPlayer_DrawFrameQueries)
161pub(crate) fn draw_stashed_queries(
162    world: &World,
163    stash: &QueryStash,
164    draw: &mut dyn DebugDraw,
165    query_index: i32,
166) {
167    for (qi, q) in stash.queries.iter().enumerate() {
168        if query_index >= 0 && qi as i32 != query_index {
169            continue;
170        }
171
172        match q.kind {
173            RecQueryType::CastRay | RecQueryType::CastRayClosest => {
174                // Ray origin to endpoint
175                let origin = q.origin;
176                let end = offset_pos(origin, q.translation);
177                draw.draw_line(origin, end, HexColor::YELLOW);
178                // Per-hit point + short normal
179                for h in &stash.hits[q.hit_start..q.hit_start + q.hit_count] {
180                    let point = h.point;
181                    draw.draw_point(point, 4.0, HexColor::YELLOW);
182                    let np = offset_pos(point, mul_sv(0.2, h.normal));
183                    draw.draw_line(point, np, HexColor::LIGHT_YELLOW);
184                }
185            }
186            RecQueryType::CastShape => {
187                // Shape cast: draw per-hit points along the swept path
188                for h in &stash.hits[q.hit_start..q.hit_start + q.hit_count] {
189                    let point = h.point;
190                    draw.draw_point(point, 4.0, HexColor::SKY_BLUE);
191                    let np = offset_pos(point, mul_sv(0.2, h.normal));
192                    draw.draw_line(point, np, HexColor::LIGHT_SKY_BLUE);
193                }
194            }
195            RecQueryType::CastMover => {
196                // The mover capsule is relative to the query origin
197                let c1 = offset_pos(q.origin, q.mover.center1);
198                let c2 = offset_pos(q.origin, q.mover.center2);
199                draw.draw_solid_capsule(c1, c2, q.mover.radius, HexColor::LIGHT_SKY_BLUE);
200            }
201            RecQueryType::OverlapAabb => {
202                // The query box is relative to the query origin
203                let lower = q.aabb.lower_bound;
204                let upper = q.aabb.upper_bound;
205                let vs = [
206                    lower,
207                    Vec2 {
208                        x: upper.x,
209                        y: lower.y,
210                    },
211                    upper,
212                    Vec2 {
213                        x: lower.x,
214                        y: upper.y,
215                    },
216                ];
217                draw.draw_polygon(
218                    WorldTransform {
219                        p: q.origin,
220                        q: ROT_IDENTITY,
221                    },
222                    &vs,
223                    HexColor::LIME_GREEN,
224                );
225                draw_hit_aabbs(world, stash, q, draw);
226            }
227            RecQueryType::OverlapShape => {
228                // The proxy points are relative to the query origin
229                if q.proxy.count == 1 {
230                    draw.draw_circle(
231                        offset_pos(q.origin, q.proxy.points[0]),
232                        q.proxy.radius,
233                        HexColor::LIME_GREEN,
234                    );
235                } else if q.proxy.count >= 2 {
236                    draw.draw_polygon(
237                        WorldTransform {
238                            p: q.origin,
239                            q: ROT_IDENTITY,
240                        },
241                        &q.proxy.points[..q.proxy.count as usize],
242                        HexColor::LIME_GREEN,
243                    );
244                }
245                draw_hit_aabbs(world, stash, q, draw);
246            }
247            RecQueryType::CollideMover => {
248                // The mover capsule and the collision planes are relative to
249                // the query origin
250                let c1 = offset_pos(q.origin, q.mover.center1);
251                let c2 = offset_pos(q.origin, q.mover.center2);
252                draw.draw_solid_capsule(c1, c2, q.mover.radius, HexColor::TAN);
253                // Per-hit plane point and normal
254                for h in &stash.hits[q.hit_start..q.hit_start + q.hit_count] {
255                    if h.plane.hit {
256                        let point = offset_pos(q.origin, h.plane.point);
257                        let np = offset_pos(point, mul_sv(0.2, h.plane.plane.normal));
258                        draw.draw_line(point, np, HexColor::ORANGE);
259                    }
260                }
261            }
262            RecQueryType::ShapeTestPoint => {
263                let c = if q.bool_result {
264                    HexColor::AQUA
265                } else {
266                    HexColor::RED
267                };
268                draw.draw_point(q.origin, 6.0, c);
269            }
270            RecQueryType::ShapeRayCast => {
271                let origin = q.origin;
272                let end = offset_pos(origin, q.translation);
273                draw.draw_line(origin, end, HexColor::VIOLET);
274                if q.cast_out.hit {
275                    draw.draw_point(q.cast_out.point, 4.0, HexColor::VIOLET);
276                }
277            }
278        }
279    }
280}
281
282/// Public query inspection over the stash. (b2RecPlayer_GetFrameQuery)
283pub(crate) fn stash_query_info(stash: &QueryStash, index: i32) -> RecQueryInfo {
284    if index < 0 || index as usize >= stash.queries.len() {
285        return RecQueryInfo::default();
286    }
287    let q = &stash.queries[index as usize];
288    RecQueryInfo {
289        type_: q.kind,
290        filter: q.filter,
291        aabb: q.aabb,
292        origin: q.origin,
293        translation: q.translation,
294        shape: q.shape,
295        hit_count: q.hit_count as i32,
296    }
297}
298
299/// (b2RecPlayer_GetFrameQueryHit)
300pub(crate) fn stash_query_hit(stash: &QueryStash, query_index: i32, hit_index: i32) -> RecQueryHit {
301    if query_index < 0 || query_index as usize >= stash.queries.len() {
302        return RecQueryHit::default();
303    }
304    let q = &stash.queries[query_index as usize];
305    if hit_index < 0 || hit_index as usize >= q.hit_count {
306        return RecQueryHit::default();
307    }
308    let h = &stash.hits[q.hit_start + hit_index as usize];
309    RecQueryHit {
310        shape: h.id,
311        point: h.point,
312        normal: h.normal,
313        fraction: h.fraction,
314    }
315}