Skip to main content

oxiphysics_collision/sap/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{HashMap, HashSet};
6
7use super::functions::{aabb3_overlaps, aabb3_point_dist_sq, axis_is_sorted};
8
9/// An endpoint on one axis used by the Sweep-and-Prune broadphase.
10#[derive(Debug, Clone)]
11pub struct SapEndpoint {
12    /// The coordinate value of this endpoint.
13    pub value: f64,
14    /// Which object this endpoint belongs to.
15    pub object_id: u64,
16    /// `true` for the minimum (left) endpoint, `false` for the maximum (right).
17    pub is_min: bool,
18}
19/// Extended SAP that tracks statistics.
20pub struct StatTrackingSap {
21    /// Inner SAP structure.
22    pub inner: IncrementalSap,
23    /// Statistics from the last query.
24    pub stats: SapStats,
25}
26impl StatTrackingSap {
27    /// Create a new statistics-tracking SAP.
28    pub fn new() -> Self {
29        Self {
30            inner: IncrementalSap::new(),
31            stats: SapStats::default(),
32        }
33    }
34    /// Insert a body.
35    pub fn insert(&mut self, id: u32, aabb: Aabb3) {
36        self.inner.insert(id, aabb);
37    }
38    /// Remove a body.
39    pub fn remove(&mut self, id: u32) {
40        self.inner.remove(id);
41    }
42    /// Update a body's AABB.
43    pub fn update(&mut self, id: u32, aabb: Aabb3) {
44        self.inner.update(id, aabb);
45    }
46    /// Query all overlapping pairs, recording statistics.
47    pub fn query_pairs(&mut self) -> Vec<(u32, u32)> {
48        let n_endpoints = self.inner.endpoints_x.len();
49        let pairs = self.inner.query_pairs();
50        self.stats = SapStats {
51            pair_count: pairs.len(),
52            sweep_count: n_endpoints,
53            body_count: self.inner.body_count(),
54        };
55        pairs
56    }
57}
58/// An axis-aligned bounding box stored inside the SAP structure.
59#[derive(Debug, Clone)]
60pub struct SapObject {
61    /// Unique identifier for this object.
62    pub id: u64,
63    /// Minimum corner `[x, y, z]`.
64    pub min: [f64; 3],
65    /// Maximum corner `[x, y, z]`.
66    pub max: [f64; 3],
67}
68/// Result of a single broadphase step.
69#[derive(Debug, Clone)]
70pub struct BroadphaseResult {
71    /// All currently overlapping pairs.
72    pub pairs: Vec<(u32, u32)>,
73    /// New pairs this frame (begin events).
74    pub new_pairs: Vec<(u32, u32)>,
75    /// Pairs that ended this frame (end events).
76    pub lost_pairs: Vec<(u32, u32)>,
77    /// Statistics for this step.
78    pub stats: SapStats,
79}
80/// An endpoint on one axis used by the incremental multi-axis SAP.
81#[derive(Debug, Clone)]
82pub struct SapEndpointU32 {
83    /// The coordinate value of this endpoint.
84    pub value: f64,
85    /// Which body this endpoint belongs to.
86    pub body_id: u32,
87    /// `true` for the minimum (left) endpoint, `false` for the maximum (right).
88    pub is_min: bool,
89}
90/// Uniform-grid broadphase that hashes objects into fixed-size cells.
91pub struct GridBroadphase {
92    /// Side length of each cubic cell.
93    pub cell_size: f64,
94    /// Map from cell coordinate to the list of object ids in that cell.
95    pub cells: HashMap<(i32, i32, i32), Vec<u64>>,
96}
97impl GridBroadphase {
98    /// Create a new grid with the given cell size.
99    pub fn new(cell_size: f64) -> Self {
100        Self {
101            cell_size,
102            cells: HashMap::new(),
103        }
104    }
105    /// Insert an object into every grid cell that its AABB overlaps.
106    pub fn insert(&mut self, id: u64, min: [f64; 3], max: [f64; 3]) {
107        let min_cx = self.cell_coord(min[0]);
108        let min_cy = self.cell_coord(min[1]);
109        let min_cz = self.cell_coord(min[2]);
110        let max_cx = self.cell_coord(max[0]);
111        let max_cy = self.cell_coord(max[1]);
112        let max_cz = self.cell_coord(max[2]);
113        for cx in min_cx..=max_cx {
114            for cy in min_cy..=max_cy {
115                for cz in min_cz..=max_cz {
116                    self.cells.entry((cx, cy, cz)).or_default().push(id);
117                }
118            }
119        }
120    }
121    /// Convert a world-space coordinate to its cell index.
122    #[inline]
123    pub fn cell_coord(&self, pos: f64) -> i32 {
124        (pos / self.cell_size).floor() as i32
125    }
126    /// Return all object-id pairs that share at least one cell (conservative).
127    pub fn query_potential_pairs(&self) -> Vec<(u64, u64)> {
128        let mut pairs: Vec<(u64, u64)> = Vec::new();
129        for ids in self.cells.values() {
130            for i in 0..ids.len() {
131                for j in (i + 1)..ids.len() {
132                    let (lo, hi) = if ids[i] < ids[j] {
133                        (ids[i], ids[j])
134                    } else {
135                        (ids[j], ids[i])
136                    };
137                    pairs.push((lo, hi));
138                }
139            }
140        }
141        pairs.sort_unstable();
142        pairs.dedup();
143        pairs
144    }
145    /// Remove all objects from all cells.
146    pub fn clear(&mut self) {
147        self.cells.clear();
148    }
149}
150impl GridBroadphase {
151    /// Return all object ids in the cell that contains world-space point `p`.
152    pub fn query_point(&self, p: [f64; 3]) -> Vec<u64> {
153        let key = (
154            self.cell_coord(p[0]),
155            self.cell_coord(p[1]),
156            self.cell_coord(p[2]),
157        );
158        self.cells.get(&key).cloned().unwrap_or_default()
159    }
160    /// Return total number of object-cell entries (not unique objects).
161    pub fn total_entries(&self) -> usize {
162        self.cells.values().map(|v| v.len()).sum()
163    }
164    /// Return the number of occupied cells.
165    pub fn occupied_cells(&self) -> usize {
166        self.cells.len()
167    }
168    /// Remove a specific object id from all cells it occupies.
169    ///
170    /// Scans all cells — `O(cells * max_per_cell)`.  For high-density scenes
171    /// prefer `clear` + bulk re-insert.
172    pub fn remove(&mut self, id: u64) {
173        for ids in self.cells.values_mut() {
174            ids.retain(|&x| x != id);
175        }
176        self.cells.retain(|_, v| !v.is_empty());
177    }
178    /// Return a conservative over-approximation of all objects that could
179    /// overlap `aabb` by checking every cell the AABB touches.
180    pub fn query_aabb(&self, min: [f64; 3], max: [f64; 3]) -> Vec<u64> {
181        let min_cx = self.cell_coord(min[0]);
182        let min_cy = self.cell_coord(min[1]);
183        let min_cz = self.cell_coord(min[2]);
184        let max_cx = self.cell_coord(max[0]);
185        let max_cy = self.cell_coord(max[1]);
186        let max_cz = self.cell_coord(max[2]);
187        let mut result = Vec::new();
188        for cx in min_cx..=max_cx {
189            for cy in min_cy..=max_cy {
190                for cz in min_cz..=max_cz {
191                    if let Some(ids) = self.cells.get(&(cx, cy, cz)) {
192                        for &id in ids {
193                            if !result.contains(&id) {
194                                result.push(id);
195                            }
196                        }
197                    }
198                }
199            }
200        }
201        result.sort_unstable();
202        result
203    }
204}
205/// A sorted endpoint array for one axis used by the incremental SAP.
206///
207/// Maintains a sorted list of `SapEndpointU32` entries and provides
208/// insertion-sort for small moves.
209#[derive(Debug, Clone, Default)]
210pub struct SapAxis {
211    /// Sorted endpoints on this axis.
212    pub endpoints: Vec<SapEndpointU32>,
213}
214impl SapAxis {
215    /// Create an empty axis.
216    pub fn new() -> Self {
217        Self {
218            endpoints: Vec::new(),
219        }
220    }
221    /// Insert min and max endpoints for `body_id` with the given values.
222    pub fn insert(&mut self, body_id: u32, min_val: f64, max_val: f64) {
223        self.endpoints.push(SapEndpointU32 {
224            value: min_val,
225            body_id,
226            is_min: true,
227        });
228        self.endpoints.push(SapEndpointU32 {
229            value: max_val,
230            body_id,
231            is_min: false,
232        });
233        let n = self.endpoints.len();
234        for start in [n - 2, n - 1] {
235            let mut i = start;
236            while i > 0 && self.endpoints[i - 1].value > self.endpoints[i].value {
237                self.endpoints.swap(i - 1, i);
238                i -= 1;
239            }
240        }
241    }
242    /// Remove all endpoints belonging to `body_id`.
243    pub fn remove(&mut self, body_id: u32) {
244        self.endpoints.retain(|e| e.body_id != body_id);
245    }
246    /// Update the endpoints for `body_id` using insertion sort (efficient for small moves).
247    pub fn update(&mut self, body_id: u32, min_val: f64, max_val: f64) {
248        for ep in self.endpoints.iter_mut() {
249            if ep.body_id == body_id {
250                ep.value = if ep.is_min { min_val } else { max_val };
251            }
252        }
253        let n = self.endpoints.len();
254        for i in 1..n {
255            let mut j = i;
256            while j > 0 && self.endpoints[j - 1].value > self.endpoints[j].value {
257                self.endpoints.swap(j - 1, j);
258                j -= 1;
259            }
260        }
261    }
262    /// Sweep the sorted endpoints and return overlapping pairs on this axis.
263    pub fn overlapping_pairs(&self) -> HashSet<(u32, u32)> {
264        let mut pairs = HashSet::new();
265        let mut active: Vec<u32> = Vec::new();
266        for ep in &self.endpoints {
267            if ep.is_min {
268                for &aid in &active {
269                    let key = if ep.body_id < aid {
270                        (ep.body_id, aid)
271                    } else {
272                        (aid, ep.body_id)
273                    };
274                    pairs.insert(key);
275                }
276                active.push(ep.body_id);
277            } else {
278                active.retain(|&id| id != ep.body_id);
279            }
280        }
281        pairs
282    }
283}
284impl SapAxis {
285    /// Return `true` if the endpoint list is sorted.
286    pub fn is_sorted(&self) -> bool {
287        axis_is_sorted(&self.endpoints)
288    }
289    /// Return the number of distinct bodies tracked on this axis.
290    pub fn body_count(&self) -> usize {
291        self.endpoints.iter().filter(|e| e.is_min).count()
292    }
293    /// Return the minimum value across all endpoints.
294    pub fn min_value(&self) -> Option<f64> {
295        self.endpoints.first().map(|e| e.value)
296    }
297    /// Return the maximum value across all endpoints.
298    pub fn max_value(&self) -> Option<f64> {
299        self.endpoints.last().map(|e| e.value)
300    }
301    /// Return all body ids currently tracked on this axis.
302    pub fn tracked_bodies(&self) -> Vec<u32> {
303        let mut ids: Vec<u32> = self
304            .endpoints
305            .iter()
306            .filter(|e| e.is_min)
307            .map(|e| e.body_id)
308            .collect();
309        ids.sort_unstable();
310        ids.dedup();
311        ids
312    }
313}
314/// A snapshot of an `IncrementalSap` that can be restored cheaply.
315#[derive(Debug, Clone)]
316pub struct SapSnapshot {
317    pub(super) aabbs: HashMap<u32, Aabb3>,
318}
319impl SapSnapshot {
320    /// Capture the current AABB map from `sap`.
321    pub fn capture(sap: &IncrementalSap) -> Self {
322        Self {
323            aabbs: sap.aabbs.clone(),
324        }
325    }
326    /// Restore `sap` to the state recorded in this snapshot.
327    ///
328    /// Bodies added after the snapshot are removed; bodies removed after the
329    /// snapshot are re-added; changed AABBs are reverted.
330    pub fn restore(self, sap: &mut IncrementalSap) {
331        let current_ids: Vec<u32> = sap.aabbs.keys().copied().collect();
332        for id in &current_ids {
333            if !self.aabbs.contains_key(id) {
334                sap.remove(*id);
335            }
336        }
337        for (id, aabb) in self.aabbs {
338            if sap.aabbs.contains_key(&id) {
339                sap.update(id, aabb);
340            } else {
341                sap.insert(id, aabb);
342            }
343        }
344    }
345}
346/// An overlap event: a pair has just started or stopped overlapping.
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub enum OverlapEvent {
349    /// Bodies `(a, b)` began overlapping this frame.
350    Begin(u32, u32),
351    /// Bodies `(a, b)` stopped overlapping this frame.
352    End(u32, u32),
353}
354/// A simple AABB for the incremental SAP (raw f64 arrays).
355#[derive(Debug, Clone)]
356pub struct Aabb3 {
357    /// Minimum corner `[x, y, z]`.
358    pub min: [f64; 3],
359    /// Maximum corner `[x, y, z]`.
360    pub max: [f64; 3],
361}
362/// Statistics collected during a SAP broadphase query.
363#[derive(Debug, Clone, Default)]
364pub struct SapStats {
365    /// Number of overlapping pairs found in the last query.
366    pub pair_count: usize,
367    /// Number of endpoint comparisons made during the last sweep.
368    pub sweep_count: usize,
369    /// Number of active bodies tracked.
370    pub body_count: usize,
371}
372/// SAP that tracks pair-begin / pair-end events across frames.
373///
374/// Call `step_pairs` each frame; it returns only the *delta* — new overlaps
375/// and vanished overlaps — rather than the full pair list.
376pub struct EventDrivenSap {
377    pub(super) inner: IncrementalSap,
378    /// Pairs that were overlapping in the *previous* frame.
379    pub(super) prev_pairs: HashSet<(u32, u32)>,
380}
381impl EventDrivenSap {
382    /// Create a new event-driven SAP.
383    pub fn new() -> Self {
384        Self {
385            inner: IncrementalSap::new(),
386            prev_pairs: HashSet::new(),
387        }
388    }
389    /// Insert a body.
390    pub fn insert(&mut self, id: u32, aabb: Aabb3) {
391        self.inner.insert(id, aabb);
392    }
393    /// Remove a body.  Any pairs it was part of will appear as `End` events
394    /// on the next `step_pairs` call.
395    pub fn remove(&mut self, id: u32) {
396        self.inner.remove(id);
397    }
398    /// Update a body's AABB.
399    pub fn update(&mut self, id: u32, aabb: Aabb3) {
400        self.inner.update(id, aabb);
401    }
402    /// Compute overlap events for the current frame.
403    ///
404    /// Returns `(events, current_pairs)` where `events` lists only the
405    /// begin/end transitions relative to the previous frame.
406    pub fn step_pairs(&mut self) -> (Vec<OverlapEvent>, Vec<(u32, u32)>) {
407        let current_vec = self.inner.query_pairs();
408        let current: HashSet<(u32, u32)> = current_vec.iter().copied().collect();
409        let mut events = Vec::new();
410        for &pair in &current {
411            if !self.prev_pairs.contains(&pair) {
412                events.push(OverlapEvent::Begin(pair.0, pair.1));
413            }
414        }
415        for &pair in &self.prev_pairs {
416            if !current.contains(&pair) {
417                events.push(OverlapEvent::End(pair.0, pair.1));
418            }
419        }
420        self.prev_pairs = current;
421        events.sort_by_key(|e| match e {
422            OverlapEvent::Begin(a, b) | OverlapEvent::End(a, b) => (*a, *b),
423        });
424        (events, current_vec)
425    }
426    /// Number of currently tracked bodies.
427    pub fn body_count(&self) -> usize {
428        self.inner.body_count()
429    }
430}
431/// Sweep-and-Prune broadphase that operates on the X axis and filters on Y/Z.
432pub struct SweepAndPrune {
433    /// All registered objects.
434    pub objects: Vec<SapObject>,
435    /// Sorted endpoints on the X axis.
436    pub sorted_x: Vec<SapEndpoint>,
437}
438impl SweepAndPrune {
439    /// Create an empty `SweepAndPrune` instance.
440    pub fn new() -> Self {
441        Self {
442            objects: Vec::new(),
443            sorted_x: Vec::new(),
444        }
445    }
446    /// Add an object with the given AABB.
447    pub fn add_object(&mut self, id: u64, min: [f64; 3], max: [f64; 3]) {
448        self.objects.push(SapObject { id, min, max });
449        self.sorted_x.push(SapEndpoint {
450            value: min[0],
451            object_id: id,
452            is_min: true,
453        });
454        self.sorted_x.push(SapEndpoint {
455            value: max[0],
456            object_id: id,
457            is_min: false,
458        });
459        self.sort_axis();
460    }
461    /// Remove the object with the given `id`.
462    pub fn remove_object(&mut self, id: u64) {
463        self.objects.retain(|o| o.id != id);
464        self.sorted_x.retain(|e| e.object_id != id);
465    }
466    /// Update (replace) the AABB for an existing object.
467    pub fn update_object(&mut self, id: u64, min: [f64; 3], max: [f64; 3]) {
468        if let Some(obj) = self.objects.iter_mut().find(|o| o.id == id) {
469            obj.min = min;
470            obj.max = max;
471        }
472        for ep in self.sorted_x.iter_mut().filter(|e| e.object_id == id) {
473            ep.value = if ep.is_min { min[0] } else { max[0] };
474        }
475        self.sort_axis();
476    }
477    /// Sort the X-axis endpoint list by value.
478    pub fn sort_axis(&mut self) {
479        self.sorted_x.sort_by(|a, b| {
480            a.value
481                .partial_cmp(&b.value)
482                .unwrap_or(std::cmp::Ordering::Equal)
483        });
484    }
485    /// Return all overlapping pairs.
486    ///
487    /// Sweeps the sorted X endpoints to collect candidate pairs that overlap on
488    /// X, then filters each candidate by checking Y and Z overlap as well.
489    /// The returned list is deduplicated and has `(min_id, max_id)` ordering.
490    pub fn query_overlapping_pairs(&mut self) -> Vec<(u64, u64)> {
491        self.sort_axis();
492        let obj_map: HashMap<u64, &SapObject> = self.objects.iter().map(|o| (o.id, o)).collect();
493        let mut pairs: Vec<(u64, u64)> = Vec::new();
494        let mut active: Vec<u64> = Vec::new();
495        for ep in &self.sorted_x {
496            if ep.is_min {
497                if let Some(obj_a) = obj_map.get(&ep.object_id) {
498                    for &active_id in &active {
499                        if let Some(obj_b) = obj_map.get(&active_id)
500                            && Self::overlaps_on_axis(
501                                obj_a.min[1],
502                                obj_a.max[1],
503                                obj_b.min[1],
504                                obj_b.max[1],
505                            )
506                            && Self::overlaps_on_axis(
507                                obj_a.min[2],
508                                obj_a.max[2],
509                                obj_b.min[2],
510                                obj_b.max[2],
511                            )
512                        {
513                            let (lo, hi) = if ep.object_id < active_id {
514                                (ep.object_id, active_id)
515                            } else {
516                                (active_id, ep.object_id)
517                            };
518                            pairs.push((lo, hi));
519                        }
520                    }
521                }
522                active.push(ep.object_id);
523            } else {
524                active.retain(|&id| id != ep.object_id);
525            }
526        }
527        pairs.sort_unstable();
528        pairs.dedup();
529        pairs
530    }
531    /// Test whether two intervals `[min_a, max_a]` and `[min_b, max_b]` overlap.
532    #[inline]
533    pub fn overlaps_on_axis(min_a: f64, max_a: f64, min_b: f64, max_b: f64) -> bool {
534        min_a <= max_b && min_b <= max_a
535    }
536    /// Return the number of registered objects.
537    pub fn object_count(&self) -> usize {
538        self.objects.len()
539    }
540}
541impl SweepAndPrune {
542    /// Batch-insert multiple objects at once, sorting the axis list only once.
543    ///
544    /// More efficient than calling `add_object` repeatedly when adding many
545    /// objects at the same time.
546    pub fn add_object_batch(&mut self, objects: &[(u64, [f64; 3], [f64; 3])]) {
547        for &(id, min, max) in objects {
548            self.objects.push(SapObject { id, min, max });
549            self.sorted_x.push(SapEndpoint {
550                value: min[0],
551                object_id: id,
552                is_min: true,
553            });
554            self.sorted_x.push(SapEndpoint {
555                value: max[0],
556                object_id: id,
557                is_min: false,
558            });
559        }
560        self.sort_axis();
561    }
562    /// Compute the variance of AABB centres along each axis.
563    ///
564    /// Returns `[var_x, var_y, var_z]`.  The axis with the highest variance is
565    /// the best choice for the SAP sweep direction (maximises early-out
566    /// opportunities).
567    pub fn compute_axis_variance(&self) -> [f64; 3] {
568        let n = self.objects.len();
569        if n == 0 {
570            return [0.0; 3];
571        }
572        let mut sum = [0.0_f64; 3];
573        let mut sum_sq = [0.0_f64; 3];
574        for obj in &self.objects {
575            for ax in 0..3 {
576                let c = (obj.min[ax] + obj.max[ax]) * 0.5;
577                sum[ax] += c;
578                sum_sq[ax] += c * c;
579            }
580        }
581        let nf = n as f64;
582        let mut var = [0.0_f64; 3];
583        for ax in 0..3 {
584            let mean = sum[ax] / nf;
585            var[ax] = (sum_sq[ax] / nf) - mean * mean;
586        }
587        var
588    }
589    /// Reorder the SAP to sweep along the axis with the highest variance.
590    ///
591    /// Updates `self.axis` (if it were a stored field; here we rebuild
592    /// `sorted_x` to reflect the chosen axis) and re-sorts the endpoint list.
593    ///
594    /// Returns the chosen axis index (`0`=X, `1`=Y, `2`=Z).
595    pub fn reorder_axes(&mut self) -> usize {
596        let var = self.compute_axis_variance();
597        let best_axis = if var[0] >= var[1] && var[0] >= var[2] {
598            0
599        } else if var[1] >= var[2] {
600            1
601        } else {
602            2
603        };
604        self.sorted_x.clear();
605        for obj in &self.objects {
606            self.sorted_x.push(SapEndpoint {
607                value: obj.min[best_axis],
608                object_id: obj.id,
609                is_min: true,
610            });
611            self.sorted_x.push(SapEndpoint {
612                value: obj.max[best_axis],
613                object_id: obj.id,
614                is_min: false,
615            });
616        }
617        self.sort_axis();
618        best_axis
619    }
620}
621/// Incremental multi-axis Sweep-and-Prune broadphase.
622///
623/// Maintains sorted endpoint lists on X, Y, and Z axes and intersects
624/// overlap sets from all three to produce the final pair set.
625pub struct IncrementalSap {
626    /// Sorted endpoints on the X axis.
627    pub endpoints_x: Vec<SapEndpointU32>,
628    /// Sorted endpoints on the Y axis.
629    pub endpoints_y: Vec<SapEndpointU32>,
630    /// Sorted endpoints on the Z axis.
631    pub endpoints_z: Vec<SapEndpointU32>,
632    /// Currently tracked AABBs keyed by body id.
633    pub aabbs: HashMap<u32, Aabb3>,
634    /// Active overlapping pairs from the last query.
635    pub active_pairs: HashSet<(u32, u32)>,
636}
637impl IncrementalSap {
638    /// Create an empty incremental SAP.
639    pub fn new() -> Self {
640        Self {
641            endpoints_x: Vec::new(),
642            endpoints_y: Vec::new(),
643            endpoints_z: Vec::new(),
644            aabbs: HashMap::new(),
645            active_pairs: HashSet::new(),
646        }
647    }
648    /// Insert a body with the given AABB.
649    pub fn insert(&mut self, body_id: u32, aabb: Aabb3) {
650        self.endpoints_x.push(SapEndpointU32 {
651            value: aabb.min[0],
652            body_id,
653            is_min: true,
654        });
655        self.endpoints_x.push(SapEndpointU32 {
656            value: aabb.max[0],
657            body_id,
658            is_min: false,
659        });
660        self.endpoints_y.push(SapEndpointU32 {
661            value: aabb.min[1],
662            body_id,
663            is_min: true,
664        });
665        self.endpoints_y.push(SapEndpointU32 {
666            value: aabb.max[1],
667            body_id,
668            is_min: false,
669        });
670        self.endpoints_z.push(SapEndpointU32 {
671            value: aabb.min[2],
672            body_id,
673            is_min: true,
674        });
675        self.endpoints_z.push(SapEndpointU32 {
676            value: aabb.max[2],
677            body_id,
678            is_min: false,
679        });
680        self.aabbs.insert(body_id, aabb);
681    }
682    /// Remove a body from the SAP.
683    pub fn remove(&mut self, body_id: u32) {
684        self.endpoints_x.retain(|e| e.body_id != body_id);
685        self.endpoints_y.retain(|e| e.body_id != body_id);
686        self.endpoints_z.retain(|e| e.body_id != body_id);
687        self.aabbs.remove(&body_id);
688        self.active_pairs
689            .retain(|&(a, b)| a != body_id && b != body_id);
690    }
691    /// Update the AABB for an existing body.
692    pub fn update(&mut self, body_id: u32, new_aabb: Aabb3) {
693        for ep in self.endpoints_x.iter_mut().filter(|e| e.body_id == body_id) {
694            ep.value = if ep.is_min {
695                new_aabb.min[0]
696            } else {
697                new_aabb.max[0]
698            };
699        }
700        for ep in self.endpoints_y.iter_mut().filter(|e| e.body_id == body_id) {
701            ep.value = if ep.is_min {
702                new_aabb.min[1]
703            } else {
704                new_aabb.max[1]
705            };
706        }
707        for ep in self.endpoints_z.iter_mut().filter(|e| e.body_id == body_id) {
708            ep.value = if ep.is_min {
709                new_aabb.min[2]
710            } else {
711                new_aabb.max[2]
712            };
713        }
714        self.aabbs.insert(body_id, new_aabb);
715    }
716    /// Batch update multiple bodies at once, then re-sort once.
717    pub fn batch_update(&mut self, updates: &[(u32, Aabb3)]) {
718        for (body_id, new_aabb) in updates {
719            for ep in self
720                .endpoints_x
721                .iter_mut()
722                .filter(|e| e.body_id == *body_id)
723            {
724                ep.value = if ep.is_min {
725                    new_aabb.min[0]
726                } else {
727                    new_aabb.max[0]
728                };
729            }
730            for ep in self
731                .endpoints_y
732                .iter_mut()
733                .filter(|e| e.body_id == *body_id)
734            {
735                ep.value = if ep.is_min {
736                    new_aabb.min[1]
737                } else {
738                    new_aabb.max[1]
739                };
740            }
741            for ep in self
742                .endpoints_z
743                .iter_mut()
744                .filter(|e| e.body_id == *body_id)
745            {
746                ep.value = if ep.is_min {
747                    new_aabb.min[2]
748                } else {
749                    new_aabb.max[2]
750                };
751            }
752            self.aabbs.insert(*body_id, new_aabb.clone());
753        }
754    }
755    /// Query all overlapping pairs by intersecting results from all three axes.
756    pub fn query_pairs(&mut self) -> Vec<(u32, u32)> {
757        let pairs_x = Self::sort_and_sweep_axis(&mut self.endpoints_x);
758        let pairs_y = Self::sort_and_sweep_axis(&mut self.endpoints_y);
759        let pairs_z = Self::sort_and_sweep_axis(&mut self.endpoints_z);
760        let result: HashSet<(u32, u32)> = pairs_x
761            .intersection(&pairs_y)
762            .copied()
763            .collect::<HashSet<_>>()
764            .intersection(&pairs_z)
765            .copied()
766            .collect();
767        self.active_pairs = result.clone();
768        let mut pairs: Vec<(u32, u32)> = result.into_iter().collect();
769        pairs.sort_unstable();
770        pairs
771    }
772    /// Sort endpoints on one axis and sweep to find overlapping pairs.
773    pub fn sort_and_sweep_axis(endpoints: &mut [SapEndpointU32]) -> HashSet<(u32, u32)> {
774        endpoints.sort_by(|a, b| {
775            a.value
776                .partial_cmp(&b.value)
777                .unwrap_or(std::cmp::Ordering::Equal)
778        });
779        let mut pairs = HashSet::new();
780        let mut active: Vec<u32> = Vec::new();
781        for ep in endpoints.iter() {
782            if ep.is_min {
783                for &aid in &active {
784                    let (lo, hi) = if ep.body_id < aid {
785                        (ep.body_id, aid)
786                    } else {
787                        (aid, ep.body_id)
788                    };
789                    pairs.insert((lo, hi));
790                }
791                active.push(ep.body_id);
792            } else {
793                active.retain(|&id| id != ep.body_id);
794            }
795        }
796        pairs
797    }
798    /// Return the number of tracked bodies.
799    pub fn body_count(&self) -> usize {
800        self.aabbs.len()
801    }
802    /// Return the current set of active pairs (from last query).
803    pub fn current_pairs(&self) -> &HashSet<(u32, u32)> {
804        &self.active_pairs
805    }
806    /// Insert a new AABB for `id` and update the active pair set.
807    pub fn insert_aabb(&mut self, id: u32, min: [f64; 3], max: [f64; 3]) {
808        self.insert(id, Aabb3 { min, max });
809        self.query_pairs();
810    }
811    /// Remove the AABB for `id` and clear its affected pairs.
812    pub fn remove_aabb(&mut self, id: u32) {
813        self.remove(id);
814        self.active_pairs.retain(|&(a, b)| a != id && b != id);
815    }
816    /// Update the AABB for `id` using incremental re-sort (insertion sort for small moves).
817    pub fn update_aabb(&mut self, id: u32, min: [f64; 3], max: [f64; 3]) {
818        self.update(id, Aabb3 { min, max });
819        self.query_pairs();
820    }
821    /// Return the current overlapping pair set.
822    pub fn active_pairs(&self) -> &HashSet<(u32, u32)> {
823        &self.active_pairs
824    }
825    /// Compute broadphase pairs between two disjoint sets using 3-axis SAP.
826    ///
827    /// Only pairs `(a, b)` where `a ∈ set_a` and `b ∈ set_b` are returned.
828    pub fn bipartite_pairs(&self, set_a: &[u32], set_b: &[u32]) -> Vec<(u32, u32)> {
829        let set_a_hs: HashSet<u32> = set_a.iter().copied().collect();
830        let set_b_hs: HashSet<u32> = set_b.iter().copied().collect();
831        let mut temp = IncrementalSap::new();
832        for &id in set_a.iter().chain(set_b.iter()) {
833            if let Some(aabb) = self.aabbs.get(&id) {
834                temp.insert(id, aabb.clone());
835            }
836        }
837        let all_pairs = temp.query_pairs();
838        all_pairs
839            .into_iter()
840            .filter(|&(a, b)| {
841                (set_a_hs.contains(&a) && set_b_hs.contains(&b))
842                    || (set_a_hs.contains(&b) && set_b_hs.contains(&a))
843            })
844            .collect()
845    }
846}
847impl IncrementalSap {
848    /// Compute the variance of AABB centres on each axis, returning
849    /// `[var_x, var_y, var_z]`.  The axis with highest variance is the best
850    /// sweep direction.
851    pub fn compute_axis_variance(&self) -> [f64; 3] {
852        let n = self.aabbs.len();
853        if n == 0 {
854            return [0.0; 3];
855        }
856        let mut sum = [0.0_f64; 3];
857        let mut sum_sq = [0.0_f64; 3];
858        for aabb in self.aabbs.values() {
859            for ax in 0..3 {
860                let c = (aabb.min[ax] + aabb.max[ax]) * 0.5;
861                sum[ax] += c;
862                sum_sq[ax] += c * c;
863            }
864        }
865        let nf = n as f64;
866        let mut var = [0.0_f64; 3];
867        for ax in 0..3 {
868            let mean = sum[ax] / nf;
869            var[ax] = (sum_sq[ax] / nf) - mean * mean;
870        }
871        var
872    }
873    /// Reorder all three endpoint arrays to sweep along the axis with the
874    /// highest AABB-centre variance, then re-sort.
875    ///
876    /// Returns the chosen axis index (`0`=X, `1`=Y, `2`=Z).
877    pub fn reorder_axes(&mut self) -> usize {
878        let var = self.compute_axis_variance();
879        let best = if var[0] >= var[1] && var[0] >= var[2] {
880            0
881        } else if var[1] >= var[2] {
882            1
883        } else {
884            2
885        };
886        self.endpoints_x.clear();
887        for (id, aabb) in &self.aabbs {
888            self.endpoints_x.push(SapEndpointU32 {
889                value: aabb.min[best],
890                body_id: *id,
891                is_min: true,
892            });
893            self.endpoints_x.push(SapEndpointU32 {
894                value: aabb.max[best],
895                body_id: *id,
896                is_min: false,
897            });
898        }
899        self.endpoints_x.sort_by(|a, b| {
900            a.value
901                .partial_cmp(&b.value)
902                .unwrap_or(std::cmp::Ordering::Equal)
903        });
904        best
905    }
906}
907impl IncrementalSap {
908    /// Return `true` if any body in the SAP overlaps `aabb`.
909    pub fn any_overlap(&self, aabb: &Aabb3) -> bool {
910        for stored in self.aabbs.values() {
911            if aabb3_overlaps(stored, aabb) {
912                return true;
913            }
914        }
915        false
916    }
917    /// Return the ids of all bodies whose AABB overlaps `aabb`.
918    pub fn query_aabb(&self, aabb: &Aabb3) -> Vec<u32> {
919        self.aabbs
920            .iter()
921            .filter_map(|(&id, stored)| {
922                if aabb3_overlaps(stored, aabb) {
923                    Some(id)
924                } else {
925                    None
926                }
927            })
928            .collect()
929    }
930    /// Return the ids of all bodies within squared distance `radius_sq` of point `p`.
931    pub fn query_sphere_sq(&self, p: [f64; 3], radius_sq: f64) -> Vec<u32> {
932        self.aabbs
933            .iter()
934            .filter_map(|(&id, aabb)| {
935                if aabb3_point_dist_sq(aabb, p) <= radius_sq {
936                    Some(id)
937                } else {
938                    None
939                }
940            })
941            .collect()
942    }
943    /// Remove all bodies and reset to empty.
944    pub fn clear(&mut self) {
945        self.endpoints_x.clear();
946        self.endpoints_y.clear();
947        self.endpoints_z.clear();
948        self.aabbs.clear();
949        self.active_pairs.clear();
950    }
951    /// Return the AABB for `id`, or `None` if not present.
952    pub fn get_aabb(&self, id: u32) -> Option<&Aabb3> {
953        self.aabbs.get(&id)
954    }
955    /// Return an iterator over all tracked body ids.
956    pub fn body_ids(&self) -> impl Iterator<Item = u32> + '_ {
957        self.aabbs.keys().copied()
958    }
959    /// Batch-insert all bodies from `other` into `self`.
960    ///
961    /// Bodies already in `self` are overwritten (their AABBs updated).
962    pub fn merge_from(&mut self, other: &IncrementalSap) {
963        for (&id, aabb) in &other.aabbs {
964            if self.aabbs.contains_key(&id) {
965                self.update(id, aabb.clone());
966            } else {
967                self.insert(id, aabb.clone());
968            }
969        }
970    }
971    /// Return `true` if `id` is currently tracked.
972    pub fn contains(&self, id: u32) -> bool {
973        self.aabbs.contains_key(&id)
974    }
975}
976/// A multi-phase SAP that integrates event tracking and statistics.
977pub struct MultiPhaseSap {
978    pub(super) event_sap: EventDrivenSap,
979    pub(super) stat_sap: StatTrackingSap,
980}
981impl MultiPhaseSap {
982    /// Create a new multi-phase SAP.
983    pub fn new() -> Self {
984        Self {
985            event_sap: EventDrivenSap::new(),
986            stat_sap: StatTrackingSap::new(),
987        }
988    }
989    /// Insert a body into both SAP structures.
990    pub fn insert(&mut self, id: u32, aabb: Aabb3) {
991        self.event_sap.insert(id, aabb.clone());
992        self.stat_sap.insert(id, aabb);
993    }
994    /// Remove a body from both SAP structures.
995    pub fn remove(&mut self, id: u32) {
996        self.event_sap.remove(id);
997        self.stat_sap.remove(id);
998    }
999    /// Update a body's AABB in both SAP structures.
1000    pub fn update(&mut self, id: u32, aabb: Aabb3) {
1001        self.event_sap.update(id, aabb.clone());
1002        self.stat_sap.update(id, aabb);
1003    }
1004    /// Perform a full broadphase step and return the combined result.
1005    pub fn step(&mut self) -> BroadphaseResult {
1006        let pairs = self.stat_sap.query_pairs();
1007        let stats = self.stat_sap.stats.clone();
1008        let (events, _) = self.event_sap.step_pairs();
1009        let mut new_pairs = Vec::new();
1010        let mut lost_pairs = Vec::new();
1011        for ev in events {
1012            match ev {
1013                OverlapEvent::Begin(a, b) => new_pairs.push((a, b)),
1014                OverlapEvent::End(a, b) => lost_pairs.push((a, b)),
1015            }
1016        }
1017        BroadphaseResult {
1018            pairs,
1019            new_pairs,
1020            lost_pairs,
1021            stats,
1022        }
1023    }
1024}