Skip to main content

oxiphysics_collision/
spatial_queries.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Advanced spatial queries.
5//!
6//! Implements a dynamic AABB tree, overlapping-pair queries (brute-force,
7//! sweep-and-prune, AABB tree), frustum culling, sphere/capsule/point/convex
8//! sweep queries, contact pair filtering by layer mask, and a batched query
9//! pipeline.
10
11// ---------------------------------------------------------------------------
12// AABB type
13// ---------------------------------------------------------------------------
14
15/// Axis-aligned bounding box.
16#[derive(Clone, Debug, PartialEq)]
17pub struct QueryAabb {
18    /// Minimum corner.
19    pub min: [f64; 3],
20    /// Maximum corner.
21    pub max: [f64; 3],
22}
23
24impl QueryAabb {
25    /// Construct from min and max corners.
26    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
27        QueryAabb { min, max }
28    }
29
30    /// Expand each side by `margin`.
31    pub fn fatten(&self, margin: f64) -> Self {
32        QueryAabb {
33            min: [
34                self.min[0] - margin,
35                self.min[1] - margin,
36                self.min[2] - margin,
37            ],
38            max: [
39                self.max[0] + margin,
40                self.max[1] + margin,
41                self.max[2] + margin,
42            ],
43        }
44    }
45
46    /// Surface area of the AABB.
47    pub fn surface_area(&self) -> f64 {
48        let dx = self.max[0] - self.min[0];
49        let dy = self.max[1] - self.min[1];
50        let dz = self.max[2] - self.min[2];
51        2.0 * (dx * dy + dy * dz + dz * dx)
52    }
53
54    /// Center of the AABB.
55    pub fn center(&self) -> [f64; 3] {
56        [
57            0.5 * (self.min[0] + self.max[0]),
58            0.5 * (self.min[1] + self.max[1]),
59            0.5 * (self.min[2] + self.max[2]),
60        ]
61    }
62
63    /// Union of two AABBs.
64    pub fn union(&self, other: &QueryAabb) -> QueryAabb {
65        QueryAabb {
66            min: [
67                self.min[0].min(other.min[0]),
68                self.min[1].min(other.min[1]),
69                self.min[2].min(other.min[2]),
70            ],
71            max: [
72                self.max[0].max(other.max[0]),
73                self.max[1].max(other.max[1]),
74                self.max[2].max(other.max[2]),
75            ],
76        }
77    }
78
79    /// Test if two AABBs overlap.
80    pub fn overlaps(&self, other: &QueryAabb) -> bool {
81        self.min[0] <= other.max[0]
82            && self.max[0] >= other.min[0]
83            && self.min[1] <= other.max[1]
84            && self.max[1] >= other.min[1]
85            && self.min[2] <= other.max[2]
86            && self.max[2] >= other.min[2]
87    }
88
89    /// Test if this AABB contains a point.
90    pub fn contains_point(&self, p: [f64; 3]) -> bool {
91        p[0] >= self.min[0]
92            && p[0] <= self.max[0]
93            && p[1] >= self.min[1]
94            && p[1] <= self.max[1]
95            && p[2] >= self.min[2]
96            && p[2] <= self.max[2]
97    }
98
99    /// Squared distance from a point to the AABB (0 if inside).
100    pub fn point_dist_sq(&self, p: [f64; 3]) -> f64 {
101        p.iter()
102            .zip(self.min.iter())
103            .zip(self.max.iter())
104            .map(|((&pi, &mini), &maxi)| {
105                if pi < mini {
106                    (mini - pi) * (mini - pi)
107                } else if pi > maxi {
108                    (pi - maxi) * (pi - maxi)
109                } else {
110                    0.0
111                }
112            })
113            .sum()
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Helper functions (public)
119// ---------------------------------------------------------------------------
120
121/// Test if two AABBs overlap.
122pub fn aabb_overlap(a: &QueryAabb, b: &QueryAabb) -> bool {
123    a.overlaps(b)
124}
125
126/// Test if an AABB passes a frustum culling test (6 plane tests).
127///
128/// `planes` is a slice of (normal, d) pairs where plane equation = n·x + d = 0.
129/// Returns true if the AABB is at least partially inside the frustum.
130pub fn frustum_aabb_test(aabb: &QueryAabb, planes: &[([f64; 3], f64)]) -> bool {
131    for &(n, d) in planes {
132        // P-vertex: corner furthest in direction n
133        let px = if n[0] > 0.0 { aabb.max[0] } else { aabb.min[0] };
134        let py = if n[1] > 0.0 { aabb.max[1] } else { aabb.min[1] };
135        let pz = if n[2] > 0.0 { aabb.max[2] } else { aabb.min[2] };
136        let dist = n[0] * px + n[1] * py + n[2] * pz + d;
137        if dist < 0.0 {
138            return false;
139        } // AABB fully outside this plane
140    }
141    true
142}
143
144/// Compute the swept AABB of an object moving from `aabb` by `motion`.
145pub fn sweep_aabb_motion(aabb: &QueryAabb, motion: [f64; 3]) -> QueryAabb {
146    let mut min = aabb.min;
147    let mut max = aabb.max;
148    for ((mn, mx), &m) in min.iter_mut().zip(max.iter_mut()).zip(motion.iter()) {
149        if m < 0.0 {
150            *mn += m;
151        } else {
152            *mx += m;
153        }
154    }
155    QueryAabb { min, max }
156}
157
158/// Rebalance an AABB tree by rotating nodes to minimize surface area.
159///
160/// This is a placeholder — actual rebalancing is done inside `AabbTree`.
161pub fn rebalance_tree(_tree: &mut AabbTree) {
162    // Rebalancing logic is integrated into AabbTree::insert
163}
164
165// ---------------------------------------------------------------------------
166// Dynamic AABB tree node
167// ---------------------------------------------------------------------------
168
169/// A node in the dynamic AABB tree.
170#[derive(Clone, Debug)]
171pub struct DynamicNode {
172    /// Fat AABB (enlarged by margin).
173    pub fat_aabb: QueryAabb,
174    /// Tight AABB (actual object bounds).
175    pub aabb: QueryAabb,
176    /// Parent node index, or None for root.
177    pub parent: Option<usize>,
178    /// Children node indices (None for leaves).
179    pub children: [Option<usize>; 2],
180    /// Height in the tree (0 = leaf).
181    pub height: i32,
182    /// User data (shape ID).
183    pub user_data: usize,
184    /// True if this is a leaf node.
185    pub is_leaf: bool,
186}
187
188impl DynamicNode {
189    /// Create a leaf node.
190    pub fn leaf(aabb: QueryAabb, margin: f64, user_data: usize) -> Self {
191        let fat_aabb = aabb.fatten(margin);
192        DynamicNode {
193            fat_aabb,
194            aabb,
195            parent: None,
196            children: [None, None],
197            height: 0,
198            user_data,
199            is_leaf: true,
200        }
201    }
202
203    /// Create an internal node.
204    pub fn internal(fat_aabb: QueryAabb) -> Self {
205        DynamicNode {
206            fat_aabb: fat_aabb.clone(),
207            aabb: fat_aabb,
208            parent: None,
209            children: [None, None],
210            height: 1,
211            user_data: usize::MAX,
212            is_leaf: false,
213        }
214    }
215}
216
217// ---------------------------------------------------------------------------
218// Dynamic AABB tree
219// ---------------------------------------------------------------------------
220
221/// Dynamic AABB tree for broad-phase collision detection.
222pub struct AabbTree {
223    /// Node storage.
224    pub nodes: Vec<DynamicNode>,
225    /// Root node index.
226    pub root: Option<usize>,
227    /// Free node list (indices of removed nodes).
228    free_nodes: Vec<usize>,
229    /// Fat AABB margin.
230    pub margin: f64,
231}
232
233impl AabbTree {
234    /// Construct an empty AABB tree.
235    pub fn new(margin: f64) -> Self {
236        AabbTree {
237            nodes: Vec::new(),
238            root: None,
239            free_nodes: Vec::new(),
240            margin,
241        }
242    }
243
244    fn alloc_node(&mut self, node: DynamicNode) -> usize {
245        if let Some(idx) = self.free_nodes.pop() {
246            self.nodes[idx] = node;
247            idx
248        } else {
249            self.nodes.push(node);
250            self.nodes.len() - 1
251        }
252    }
253
254    /// Insert an AABB with user data, returns the leaf node index.
255    pub fn insert(&mut self, aabb: QueryAabb, user_data: usize) -> usize {
256        let leaf_idx = self.alloc_node(DynamicNode::leaf(aabb.clone(), self.margin, user_data));
257
258        let root = match self.root {
259            None => {
260                self.root = Some(leaf_idx);
261                return leaf_idx;
262            }
263            Some(r) => r,
264        };
265
266        // Find best sibling (greedy surface area heuristic)
267        let mut best = root;
268        let mut stack = vec![root];
269        let leaf_aabb = aabb.clone();
270        while let Some(idx) = stack.pop() {
271            let node_aabb = self.nodes[idx].fat_aabb.clone();
272            let combined = node_aabb.union(&leaf_aabb);
273            let combined_sa = combined.surface_area();
274            let cost = combined_sa;
275            if cost < self.nodes[best].fat_aabb.union(&leaf_aabb).surface_area() {
276                best = idx;
277            }
278            if !self.nodes[idx].is_leaf {
279                for c in 0..2 {
280                    if let Some(child) = self.nodes[idx].children[c] {
281                        stack.push(child);
282                    }
283                }
284            }
285        }
286
287        // Create a new internal node as sibling
288        let sibling = best;
289        let old_parent = self.nodes[sibling].parent;
290        let new_parent_aabb = self.nodes[sibling]
291            .fat_aabb
292            .union(&leaf_aabb.fatten(self.margin));
293        let new_parent_idx = self.alloc_node(DynamicNode::internal(new_parent_aabb));
294        self.nodes[new_parent_idx].parent = old_parent;
295        self.nodes[new_parent_idx].children = [Some(sibling), Some(leaf_idx)];
296        self.nodes[new_parent_idx].height = self.nodes[sibling].height + 1;
297        self.nodes[sibling].parent = Some(new_parent_idx);
298        self.nodes[leaf_idx].parent = Some(new_parent_idx);
299
300        if let Some(op) = old_parent {
301            for c in 0..2 {
302                if self.nodes[op].children[c] == Some(sibling) {
303                    self.nodes[op].children[c] = Some(new_parent_idx);
304                }
305            }
306        } else {
307            self.root = Some(new_parent_idx);
308        }
309
310        leaf_idx
311    }
312
313    /// Remove a leaf node by index.
314    pub fn remove(&mut self, leaf_idx: usize) {
315        if self.root == Some(leaf_idx) {
316            self.root = None;
317            self.free_nodes.push(leaf_idx);
318            return;
319        }
320        if let Some(parent_idx) = self.nodes[leaf_idx].parent {
321            let sibling = {
322                let ch = self.nodes[parent_idx].children;
323                if ch[0] == Some(leaf_idx) {
324                    ch[1]
325                } else {
326                    ch[0]
327                }
328            };
329            if let Some(grandparent) = self.nodes[parent_idx].parent {
330                for c in 0..2 {
331                    if self.nodes[grandparent].children[c] == Some(parent_idx) {
332                        self.nodes[grandparent].children[c] = sibling;
333                    }
334                }
335                if let Some(s) = sibling {
336                    self.nodes[s].parent = Some(grandparent);
337                }
338            } else {
339                self.root = sibling;
340                if let Some(s) = sibling {
341                    self.nodes[s].parent = None;
342                }
343            }
344            self.free_nodes.push(parent_idx);
345        }
346        self.free_nodes.push(leaf_idx);
347    }
348
349    /// Update a leaf node's AABB (remove + re-insert if moved outside fat AABB).
350    pub fn update(&mut self, leaf_idx: usize, new_aabb: QueryAabb) {
351        if self.nodes[leaf_idx].fat_aabb.overlaps(&new_aabb) {
352            self.nodes[leaf_idx].aabb = new_aabb;
353            return;
354        }
355        let user_data = self.nodes[leaf_idx].user_data;
356        self.remove(leaf_idx);
357        self.insert(new_aabb, user_data);
358    }
359
360    /// Query all leaf nodes whose fat AABB overlaps `query_aabb`.
361    pub fn query_aabb(&self, query_aabb: &QueryAabb) -> Vec<usize> {
362        let mut result = Vec::new();
363        let Some(root) = self.root else {
364            return result;
365        };
366        let mut stack = vec![root];
367        while let Some(idx) = stack.pop() {
368            let node = &self.nodes[idx];
369            if !node.fat_aabb.overlaps(query_aabb) {
370                continue;
371            }
372            if node.is_leaf {
373                result.push(node.user_data);
374            } else {
375                for c in 0..2 {
376                    if let Some(child) = node.children[c] {
377                        stack.push(child);
378                    }
379                }
380            }
381        }
382        result
383    }
384
385    /// Raycast: find all leaf nodes whose fat AABB is hit by ray (origin, dir, max_t).
386    pub fn raycast(&self, origin: [f64; 3], dir: [f64; 3], max_t: f64) -> Vec<usize> {
387        let mut result = Vec::new();
388        let Some(root) = self.root else {
389            return result;
390        };
391        let mut stack = vec![root];
392        while let Some(idx) = stack.pop() {
393            let node = &self.nodes[idx];
394            if !ray_aabb_hit(&node.fat_aabb, origin, dir, max_t) {
395                continue;
396            }
397            if node.is_leaf {
398                result.push(node.user_data);
399            } else {
400                for c in 0..2 {
401                    if let Some(child) = node.children[c] {
402                        stack.push(child);
403                    }
404                }
405            }
406        }
407        result
408    }
409
410    /// Number of live nodes.
411    pub fn n_nodes(&self) -> usize {
412        self.nodes.len().saturating_sub(self.free_nodes.len())
413    }
414}
415
416fn ray_aabb_hit(aabb: &QueryAabb, origin: [f64; 3], dir: [f64; 3], max_t: f64) -> bool {
417    let mut t_min = 0.0_f64;
418    let mut t_max = max_t;
419    for ((&o, &d), (&mn, &mx)) in origin
420        .iter()
421        .zip(dir.iter())
422        .zip(aabb.min.iter().zip(aabb.max.iter()))
423    {
424        if d.abs() < 1e-14 {
425            if o < mn || o > mx {
426                return false;
427            }
428        } else {
429            let inv = 1.0 / d;
430            let t1 = (mn - o) * inv;
431            let t2 = (mx - o) * inv;
432            let ta = t1.min(t2);
433            let tb = t1.max(t2);
434            t_min = t_min.max(ta);
435            t_max = t_max.min(tb);
436            if t_min > t_max {
437                return false;
438            }
439        }
440    }
441    true
442}
443
444// ---------------------------------------------------------------------------
445// Pair query
446// ---------------------------------------------------------------------------
447
448/// An overlapping pair of shape IDs.
449#[derive(Clone, Debug, PartialEq, Eq)]
450pub struct ShapePair {
451    /// First shape ID.
452    pub a: usize,
453    /// Second shape ID.
454    pub b: usize,
455}
456
457impl ShapePair {
458    /// Create a new pair (a < b always).
459    pub fn new(a: usize, b: usize) -> Self {
460        if a < b {
461            ShapePair { a, b }
462        } else {
463            ShapePair { a: b, b: a }
464        }
465    }
466}
467
468/// Overlapping pair detection using multiple algorithms.
469pub struct PairQuery;
470
471impl PairQuery {
472    /// Brute-force O(n²) pair detection.
473    pub fn brute_force(aabbs: &[QueryAabb]) -> Vec<ShapePair> {
474        let mut pairs = Vec::new();
475        for (i, a) in aabbs.iter().enumerate() {
476            for (j, b) in aabbs[i + 1..].iter().enumerate() {
477                if a.overlaps(b) {
478                    pairs.push(ShapePair::new(i, i + 1 + j));
479                }
480            }
481        }
482        pairs
483    }
484
485    /// Sweep-and-prune along X axis.
486    pub fn sweep_and_prune(aabbs: &[QueryAabb]) -> Vec<ShapePair> {
487        let mut sorted: Vec<usize> = (0..aabbs.len()).collect();
488        sorted.sort_unstable_by(|&a, &b| {
489            aabbs[a].min[0]
490                .partial_cmp(&aabbs[b].min[0])
491                .unwrap_or(std::cmp::Ordering::Equal)
492        });
493        let mut pairs = Vec::new();
494        for (i, &a) in sorted.iter().enumerate() {
495            for &b in &sorted[i + 1..] {
496                if aabbs[b].min[0] > aabbs[a].max[0] {
497                    break;
498                }
499                if aabbs[a].overlaps(&aabbs[b]) {
500                    pairs.push(ShapePair::new(a, b));
501                }
502            }
503        }
504        pairs
505    }
506
507    /// AABB tree-based pair detection.
508    pub fn aabb_tree(aabbs: &[QueryAabb]) -> Vec<ShapePair> {
509        let mut tree = AabbTree::new(0.01);
510        for (i, aabb) in aabbs.iter().enumerate() {
511            tree.insert(aabb.clone(), i);
512        }
513        let mut pairs = Vec::new();
514        for (i, aabb) in aabbs.iter().enumerate() {
515            let hits = tree.query_aabb(aabb);
516            for h in hits {
517                if h != i && h > i {
518                    pairs.push(ShapePair::new(i, h));
519                }
520            }
521        }
522        // Deduplicate
523        pairs.sort_unstable_by_key(|p| (p.a, p.b));
524        pairs.dedup();
525        pairs
526    }
527}
528
529// ---------------------------------------------------------------------------
530// Frustum culling
531// ---------------------------------------------------------------------------
532
533/// Frustum defined by 6 planes.
534pub struct FrustumCulling {
535    /// Planes as (normal, d): n·x + d = 0.
536    pub planes: Vec<([f64; 3], f64)>,
537}
538
539impl FrustumCulling {
540    /// Construct from 6 planes.
541    pub fn new(planes: Vec<([f64; 3], f64)>) -> Self {
542        FrustumCulling { planes }
543    }
544
545    /// Cull AABBs: return indices of visible (not fully outside) objects.
546    pub fn cull(&self, aabbs: &[QueryAabb]) -> Vec<usize> {
547        aabbs
548            .iter()
549            .enumerate()
550            .filter(|(_, aabb)| frustum_aabb_test(aabb, &self.planes))
551            .map(|(i, _)| i)
552            .collect()
553    }
554
555    /// Test a single AABB.
556    pub fn test(&self, aabb: &QueryAabb) -> bool {
557        frustum_aabb_test(aabb, &self.planes)
558    }
559}
560
561// ---------------------------------------------------------------------------
562// Sphere query
563// ---------------------------------------------------------------------------
564
565/// Query for shapes overlapping a sphere.
566#[derive(Clone, Debug)]
567pub struct SphereQuery {
568    /// Center.
569    pub center: [f64; 3],
570    /// Radius.
571    pub radius: f64,
572}
573
574impl SphereQuery {
575    /// Construct a sphere query.
576    pub fn new(center: [f64; 3], radius: f64) -> Self {
577        SphereQuery { center, radius }
578    }
579
580    /// Return shape indices overlapping this sphere (against AABB list), sorted by distance.
581    pub fn query_sorted(&self, aabbs: &[QueryAabb]) -> Vec<usize> {
582        let r2 = self.radius * self.radius;
583        let mut hits: Vec<(usize, f64)> = aabbs
584            .iter()
585            .enumerate()
586            .filter_map(|(i, aabb)| {
587                let d2 = aabb.point_dist_sq(self.center);
588                if d2 <= r2 { Some((i, d2)) } else { None }
589            })
590            .collect();
591        hits.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
592        hits.into_iter().map(|(i, _)| i).collect()
593    }
594
595    /// Sphere AABB for broad-phase.
596    pub fn aabb(&self) -> QueryAabb {
597        let r = self.radius;
598        QueryAabb {
599            min: [self.center[0] - r, self.center[1] - r, self.center[2] - r],
600            max: [self.center[0] + r, self.center[1] + r, self.center[2] + r],
601        }
602    }
603}
604
605// ---------------------------------------------------------------------------
606// Capsule query
607// ---------------------------------------------------------------------------
608
609/// Query for shapes along a capsule sweep path.
610pub struct CapsuleQuery {
611    /// Start of capsule axis.
612    pub p0: [f64; 3],
613    /// End of capsule axis.
614    pub p1: [f64; 3],
615    /// Radius.
616    pub radius: f64,
617}
618
619impl CapsuleQuery {
620    /// Construct a capsule query.
621    pub fn new(p0: [f64; 3], p1: [f64; 3], radius: f64) -> Self {
622        CapsuleQuery { p0, p1, radius }
623    }
624
625    /// AABB enclosing the capsule.
626    pub fn aabb(&self) -> QueryAabb {
627        let r = self.radius;
628        QueryAabb {
629            min: [
630                self.p0[0].min(self.p1[0]) - r,
631                self.p0[1].min(self.p1[1]) - r,
632                self.p0[2].min(self.p1[2]) - r,
633            ],
634            max: [
635                self.p0[0].max(self.p1[0]) + r,
636                self.p0[1].max(self.p1[1]) + r,
637                self.p0[2].max(self.p1[2]) + r,
638            ],
639        }
640    }
641
642    /// Return shapes hit by this capsule (broad-phase AABB test).
643    pub fn query(&self, aabbs: &[QueryAabb]) -> Vec<usize> {
644        let caps_aabb = self.aabb();
645        aabbs
646            .iter()
647            .enumerate()
648            .filter(|(_, aabb)| aabb.overlaps(&caps_aabb))
649            .map(|(i, _)| i)
650            .collect()
651    }
652}
653
654// ---------------------------------------------------------------------------
655// Point query
656// ---------------------------------------------------------------------------
657
658/// Query for the N shapes nearest to a point.
659pub struct PointQuery {
660    /// Query point.
661    pub point: [f64; 3],
662}
663
664impl PointQuery {
665    /// Construct a point query.
666    pub fn new(point: [f64; 3]) -> Self {
667        PointQuery { point }
668    }
669
670    /// Return the `n` shape indices nearest to the point, sorted by distance.
671    pub fn nearest_n(&self, aabbs: &[QueryAabb], n: usize) -> Vec<usize> {
672        let mut dists: Vec<(usize, f64)> = aabbs
673            .iter()
674            .enumerate()
675            .map(|(i, aabb)| (i, aabb.point_dist_sq(self.point).sqrt()))
676            .collect();
677        dists.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
678        dists.into_iter().take(n).map(|(i, _)| i).collect()
679    }
680
681    /// Exact distance to AABB i.
682    pub fn distance_to(&self, aabb: &QueryAabb) -> f64 {
683        aabb.point_dist_sq(self.point).sqrt()
684    }
685}
686
687// ---------------------------------------------------------------------------
688// Convex sweep
689// ---------------------------------------------------------------------------
690
691/// Convex sweep: find all shapes hit by a box swept along `motion`.
692pub struct ConvexSweep {
693    /// AABB of the convex shape at rest.
694    pub shape_aabb: QueryAabb,
695    /// Sweep motion vector.
696    pub motion: [f64; 3],
697}
698
699impl ConvexSweep {
700    /// Construct a convex sweep.
701    pub fn new(shape_aabb: QueryAabb, motion: [f64; 3]) -> Self {
702        ConvexSweep { shape_aabb, motion }
703    }
704
705    /// Swept AABB enclosing the entire motion path.
706    pub fn swept_aabb(&self) -> QueryAabb {
707        sweep_aabb_motion(&self.shape_aabb, self.motion)
708    }
709
710    /// Return shapes hit by the swept volume.
711    pub fn query(&self, aabbs: &[QueryAabb]) -> Vec<usize> {
712        let swept = self.swept_aabb();
713        aabbs
714            .iter()
715            .enumerate()
716            .filter(|(_, aabb)| aabb.overlaps(&swept))
717            .map(|(i, _)| i)
718            .collect()
719    }
720}
721
722// ---------------------------------------------------------------------------
723// Contact pair filter
724// ---------------------------------------------------------------------------
725
726/// Contact pair filter using collision layer masks.
727pub struct ContactPairFilter {
728    /// Layer mask for each shape.
729    pub layer_masks: Vec<u32>,
730    /// Group for each shape.
731    pub groups: Vec<u32>,
732}
733
734impl ContactPairFilter {
735    /// Construct a contact pair filter.
736    pub fn new(layer_masks: Vec<u32>, groups: Vec<u32>) -> Self {
737        ContactPairFilter {
738            layer_masks,
739            groups,
740        }
741    }
742
743    /// True if shapes `a` and `b` should collide (bitmask AND ≠ 0 and same group check).
744    pub fn should_collide(&self, a: usize, b: usize) -> bool {
745        let mask_a = self.layer_masks.get(a).copied().unwrap_or(0xFFFF_FFFF);
746        let mask_b = self.layer_masks.get(b).copied().unwrap_or(0xFFFF_FFFF);
747        (mask_a & mask_b) != 0
748    }
749
750    /// Filter a list of shape pairs.
751    pub fn filter(&self, pairs: &[ShapePair]) -> Vec<ShapePair> {
752        pairs
753            .iter()
754            .filter(|p| self.should_collide(p.a, p.b))
755            .cloned()
756            .collect()
757    }
758}
759
760// ---------------------------------------------------------------------------
761// Query pipeline
762// ---------------------------------------------------------------------------
763
764/// A pending query (for batched processing).
765#[derive(Clone, Debug)]
766pub enum PendingQuery {
767    /// AABB overlap query.
768    AabbQuery(QueryAabb),
769    /// Sphere overlap query.
770    SphereQuery(SphereQuery),
771    /// Point nearest-N query.
772    PointQuery {
773        /// Query point in 3D space.
774        point: [f64; 3],
775        /// Number of nearest neighbours to find.
776        n: usize,
777    },
778}
779
780/// Result of a pending query.
781#[derive(Clone, Debug)]
782pub struct QueryResult {
783    /// Shape indices returned by the query.
784    pub hits: Vec<usize>,
785}
786
787/// Multi-query pipeline with deferred (next-frame) result dispatch.
788pub struct QueryPipeline {
789    /// Registered AABBs.
790    pub aabbs: Vec<QueryAabb>,
791    /// Pending queries.
792    pub pending: Vec<PendingQuery>,
793    /// Results from previous frame.
794    pub results: Vec<QueryResult>,
795}
796
797impl QueryPipeline {
798    /// Construct an empty pipeline.
799    pub fn new() -> Self {
800        QueryPipeline {
801            aabbs: Vec::new(),
802            pending: Vec::new(),
803            results: Vec::new(),
804        }
805    }
806
807    /// Register a shape AABB.
808    pub fn add_shape(&mut self, aabb: QueryAabb) -> usize {
809        self.aabbs.push(aabb);
810        self.aabbs.len() - 1
811    }
812
813    /// Submit a query for deferred processing.
814    pub fn submit(&mut self, query: PendingQuery) {
815        self.pending.push(query);
816    }
817
818    /// Process all pending queries and store results (simulated next-frame).
819    pub fn flush(&mut self) {
820        let aabbs = &self.aabbs;
821        let mut new_results = Vec::with_capacity(self.pending.len());
822        for query in self.pending.drain(..) {
823            let hits = match query {
824                PendingQuery::AabbQuery(aabb) => aabbs
825                    .iter()
826                    .enumerate()
827                    .filter(|(_, a)| a.overlaps(&aabb))
828                    .map(|(i, _)| i)
829                    .collect(),
830                PendingQuery::SphereQuery(sq) => {
831                    let sphere_aabb = sq.aabb();
832                    aabbs
833                        .iter()
834                        .enumerate()
835                        .filter(|(_, a)| a.overlaps(&sphere_aabb))
836                        .map(|(i, _)| i)
837                        .collect()
838                }
839                PendingQuery::PointQuery { point, n } => {
840                    let pq = PointQuery::new(point);
841                    pq.nearest_n(aabbs, n)
842                }
843            };
844            new_results.push(QueryResult { hits });
845        }
846        self.results = new_results;
847    }
848
849    /// Number of shapes registered.
850    pub fn n_shapes(&self) -> usize {
851        self.aabbs.len()
852    }
853}
854
855impl Default for QueryPipeline {
856    fn default() -> Self {
857        Self::new()
858    }
859}
860
861// ---------------------------------------------------------------------------
862// Tests
863// ---------------------------------------------------------------------------
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868
869    fn unit_aabb() -> QueryAabb {
870        QueryAabb::new([0.0; 3], [1.0; 3])
871    }
872
873    fn shifted_aabb(offset: f64) -> QueryAabb {
874        QueryAabb::new([offset; 3], [offset + 1.0; 3])
875    }
876
877    #[test]
878    fn test_aabb_overlaps_touching() {
879        let a = QueryAabb::new([0.0; 3], [1.0; 3]);
880        let b = QueryAabb::new([1.0; 3], [2.0; 3]);
881        assert!(a.overlaps(&b));
882    }
883
884    #[test]
885    fn test_aabb_no_overlap() {
886        let a = QueryAabb::new([0.0; 3], [1.0; 3]);
887        let b = QueryAabb::new([2.0; 3], [3.0; 3]);
888        assert!(!a.overlaps(&b));
889    }
890
891    #[test]
892    fn test_aabb_union() {
893        let a = QueryAabb::new([0.0; 3], [1.0; 3]);
894        let b = QueryAabb::new([1.0; 3], [2.0; 3]);
895        let u = a.union(&b);
896        assert_eq!(u.min, [0.0; 3]);
897        assert_eq!(u.max, [2.0; 3]);
898    }
899
900    #[test]
901    fn test_aabb_contains_point() {
902        let a = unit_aabb();
903        assert!(a.contains_point([0.5, 0.5, 0.5]));
904        assert!(!a.contains_point([2.0, 0.5, 0.5]));
905    }
906
907    #[test]
908    fn test_aabb_point_dist_sq_inside() {
909        let a = unit_aabb();
910        assert_eq!(a.point_dist_sq([0.5, 0.5, 0.5]), 0.0);
911    }
912
913    #[test]
914    fn test_aabb_point_dist_sq_outside() {
915        let a = unit_aabb();
916        let d2 = a.point_dist_sq([2.0, 0.5, 0.5]);
917        assert!((d2 - 1.0).abs() < 1e-12);
918    }
919
920    #[test]
921    fn test_aabb_fatten() {
922        let a = unit_aabb();
923        let fat = a.fatten(0.5);
924        assert!((fat.min[0] - (-0.5)).abs() < 1e-12);
925        assert!((fat.max[0] - 1.5).abs() < 1e-12);
926    }
927
928    #[test]
929    fn test_aabb_tree_insert_and_query() {
930        let mut tree = AabbTree::new(0.1);
931        let idx = tree.insert(unit_aabb(), 42);
932        let hits = tree.query_aabb(&unit_aabb());
933        assert!(hits.contains(&42), "hits={:?}, leaf_idx={}", hits, idx);
934    }
935
936    #[test]
937    fn test_aabb_tree_remove() {
938        let mut tree = AabbTree::new(0.1);
939        let leaf = tree.insert(unit_aabb(), 0);
940        tree.remove(leaf);
941        let hits = tree.query_aabb(&unit_aabb());
942        assert!(hits.is_empty());
943    }
944
945    #[test]
946    fn test_aabb_tree_multiple_inserts() {
947        let mut tree = AabbTree::new(0.1);
948        tree.insert(unit_aabb(), 0);
949        tree.insert(shifted_aabb(5.0), 1);
950        let hits = tree.query_aabb(&unit_aabb());
951        assert!(hits.contains(&0));
952        assert!(!hits.contains(&1));
953    }
954
955    #[test]
956    fn test_aabb_tree_raycast() {
957        let mut tree = AabbTree::new(0.1);
958        tree.insert(unit_aabb(), 7);
959        let hits = tree.raycast([-5.0, 0.5, 0.5], [1.0, 0.0, 0.0], 20.0);
960        assert!(hits.contains(&7));
961    }
962
963    #[test]
964    fn test_pair_query_brute_force() {
965        let aabbs = vec![unit_aabb(), shifted_aabb(0.5), shifted_aabb(10.0)];
966        let pairs = PairQuery::brute_force(&aabbs);
967        // 0 and 1 overlap, 2 does not
968        assert!(
969            pairs
970                .iter()
971                .any(|p| (p.a == 0 && p.b == 1) || (p.a == 1 && p.b == 0))
972        );
973        assert!(!pairs.iter().any(|p| p.a == 2 || p.b == 2));
974    }
975
976    #[test]
977    fn test_pair_query_sweep_and_prune() {
978        let aabbs = vec![unit_aabb(), shifted_aabb(0.5)];
979        let pairs = PairQuery::sweep_and_prune(&aabbs);
980        assert!(!pairs.is_empty());
981    }
982
983    #[test]
984    fn test_pair_query_aabb_tree() {
985        let aabbs = vec![unit_aabb(), shifted_aabb(0.5), shifted_aabb(10.0)];
986        let pairs = PairQuery::aabb_tree(&aabbs);
987        assert!(pairs.iter().any(|p| p.a == 0 && p.b == 1));
988    }
989
990    #[test]
991    fn test_frustum_culling_inside() {
992        // Frustum: half-space x > -10
993        let planes = vec![([1.0_f64, 0.0, 0.0], 10.0)];
994        let fc = FrustumCulling::new(planes);
995        let visible = fc.cull(&[unit_aabb()]);
996        assert!(visible.contains(&0));
997    }
998
999    #[test]
1000    fn test_frustum_culling_outside() {
1001        // Frustum: x > 100
1002        let planes = vec![([1.0_f64, 0.0, 0.0], -200.0)];
1003        let fc = FrustumCulling::new(planes);
1004        let visible = fc.cull(&[unit_aabb()]);
1005        assert!(visible.is_empty());
1006    }
1007
1008    #[test]
1009    fn test_sphere_query_hit() {
1010        let sq = SphereQuery::new([0.5, 0.5, 0.5], 0.1);
1011        let hits = sq.query_sorted(&[unit_aabb()]);
1012        assert!(hits.contains(&0));
1013    }
1014
1015    #[test]
1016    fn test_sphere_query_miss() {
1017        let sq = SphereQuery::new([10.0, 10.0, 10.0], 0.1);
1018        let hits = sq.query_sorted(&[unit_aabb()]);
1019        assert!(hits.is_empty());
1020    }
1021
1022    #[test]
1023    fn test_capsule_query_hit() {
1024        let cq = CapsuleQuery::new([0.5, 0.5, -1.0], [0.5, 0.5, 2.0], 0.1);
1025        let hits = cq.query(&[unit_aabb()]);
1026        assert!(hits.contains(&0));
1027    }
1028
1029    #[test]
1030    fn test_capsule_query_miss() {
1031        let cq = CapsuleQuery::new([10.0, 10.0, 10.0], [11.0, 11.0, 11.0], 0.1);
1032        let hits = cq.query(&[unit_aabb()]);
1033        assert!(hits.is_empty());
1034    }
1035
1036    #[test]
1037    fn test_point_query_nearest() {
1038        let aabbs = vec![unit_aabb(), shifted_aabb(5.0)];
1039        let pq = PointQuery::new([0.5, 0.5, 0.5]);
1040        let nearest = pq.nearest_n(&aabbs, 1);
1041        assert_eq!(nearest[0], 0);
1042    }
1043
1044    #[test]
1045    fn test_point_query_distance_inside() {
1046        let aabb = unit_aabb();
1047        let pq = PointQuery::new([0.5, 0.5, 0.5]);
1048        assert_eq!(pq.distance_to(&aabb), 0.0);
1049    }
1050
1051    #[test]
1052    fn test_convex_sweep_hit() {
1053        let cs = ConvexSweep::new(QueryAabb::new([-1.0; 3], [0.0; 3]), [2.0, 2.0, 2.0]);
1054        let hits = cs.query(&[unit_aabb()]);
1055        assert!(hits.contains(&0));
1056    }
1057
1058    #[test]
1059    fn test_convex_sweep_miss() {
1060        let cs = ConvexSweep::new(QueryAabb::new([-10.0; 3], [-9.0; 3]), [-1.0, 0.0, 0.0]);
1061        let hits = cs.query(&[unit_aabb()]);
1062        assert!(hits.is_empty());
1063    }
1064
1065    #[test]
1066    fn test_contact_pair_filter_passes() {
1067        let cf = ContactPairFilter::new(vec![0xF, 0xF], vec![0, 0]);
1068        assert!(cf.should_collide(0, 1));
1069    }
1070
1071    #[test]
1072    fn test_contact_pair_filter_blocks() {
1073        let cf = ContactPairFilter::new(vec![0x1, 0x2], vec![0, 0]);
1074        assert!(!cf.should_collide(0, 1));
1075    }
1076
1077    #[test]
1078    fn test_contact_pair_filter_filter() {
1079        let cf = ContactPairFilter::new(vec![0xF, 0xF, 0x1], vec![0; 3]);
1080        let pairs = vec![ShapePair::new(0, 1), ShapePair::new(0, 2)];
1081        let filtered = cf.filter(&pairs);
1082        // 0↔1: mask 0xF&0xF ≠ 0 → passes; 0↔2: mask 0xF&0x1 = 0x1 ≠ 0 → passes
1083        assert_eq!(filtered.len(), 2);
1084    }
1085
1086    #[test]
1087    fn test_query_pipeline_flush() {
1088        let mut qp = QueryPipeline::new();
1089        qp.add_shape(unit_aabb());
1090        qp.submit(PendingQuery::AabbQuery(unit_aabb()));
1091        qp.flush();
1092        assert_eq!(qp.results.len(), 1);
1093        assert!(qp.results[0].hits.contains(&0));
1094    }
1095
1096    #[test]
1097    fn test_query_pipeline_sphere_query() {
1098        let mut qp = QueryPipeline::new();
1099        qp.add_shape(unit_aabb());
1100        let sq = SphereQuery::new([0.5, 0.5, 0.5], 0.5);
1101        qp.submit(PendingQuery::SphereQuery(sq));
1102        qp.flush();
1103        assert!(!qp.results.is_empty());
1104    }
1105
1106    #[test]
1107    fn test_query_pipeline_point_query() {
1108        let mut qp = QueryPipeline::new();
1109        qp.add_shape(unit_aabb());
1110        qp.add_shape(shifted_aabb(5.0));
1111        qp.submit(PendingQuery::PointQuery {
1112            point: [0.5, 0.5, 0.5],
1113            n: 1,
1114        });
1115        qp.flush();
1116        assert_eq!(qp.results[0].hits.len(), 1);
1117        assert_eq!(qp.results[0].hits[0], 0);
1118    }
1119
1120    #[test]
1121    fn test_sweep_aabb_motion_positive() {
1122        let aabb = unit_aabb();
1123        let swept = sweep_aabb_motion(&aabb, [2.0, 0.0, 0.0]);
1124        assert!((swept.max[0] - 3.0).abs() < 1e-12);
1125        assert!((swept.min[0] - 0.0).abs() < 1e-12);
1126    }
1127
1128    #[test]
1129    fn test_sweep_aabb_motion_negative() {
1130        let aabb = unit_aabb();
1131        let swept = sweep_aabb_motion(&aabb, [-1.0, 0.0, 0.0]);
1132        assert!((swept.min[0] - (-1.0)).abs() < 1e-12);
1133    }
1134
1135    #[test]
1136    fn test_aabb_overlap_function() {
1137        let a = unit_aabb();
1138        let b = shifted_aabb(0.5);
1139        assert!(aabb_overlap(&a, &b));
1140    }
1141}