Skip to main content

oxiphysics_collision/broadphase/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::types::CollisionPair;
6use oxiphysics_core::Aabb;
7use oxiphysics_core::math::{Real, Vec3};
8
9use super::functions::{BroadPhase, aabb_in_frustum, inflate_aabb, ray_aabb_intersect};
10
11/// Returns true if `outer` fully contains `inner` (every point of inner is inside outer).
12#[inline]
13fn aabb_contains_aabb(outer: &Aabb, inner: &Aabb) -> bool {
14    outer.min.x <= inner.min.x
15        && outer.min.y <= inner.min.y
16        && outer.min.z <= inner.min.z
17        && inner.max.x <= outer.max.x
18        && inner.max.y <= outer.max.y
19        && inner.max.z <= outer.max.z
20}
21
22/// O(n^2) brute force broad phase for reference/debugging.
23pub struct BruteForceBroadPhase;
24/// Object type classification for pair-count histograms.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum ObjectType {
27    /// Static immovable object.
28    Static,
29    /// Dynamic simulated object.
30    Dynamic,
31    /// Kinematic (script-driven) object.
32    Kinematic,
33}
34/// A node in the dynamic BVH tree.
35#[derive(Debug, Clone)]
36pub(super) enum BvhNodeData {
37    /// Internal node with two children.
38    Internal { left: usize, right: usize },
39    /// Leaf node storing an object index.
40    Leaf { object_index: usize },
41}
42/// A hierarchical scene graph that accelerates broadphase by skipping
43/// subtrees whose root AABB does not overlap the query.
44#[derive(Debug, Default)]
45pub struct BroadphaseSceneGraph {
46    pub(super) nodes: Vec<SceneGraphNode>,
47}
48impl BroadphaseSceneGraph {
49    /// Create an empty scene graph.
50    pub fn new() -> Self {
51        Self { nodes: Vec::new() }
52    }
53    /// Add a node. Returns its index.
54    pub fn add_node(&mut self, aabb: Aabb, parent: Option<usize>) -> usize {
55        let idx = self.nodes.len();
56        if let Some(p) = parent
57            && p < self.nodes.len()
58        {
59            self.nodes[p].children.push(idx);
60        }
61        self.nodes.push(SceneGraphNode {
62            aabb,
63            parent,
64            children: Vec::new(),
65            active: true,
66        });
67        idx
68    }
69    /// Remove a node (marks inactive, does not compact).
70    pub fn remove_node(&mut self, idx: usize) {
71        if idx < self.nodes.len() {
72            self.nodes[idx].active = false;
73        }
74    }
75    /// Update the AABB for a node.
76    pub fn update_aabb(&mut self, idx: usize, aabb: Aabb) {
77        if idx < self.nodes.len() {
78            self.nodes[idx].aabb = aabb;
79        }
80    }
81    /// Number of active nodes.
82    pub fn node_count(&self) -> usize {
83        self.nodes.iter().filter(|n| n.active).count()
84    }
85    /// Returns the indices of children of `parent`.
86    pub fn children_of(&self, parent: usize) -> Vec<usize> {
87        if parent < self.nodes.len() {
88            self.nodes[parent]
89                .children
90                .iter()
91                .copied()
92                .filter(|&c| self.nodes.get(c).is_some_and(|n| n.active))
93                .collect()
94        } else {
95            Vec::new()
96        }
97    }
98    /// Collect all active AABBs with their original node indices.
99    fn active_aabbs(&self) -> (Vec<Aabb>, Vec<usize>) {
100        let mut aabbs = Vec::new();
101        let mut indices = Vec::new();
102        for (i, node) in self.nodes.iter().enumerate() {
103            if node.active {
104                aabbs.push(node.aabb.clone());
105                indices.push(i);
106            }
107        }
108        (aabbs, indices)
109    }
110    /// Find all overlapping pairs using a BVH.
111    pub fn find_pairs_bvh(&self) -> Vec<CollisionPair> {
112        let (aabbs, node_indices) = self.active_aabbs();
113        let mut bvh = BvhBroadphase::new();
114        bvh.build(&aabbs);
115        let raw_pairs = bvh.find_all_pairs(&aabbs);
116        raw_pairs
117            .iter()
118            .map(|p| {
119                let a = node_indices[p.a];
120                let b = node_indices[p.b];
121                if a < b {
122                    CollisionPair::new(a, b)
123                } else {
124                    CollisionPair::new(b, a)
125                }
126            })
127            .collect()
128    }
129    /// Query all active nodes overlapping the given AABB.
130    pub fn query_aabb(&self, query: &Aabb) -> Vec<usize> {
131        let (aabbs, node_indices) = self.active_aabbs();
132        let mut bvh = BvhBroadphase::new();
133        bvh.build(&aabbs);
134        bvh.query(query).iter().map(|&i| node_indices[i]).collect()
135    }
136}
137/// Instruments a broadphase query to collect timing information.
138#[derive(Debug, Default)]
139pub struct BroadphaseProfiler {
140    /// Elapsed nanoseconds for the last query.
141    pub(super) last_elapsed_ns: u64,
142    /// Total number of profiled calls.
143    pub(super) call_count: u64,
144    /// Total elapsed nanoseconds across all calls.
145    pub(super) total_elapsed_ns: u64,
146}
147impl BroadphaseProfiler {
148    /// Create a new profiler.
149    pub fn new() -> Self {
150        Self::default()
151    }
152    /// Run SAP broadphase and record timing.
153    pub fn profile_sap(&mut self, aabbs: &[Aabb], axis: usize) -> Vec<CollisionPair> {
154        let start = std::time::Instant::now();
155        let result = SweepAndPrune::new(axis).find_pairs(aabbs);
156        let elapsed = start.elapsed().as_nanos() as u64;
157        self.last_elapsed_ns = elapsed;
158        self.total_elapsed_ns += elapsed;
159        self.call_count += 1;
160        result
161    }
162    /// Run BVH broadphase and record timing.
163    pub fn profile_bvh(&mut self, aabbs: &[Aabb]) -> Vec<CollisionPair> {
164        let start = std::time::Instant::now();
165        let mut bvh = BvhBroadphase::new();
166        bvh.build(aabbs);
167        let result = bvh.find_all_pairs(aabbs);
168        let elapsed = start.elapsed().as_nanos() as u64;
169        self.last_elapsed_ns = elapsed;
170        self.total_elapsed_ns += elapsed;
171        self.call_count += 1;
172        result
173    }
174    /// Elapsed nanoseconds for the most recent profiled call.
175    pub fn last_elapsed_ns(&self) -> u64 {
176        self.last_elapsed_ns
177    }
178    /// Total number of calls profiled.
179    pub fn call_count(&self) -> u64 {
180        self.call_count
181    }
182    /// Average nanoseconds per call.
183    pub fn avg_elapsed_ns(&self) -> f64 {
184        if self.call_count == 0 {
185            0.0
186        } else {
187            self.total_elapsed_ns as f64 / self.call_count as f64
188        }
189    }
190    /// Reset all counters.
191    pub fn reset(&mut self) {
192        *self = Self::default();
193    }
194}
195/// A view frustum defined by 6 planes.
196///
197/// Each plane is stored as `(normal, offset)` where `normal · x = offset`
198/// defines the plane boundary.  The normal points inward (toward the
199/// visible region).
200#[derive(Debug, Clone)]
201pub struct Frustum {
202    /// Six frustum planes: `(inward_normal, offset)`.
203    pub planes: [(Vec3, Real); 6],
204}
205impl Frustum {
206    /// Create a frustum from six plane definitions.
207    pub fn new(planes: [(Vec3, Real); 6]) -> Self {
208        Self { planes }
209    }
210    /// Create a symmetric frustum given camera parameters.
211    ///
212    /// Camera looks along -Z.  Near plane at z = -near, far plane at z = -far.
213    /// Inward normals point toward the visible region.
214    pub fn from_perspective(fov_y_rad: Real, aspect: Real, near: Real, far: Real) -> Self {
215        let half_fov = fov_y_rad * 0.5;
216        let tan_half = half_fov.tan();
217        let near_plane = (Vec3::new(0.0, 0.0, -1.0), near);
218        let far_plane = (Vec3::new(0.0, 0.0, 1.0), -far);
219        let cos_fov = (1.0 / (1.0 + tan_half * tan_half)).sqrt();
220        let sin_fov = tan_half * cos_fov;
221        let top_plane = (Vec3::new(0.0, -cos_fov, -sin_fov), 0.0);
222        let bottom_plane = (Vec3::new(0.0, cos_fov, -sin_fov), 0.0);
223        let tan_half_x = tan_half * aspect;
224        let cos_fov_x = (1.0 / (1.0 + tan_half_x * tan_half_x)).sqrt();
225        let sin_fov_x = tan_half_x * cos_fov_x;
226        let left_plane = (Vec3::new(cos_fov_x, 0.0, -sin_fov_x), 0.0);
227        let right_plane = (Vec3::new(-cos_fov_x, 0.0, -sin_fov_x), 0.0);
228        Self {
229            planes: [
230                near_plane,
231                far_plane,
232                top_plane,
233                bottom_plane,
234                left_plane,
235                right_plane,
236            ],
237        }
238    }
239    /// Test whether an AABB is (potentially) inside the frustum.
240    ///
241    /// Returns `true` if the AABB is not fully outside any plane.
242    pub fn contains_aabb(&self, aabb: &Aabb) -> bool {
243        for &(ref normal, offset) in &self.planes {
244            let px = if normal.x >= 0.0 {
245                aabb.max.x
246            } else {
247                aabb.min.x
248            };
249            let py = if normal.y >= 0.0 {
250                aabb.max.y
251            } else {
252                aabb.min.y
253            };
254            let pz = if normal.z >= 0.0 {
255                aabb.max.z
256            } else {
257                aabb.min.z
258            };
259            let p = Vec3::new(px, py, pz);
260            if p.dot(normal) < offset {
261                return false;
262            }
263        }
264        true
265    }
266}
267/// Incrementally updated AABB tree (insertion/removal without full rebuild).
268///
269/// Each object gets a fattened AABB; the tree is only updated when the
270/// object moves outside its fat AABB.
271pub struct DynamicAabbTree {
272    /// Object AABBs (tight).
273    pub(super) tight_aabbs: Vec<Aabb>,
274    /// Fattened AABBs (tight + margin).
275    pub(super) fat_aabbs: Vec<Aabb>,
276    /// Fattening margin.
277    pub(super) margin: Real,
278    /// Internal BVH for queries (rebuilt on demand).
279    pub(super) bvh: BvhBroadphase,
280    /// Whether the tree needs rebuilding.
281    pub(super) dirty: bool,
282}
283impl DynamicAabbTree {
284    /// Create a new dynamic tree with the given fattening margin.
285    pub fn new(margin: Real) -> Self {
286        Self {
287            tight_aabbs: Vec::new(),
288            fat_aabbs: Vec::new(),
289            margin,
290            bvh: BvhBroadphase::new(),
291            dirty: true,
292        }
293    }
294    /// Insert a new object. Returns its index.
295    pub fn insert(&mut self, aabb: Aabb) -> usize {
296        let idx = self.tight_aabbs.len();
297        let fat = self.fatten(&aabb);
298        self.tight_aabbs.push(aabb);
299        self.fat_aabbs.push(fat);
300        self.dirty = true;
301        idx
302    }
303    /// Update the AABB for an existing object.
304    ///
305    /// Returns `true` if the internal tree was invalidated.
306    pub fn update(&mut self, index: usize, aabb: Aabb) -> bool {
307        self.tight_aabbs[index] = aabb.clone();
308        if !aabb_contains_aabb(&self.fat_aabbs[index], &aabb) {
309            self.fat_aabbs[index] = self.fatten(&aabb);
310            self.dirty = true;
311            return true;
312        }
313        false
314    }
315    /// Ensure the internal BVH is up to date.
316    pub fn rebuild_if_dirty(&mut self) {
317        if self.dirty {
318            self.bvh.build(&self.fat_aabbs);
319            self.dirty = false;
320        }
321    }
322    /// Query all overlapping pairs.
323    pub fn find_pairs(&mut self) -> Vec<CollisionPair> {
324        self.rebuild_if_dirty();
325        let mut pairs = Vec::new();
326        for i in 0..self.tight_aabbs.len() {
327            let hits = self.bvh.query(&self.tight_aabbs[i]);
328            for j in hits {
329                if j > i && self.tight_aabbs[i].intersects(&self.tight_aabbs[j]) {
330                    pairs.push(CollisionPair::new(i, j));
331                }
332            }
333        }
334        pairs
335    }
336    /// Query all objects overlapping a given AABB.
337    pub fn query(&mut self, aabb: &Aabb) -> Vec<usize> {
338        self.rebuild_if_dirty();
339        self.bvh.query(aabb)
340    }
341    /// Number of objects.
342    pub fn len(&self) -> usize {
343        self.tight_aabbs.len()
344    }
345    /// Whether the tree is empty.
346    pub fn is_empty(&self) -> bool {
347        self.tight_aabbs.is_empty()
348    }
349    fn fatten(&self, aabb: &Aabb) -> Aabb {
350        let m = Vec3::new(self.margin, self.margin, self.margin);
351        Aabb::new(aabb.min - m, aabb.max + m)
352    }
353}
354impl DynamicAabbTree {
355    /// Mark the tree as dirty so it will be rebuilt on the next query.
356    pub fn mark_dirty(&mut self) {
357        self.dirty = true;
358    }
359    /// Returns `true` if the tree needs rebuilding.
360    pub fn is_dirty(&self) -> bool {
361        self.dirty
362    }
363    /// Return all pairs whose AABBs overlap when *inflated* by `inflation`.
364    ///
365    /// This is a proximity query: objects within `inflation` of each other
366    /// are included even if their tight AABBs do not touch.
367    pub fn find_proximity_pairs(&mut self, inflation: Real) -> Vec<CollisionPair> {
368        self.rebuild_if_dirty();
369        let n = self.tight_aabbs.len();
370        let mut pairs = Vec::new();
371        for i in 0..n {
372            let inflated = inflate_aabb(&self.tight_aabbs[i], inflation);
373            let hits = self.bvh.query(&inflated);
374            for j in hits {
375                if j > i {
376                    let inflated_j = inflate_aabb(&self.tight_aabbs[j], inflation);
377                    if inflated.intersects(&inflated_j) {
378                        pairs.push(CollisionPair::new(i, j));
379                    }
380                }
381            }
382        }
383        pairs
384    }
385    /// Batch-insert many AABBs at once (more efficient than repeated `insert`).
386    ///
387    /// Appends all AABBs and marks the tree dirty; call `rebuild_if_dirty`
388    /// (or any query method) to make the tree consistent.
389    pub fn batch_insert(&mut self, aabbs: &[Aabb]) {
390        for aabb in aabbs {
391            let fat = self.fatten(aabb);
392            self.tight_aabbs.push(aabb.clone());
393            self.fat_aabbs.push(fat);
394        }
395        self.dirty = true;
396    }
397    /// Validate parent/child link consistency in the underlying BVH.
398    ///
399    /// Calls `rebuild_if_dirty` first, then delegates to BvhBroadphase::validate.
400    pub fn validate(&mut self) -> bool {
401        self.rebuild_if_dirty();
402        self.bvh.validate()
403    }
404    /// Return tree statistics: (total objects, is_dirty).
405    pub fn tree_info(&self) -> (usize, bool) {
406        (self.tight_aabbs.len(), self.dirty)
407    }
408    /// Remove all objects and reset the tree.
409    pub fn clear(&mut self) {
410        self.tight_aabbs.clear();
411        self.fat_aabbs.clear();
412        self.bvh = BvhBroadphase::new();
413        self.dirty = false;
414    }
415}
416/// Hints for GPU-accelerated broad-phase.
417///
418/// These values can be used to configure a GPU compute shader for
419/// uniform-grid or sort-based broadphase.
420#[derive(Debug, Clone)]
421pub struct GpuBroadphaseHints {
422    /// Number of objects in the scene.
423    pub n_objects: usize,
424    /// Scene AABB minimum corner.
425    pub scene_min: [Real; 3],
426    /// Scene AABB maximum corner.
427    pub scene_max: [Real; 3],
428    /// Average AABB half-extent across all objects.
429    pub avg_half_extent: Real,
430    /// Maximum AABB half-extent.
431    pub max_half_extent: Real,
432    /// Recommended number of GPU threads.
433    pub recommended_threads: usize,
434}
435impl GpuBroadphaseHints {
436    /// Compute GPU hints from a slice of AABBs.
437    pub fn from_aabbs(aabbs: &[Aabb]) -> Self {
438        if aabbs.is_empty() {
439            return Self {
440                n_objects: 0,
441                scene_min: [0.0; 3],
442                scene_max: [0.0; 3],
443                avg_half_extent: 0.0,
444                max_half_extent: 0.0,
445                recommended_threads: 64,
446            };
447        }
448        let mut scene_min = [Real::MAX; 3];
449        let mut scene_max = [Real::MIN; 3];
450        let mut sum_extent = 0.0_f64;
451        let mut max_extent = 0.0_f64;
452        for aabb in aabbs {
453            for i in 0..3 {
454                if aabb.min[i] < scene_min[i] {
455                    scene_min[i] = aabb.min[i];
456                }
457                if aabb.max[i] > scene_max[i] {
458                    scene_max[i] = aabb.max[i];
459                }
460            }
461            let half = aabb.half_extents();
462            let extent = half.x.max(half.y).max(half.z);
463            sum_extent += extent;
464            if extent > max_extent {
465                max_extent = extent;
466            }
467        }
468        let avg_half_extent = sum_extent / aabbs.len() as f64;
469        let recommended_threads = aabbs.len().next_power_of_two().max(64);
470        Self {
471            n_objects: aabbs.len(),
472            scene_min,
473            scene_max,
474            avg_half_extent,
475            max_half_extent: max_extent,
476            recommended_threads,
477        }
478    }
479    /// Suggested uniform-grid cell size (2× average extent for good fill).
480    pub fn suggested_cell_size(&self) -> Real {
481        if self.n_objects == 0 {
482            return 1.0;
483        }
484        (self.avg_half_extent * 2.0).max(1e-6)
485    }
486    /// Estimated number of non-empty grid cells.
487    pub fn estimated_cells(&self) -> usize {
488        if self.n_objects == 0 {
489            return 0;
490        }
491        let cell = self.suggested_cell_size();
492        let mut count = 1usize;
493        for i in 0..3 {
494            let extent = (self.scene_max[i] - self.scene_min[i]).max(0.0);
495            count *= ((extent / cell).ceil() as usize).max(1);
496        }
497        count
498    }
499    /// Return flat array of AABB data suitable for GPU upload:
500    /// `[min_x, min_y, min_z, max_x, max_y, max_z]` per object.
501    pub fn to_flat_buffer(aabbs: &[Aabb]) -> Vec<f32> {
502        let mut buf = Vec::with_capacity(aabbs.len() * 6);
503        for aabb in aabbs {
504            buf.push(aabb.min.x as f32);
505            buf.push(aabb.min.y as f32);
506            buf.push(aabb.min.z as f32);
507            buf.push(aabb.max.x as f32);
508            buf.push(aabb.max.y as f32);
509            buf.push(aabb.max.z as f32);
510        }
511        buf
512    }
513}
514/// Quality metrics for a static BVH.
515#[derive(Debug, Clone, Default)]
516pub struct BvhQualityMetrics {
517    /// SAH cost normalised by root surface area.
518    pub sah_cost: f64,
519    /// Maximum depth of any leaf.
520    pub max_depth: usize,
521    /// Average depth of leaves.
522    pub avg_leaf_depth: f64,
523    /// Number of leaf nodes.
524    pub leaf_count: usize,
525    /// Number of internal nodes.
526    pub internal_count: usize,
527}
528/// A node in the dynamic BVH tree.
529#[derive(Debug, Clone)]
530pub(super) struct BvhNode {
531    pub(super) aabb: Aabb,
532    pub(super) parent: Option<usize>,
533    pub(super) data: BvhNodeData,
534}
535/// Statistics from a broad-phase query.
536#[derive(Debug, Clone, Default)]
537pub struct BroadphaseStats {
538    /// Number of objects.
539    pub num_objects: usize,
540    /// Number of candidate pairs found.
541    pub num_pairs: usize,
542    /// Number of AABB-AABB tests performed (estimated).
543    pub num_tests: usize,
544}
545/// A node in the broadphase scene graph.
546#[derive(Debug, Clone)]
547pub struct SceneGraphNode {
548    /// World-space AABB for this node.
549    pub aabb: Aabb,
550    /// Optional parent node index.
551    pub parent: Option<usize>,
552    /// Child node indices.
553    pub children: Vec<usize>,
554    /// Whether this node is active (not removed).
555    pub active: bool,
556}
557/// Histogram entry: how many collision pairs exist between two object types.
558#[derive(Debug, Clone, Default)]
559pub struct PairCountHistogram {
560    /// Static vs Static pairs.
561    pub static_static: usize,
562    /// Static vs Dynamic pairs.
563    pub static_dynamic: usize,
564    /// Static vs Kinematic pairs.
565    pub static_kinematic: usize,
566    /// Dynamic vs Dynamic pairs.
567    pub dynamic_dynamic: usize,
568    /// Dynamic vs Kinematic pairs.
569    pub dynamic_kinematic: usize,
570    /// Kinematic vs Kinematic pairs.
571    pub kinematic_kinematic: usize,
572}
573impl PairCountHistogram {
574    /// Total pair count across all categories.
575    pub fn total(&self) -> usize {
576        self.static_static
577            + self.static_dynamic
578            + self.static_kinematic
579            + self.dynamic_dynamic
580            + self.dynamic_kinematic
581            + self.kinematic_kinematic
582    }
583}
584/// Top-down Bounding Volume Hierarchy for efficient broad-phase queries.
585///
586/// This implementation uses a static top-down build and is best used by
587/// calling [`BvhBroadphase::build`] once per frame or after bulk updates.
588/// For incremental updates, see [`crate::DynamicBvh`].
589pub struct BvhBroadphase {
590    pub(super) nodes: Vec<BvhNode>,
591    pub(super) root: Option<usize>,
592    /// Maps object index -> node index in the tree.
593    pub(super) object_to_node: Vec<Option<usize>>,
594}
595impl BvhBroadphase {
596    /// Create a new empty BVH.
597    pub fn new() -> Self {
598        Self {
599            nodes: Vec::new(),
600            root: None,
601            object_to_node: Vec::new(),
602        }
603    }
604    /// Build the BVH from a set of AABBs using top-down construction.
605    pub fn build(&mut self, aabbs: &[Aabb]) {
606        self.nodes.clear();
607        self.object_to_node.clear();
608        self.object_to_node.resize(aabbs.len(), None);
609        if aabbs.is_empty() {
610            self.root = None;
611            return;
612        }
613        let indices: Vec<usize> = (0..aabbs.len()).collect();
614        self.root = Some(self.build_recursive(aabbs, &indices));
615    }
616    fn build_recursive(&mut self, aabbs: &[Aabb], indices: &[usize]) -> usize {
617        if indices.len() == 1 {
618            let idx = indices[0];
619            let node_idx = self.nodes.len();
620            self.nodes.push(BvhNode {
621                aabb: aabbs[idx].clone(),
622                parent: None,
623                data: BvhNodeData::Leaf { object_index: idx },
624            });
625            self.object_to_node[idx] = Some(node_idx);
626            return node_idx;
627        }
628        let mut combined = aabbs[indices[0]].clone();
629        for &i in &indices[1..] {
630            combined = combined.merge(&aabbs[i]);
631        }
632        let extent = combined.half_extents();
633        let split_axis = if extent.x >= extent.y && extent.x >= extent.z {
634            0
635        } else if extent.y >= extent.z {
636            1
637        } else {
638            2
639        };
640        let mut sorted = indices.to_vec();
641        sorted.sort_by(|&a, &b| {
642            aabbs[a].center()[split_axis]
643                .partial_cmp(&aabbs[b].center()[split_axis])
644                .unwrap_or(std::cmp::Ordering::Equal)
645        });
646        let mid = sorted.len() / 2;
647        let left = self.build_recursive(aabbs, &sorted[..mid]);
648        let right = self.build_recursive(aabbs, &sorted[mid..]);
649        let node_idx = self.nodes.len();
650        self.nodes.push(BvhNode {
651            aabb: combined,
652            parent: None,
653            data: BvhNodeData::Internal { left, right },
654        });
655        self.nodes[left].parent = Some(node_idx);
656        self.nodes[right].parent = Some(node_idx);
657        node_idx
658    }
659    /// Query all pairs of overlapping AABBs.
660    pub fn find_all_pairs(&self, aabbs: &[Aabb]) -> Vec<CollisionPair> {
661        let mut pairs = Vec::new();
662        if self.root.is_none() {
663            return pairs;
664        }
665        let leaves: Vec<(usize, usize)> = self
666            .nodes
667            .iter()
668            .enumerate()
669            .filter_map(|(node_idx, node)| {
670                if let BvhNodeData::Leaf { object_index } = node.data {
671                    Some((node_idx, object_index))
672                } else {
673                    None
674                }
675            })
676            .collect();
677        for (i, &(_, obj_a)) in leaves.iter().enumerate() {
678            for &(_, obj_b) in &leaves[(i + 1)..] {
679                if aabbs[obj_a].intersects(&aabbs[obj_b]) {
680                    pairs.push(CollisionPair::new(obj_a, obj_b));
681                }
682            }
683        }
684        pairs
685    }
686    /// Query all objects whose AABB overlaps the given AABB.
687    pub fn query(&self, aabb: &Aabb) -> Vec<usize> {
688        let mut results = Vec::new();
689        if let Some(root) = self.root {
690            self.query_recursive(root, aabb, &mut results);
691        }
692        results
693    }
694    fn query_recursive(&self, node_idx: usize, aabb: &Aabb, results: &mut Vec<usize>) {
695        let node = &self.nodes[node_idx];
696        if !node.aabb.intersects(aabb) {
697            return;
698        }
699        match node.data {
700            BvhNodeData::Leaf { object_index } => {
701                results.push(object_index);
702            }
703            BvhNodeData::Internal { left, right } => {
704                self.query_recursive(left, aabb, results);
705                self.query_recursive(right, aabb, results);
706            }
707        }
708    }
709    /// Ray query: find all objects whose AABB is hit by the ray.
710    pub fn ray_query(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Vec<usize> {
711        let mut results = Vec::new();
712        if let Some(root) = self.root {
713            self.ray_query_recursive(root, ray_origin, ray_direction, max_toi, &mut results);
714        }
715        results
716    }
717    fn ray_query_recursive(
718        &self,
719        node_idx: usize,
720        ray_origin: &Vec3,
721        ray_direction: &Vec3,
722        max_toi: Real,
723        results: &mut Vec<usize>,
724    ) {
725        let node = &self.nodes[node_idx];
726        if !ray_aabb_intersect(ray_origin, ray_direction, &node.aabb, max_toi) {
727            return;
728        }
729        match node.data {
730            BvhNodeData::Leaf { object_index } => {
731                results.push(object_index);
732            }
733            BvhNodeData::Internal { left, right } => {
734                self.ray_query_recursive(left, ray_origin, ray_direction, max_toi, results);
735                self.ray_query_recursive(right, ray_origin, ray_direction, max_toi, results);
736            }
737        }
738    }
739}
740impl BvhBroadphase {
741    /// Compute quality metrics for this BVH.
742    ///
743    /// Returns `None` if the tree is empty.
744    pub fn quality_metrics(&self) -> Option<BvhQualityMetrics> {
745        let root = self.root?;
746        let root_sa = self.nodes[root].aabb.surface_area();
747        if root_sa <= 0.0 {
748            return None;
749        }
750        let mut sah = 0.0_f64;
751        let mut max_depth = 0usize;
752        let mut total_leaf_depth = 0usize;
753        let mut leaf_count = 0usize;
754        let mut internal_count = 0usize;
755        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
756        while let Some((idx, depth)) = stack.pop() {
757            if depth > max_depth {
758                max_depth = depth;
759            }
760            match self.nodes[idx].data {
761                BvhNodeData::Leaf { .. } => {
762                    leaf_count += 1;
763                    total_leaf_depth += depth;
764                }
765                BvhNodeData::Internal { left, right } => {
766                    internal_count += 1;
767                    sah += self.nodes[idx].aabb.surface_area() / root_sa;
768                    stack.push((left, depth + 1));
769                    stack.push((right, depth + 1));
770                }
771            }
772        }
773        let avg_leaf_depth = if leaf_count > 0 {
774            total_leaf_depth as f64 / leaf_count as f64
775        } else {
776            0.0
777        };
778        Some(BvhQualityMetrics {
779            sah_cost: sah,
780            max_depth,
781            avg_leaf_depth,
782            leaf_count,
783            internal_count,
784        })
785    }
786    /// Rebuild the BVH from `aabbs` if the SAH cost exceeds `threshold`.
787    ///
788    /// Returns `true` if a rebuild was triggered.
789    pub fn rebuild_if_degraded(&mut self, aabbs: &[Aabb], threshold: f64) -> bool {
790        let degraded = match self.quality_metrics() {
791            Some(q) => q.sah_cost > threshold,
792            None => false,
793        };
794        if degraded {
795            self.build(aabbs);
796        }
797        degraded
798    }
799    /// SAH cost normalised by root surface area, or `0.0` for an empty tree.
800    pub fn sah_cost(&self) -> f64 {
801        self.quality_metrics().map(|q| q.sah_cost).unwrap_or(0.0)
802    }
803    /// Validate all parent/child links and AABB containment.
804    ///
805    /// Returns `true` if the tree is consistent.
806    pub fn validate(&self) -> bool {
807        match self.root {
808            None => true,
809            Some(root) => {
810                if self.nodes[root].parent.is_some() {
811                    return false;
812                }
813                self.validate_node(root)
814            }
815        }
816    }
817    fn validate_node(&self, idx: usize) -> bool {
818        match self.nodes[idx].data {
819            BvhNodeData::Leaf { .. } => true,
820            BvhNodeData::Internal { left, right } => {
821                if self.nodes[left].parent != Some(idx) {
822                    return false;
823                }
824                if self.nodes[right].parent != Some(idx) {
825                    return false;
826                }
827                if !aabb_contains_aabb(&self.nodes[idx].aabb, &self.nodes[left].aabb) {
828                    return false;
829                }
830                if !aabb_contains_aabb(&self.nodes[idx].aabb, &self.nodes[right].aabb) {
831                    return false;
832                }
833                self.validate_node(left) && self.validate_node(right)
834            }
835        }
836    }
837    /// Stack-based ray traversal against this BVH; returns hit object indices.
838    ///
839    /// Equivalent to `ray_query` but uses an explicit stack instead of recursion.
840    pub fn ray_query_iterative(
841        &self,
842        ray_origin: &Vec3,
843        ray_direction: &Vec3,
844        max_toi: Real,
845    ) -> Vec<usize> {
846        let mut results = Vec::new();
847        let Some(root) = self.root else {
848            return results;
849        };
850        let mut stack = vec![root];
851        while let Some(idx) = stack.pop() {
852            let node = &self.nodes[idx];
853            if !ray_aabb_intersect(ray_origin, ray_direction, &node.aabb, max_toi) {
854                continue;
855            }
856            match node.data {
857                BvhNodeData::Leaf { object_index } => results.push(object_index),
858                BvhNodeData::Internal { left, right } => {
859                    stack.push(left);
860                    stack.push(right);
861                }
862            }
863        }
864        results
865    }
866    /// Frustum culling against this BVH using a stack-based traversal.
867    ///
868    /// `planes`: six `(inward_normal, offset)` pairs.
869    /// Returns object indices of leaves that are potentially inside the frustum.
870    pub fn frustum_query_iterative(&self, planes: &[(Vec3, Real); 6]) -> Vec<usize> {
871        let mut results = Vec::new();
872        let Some(root) = self.root else {
873            return results;
874        };
875        let mut stack = vec![root];
876        while let Some(idx) = stack.pop() {
877            let node = &self.nodes[idx];
878            if !aabb_in_frustum(&node.aabb, planes) {
879                continue;
880            }
881            match node.data {
882                BvhNodeData::Leaf { object_index } => results.push(object_index),
883                BvhNodeData::Internal { left, right } => {
884                    stack.push(left);
885                    stack.push(right);
886                }
887            }
888        }
889        results
890    }
891    /// Batch insert a slice of AABBs and rebuild.
892    ///
893    /// Clears any existing tree and builds from the provided `aabbs`.
894    pub fn batch_insert(&mut self, aabbs: &[Aabb]) {
895        self.build(aabbs);
896    }
897}
898/// Sweep and Prune (SAP) broad phase on a single axis.
899pub struct SweepAndPrune {
900    pub(super) axis: usize,
901}
902impl SweepAndPrune {
903    /// Create a new SAP on the given axis (0=X, 1=Y, 2=Z).
904    pub fn new(axis: usize) -> Self {
905        assert!(axis < 3, "axis must be 0, 1, or 2");
906        Self { axis }
907    }
908    /// Create SAP on X axis.
909    pub fn x_axis() -> Self {
910        Self::new(0)
911    }
912}
913/// Cache of collision pairs from the previous frame to speed up the
914/// next broadphase query (warm starting).
915///
916/// On each frame:
917/// 1. Filter cached pairs to those still overlapping.
918/// 2. Merge with newly detected pairs.
919/// 3. Update the cache with the merged set.
920#[derive(Debug, Default)]
921pub struct BroadphaseWarmstart {
922    pub(super) cached_pairs: Vec<CollisionPair>,
923}
924impl BroadphaseWarmstart {
925    /// Create a new empty warmstart cache.
926    pub fn new() -> Self {
927        Self::default()
928    }
929    /// Update the cache with the pairs from the current frame.
930    pub fn update(&mut self, pairs: &[CollisionPair]) {
931        self.cached_pairs = pairs.to_vec();
932    }
933    /// Number of pairs in the cache.
934    pub fn cached_pair_count(&self) -> usize {
935        self.cached_pairs.len()
936    }
937    /// Filter cached pairs, retaining only those whose AABBs still overlap.
938    pub fn filter_still_overlapping(&self, aabbs: &[Aabb]) -> Vec<CollisionPair> {
939        self.cached_pairs
940            .iter()
941            .filter(|p| {
942                let max_idx = aabbs.len();
943                p.a < max_idx && p.b < max_idx && aabbs[p.a].intersects(&aabbs[p.b])
944            })
945            .copied()
946            .collect()
947    }
948    /// Merge cached pairs that are still overlapping with fresh new pairs.
949    ///
950    /// Deduplicates the output.
951    pub fn merge_with_new(
952        &self,
953        new_pairs: &[CollisionPair],
954        aabbs: &[Aabb],
955    ) -> Vec<CollisionPair> {
956        let mut merged: Vec<CollisionPair> = self.filter_still_overlapping(aabbs);
957        for &p in new_pairs {
958            if !merged.iter().any(|e| e.a == p.a && e.b == p.b) {
959                merged.push(p);
960            }
961        }
962        merged
963    }
964    /// Clear the cache.
965    pub fn clear(&mut self) {
966        self.cached_pairs.clear();
967    }
968    /// Run a full broadphase using SAP with warmstart.
969    ///
970    /// Returns the merged pair set (warm + fresh) and updates the cache.
971    pub fn broadphase_with_warmstart(&mut self, aabbs: &[Aabb], axis: usize) -> Vec<CollisionPair> {
972        let fresh = SweepAndPrune::new(axis).find_pairs(aabbs);
973        let merged = self.merge_with_new(&fresh, aabbs);
974        self.update(&merged);
975        merged
976    }
977}