Skip to main content

goud_engine/ecs/broad_phase/
queries.rs

1//! Query operations for the spatial hash.
2
3use crate::core::math::{Rect, Vec2};
4use crate::ecs::Entity;
5use std::collections::HashSet;
6
7use super::grid::CellCoord;
8use super::spatial_hash::SpatialHash;
9use super::stats::SpatialHashStats;
10
11impl SpatialHash {
12    // =========================================================================
13    // Queries
14    // =========================================================================
15
16    /// Queries for all potential collision pairs.
17    ///
18    /// Returns a list of entity pairs that share at least one cell. These are
19    /// candidates for narrow phase collision testing.
20    ///
21    /// # Returns
22    ///
23    /// A vector of (Entity, Entity) pairs where the first entity has a lower
24    /// index than the second (no duplicate pairs).
25    ///
26    /// # Performance
27    ///
28    /// O(m * n^2) where m = number of occupied cells, n = average entities per cell.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use goud_engine::ecs::broad_phase::SpatialHash;
34    /// use goud_engine::ecs::Entity;
35    /// use goud_engine::core::math::Rect;
36    ///
37    /// let mut hash = SpatialHash::new(64.0);
38    ///
39    /// let e1 = Entity::new(0, 0);
40    /// let e2 = Entity::new(1, 0);
41    ///
42    /// hash.insert(e1, Rect::new(0.0, 0.0, 32.0, 32.0));
43    /// hash.insert(e2, Rect::new(30.0, 0.0, 32.0, 32.0));
44    ///
45    /// let pairs = hash.query_pairs();
46    /// assert_eq!(pairs.len(), 1);  // Overlapping cells
47    /// ```
48    pub fn query_pairs(&mut self) -> Vec<(Entity, Entity)> {
49        let mut pairs = Vec::new();
50        let mut seen = HashSet::new();
51
52        // For each occupied cell
53        for cell_entities in self.grid.values() {
54            // Generate pairs within the cell
55            let entities: Vec<_> = cell_entities.iter().copied().collect();
56
57            for i in 0..entities.len() {
58                for j in (i + 1)..entities.len() {
59                    let a = entities[i];
60                    let b = entities[j];
61
62                    // Ensure consistent ordering (smaller entity first)
63                    let pair = if a.to_bits() < b.to_bits() {
64                        (a, b)
65                    } else {
66                        (b, a)
67                    };
68
69                    // Avoid duplicate pairs (entities can share multiple cells)
70                    if seen.insert(pair) {
71                        pairs.push(pair);
72                    }
73                }
74            }
75        }
76
77        self.stats.last_query_pairs = pairs.len();
78        pairs
79    }
80
81    /// Queries for entities near a specific point.
82    ///
83    /// Returns all entities whose AABBs occupy the same cell as the point.
84    ///
85    /// # Arguments
86    ///
87    /// * `point` - The query point
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use goud_engine::ecs::broad_phase::SpatialHash;
93    /// use goud_engine::ecs::Entity;
94    /// use goud_engine::core::math::{Rect, Vec2};
95    ///
96    /// let mut hash = SpatialHash::new(64.0);
97    /// let entity = Entity::new(0, 0);
98    ///
99    /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
100    ///
101    /// let nearby = hash.query_point(Vec2::new(10.0, 10.0));
102    /// assert_eq!(nearby.len(), 1);
103    /// ```
104    pub fn query_point(&self, point: Vec2) -> Vec<Entity> {
105        let cell = CellCoord::from_world(point, self.cell_size);
106
107        self.grid
108            .get(&cell)
109            .map(|entities| entities.iter().copied().collect())
110            .unwrap_or_default()
111    }
112
113    /// Queries for entities overlapping an AABB.
114    ///
115    /// Returns all entities whose AABBs occupy any cell overlapped by the query AABB.
116    ///
117    /// # Arguments
118    ///
119    /// * `aabb` - The query AABB
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use goud_engine::ecs::broad_phase::SpatialHash;
125    /// use goud_engine::ecs::Entity;
126    /// use goud_engine::core::math::Rect;
127    ///
128    /// let mut hash = SpatialHash::new(64.0);
129    /// let entity = Entity::new(0, 0);
130    ///
131    /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
132    ///
133    /// let nearby = hash.query_aabb(Rect::new(-10.0, -10.0, 50.0, 50.0));
134    /// assert_eq!(nearby.len(), 1);
135    /// ```
136    pub fn query_aabb(&self, aabb: Rect) -> Vec<Entity> {
137        let cells = self.get_cells_for_aabb(aabb);
138        let mut result = HashSet::new();
139
140        for cell in cells {
141            if let Some(cell_entities) = self.grid.get(&cell) {
142                result.extend(cell_entities.iter().copied());
143            }
144        }
145
146        result.into_iter().collect()
147    }
148
149    /// Queries for entities overlapping a circle.
150    ///
151    /// Returns all entities in cells that overlap the circle's bounding square.
152    /// This is a conservative estimate - narrow phase should verify actual overlap.
153    ///
154    /// # Arguments
155    ///
156    /// * `center` - Circle center position
157    /// * `radius` - Circle radius
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use goud_engine::ecs::broad_phase::SpatialHash;
163    /// use goud_engine::ecs::Entity;
164    /// use goud_engine::core::math::{Rect, Vec2};
165    ///
166    /// let mut hash = SpatialHash::new(64.0);
167    /// let entity = Entity::new(0, 0);
168    ///
169    /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
170    ///
171    /// let nearby = hash.query_circle(Vec2::new(16.0, 16.0), 20.0);
172    /// assert_eq!(nearby.len(), 1);
173    /// ```
174    pub fn query_circle(&self, center: Vec2, radius: f32) -> Vec<Entity> {
175        // Use bounding square for conservative estimate
176        let aabb = Rect::new(
177            center.x - radius,
178            center.y - radius,
179            radius * 2.0,
180            radius * 2.0,
181        );
182
183        self.query_aabb(aabb)
184    }
185
186    // =========================================================================
187    // Accessors
188    // =========================================================================
189
190    /// Returns the cell size.
191    #[inline]
192    pub fn cell_size(&self) -> f32 {
193        self.cell_size
194    }
195
196    /// Returns the number of entities in the hash.
197    #[inline]
198    pub fn entity_count(&self) -> usize {
199        self.entity_bounds.len()
200    }
201
202    /// Returns the number of occupied cells.
203    #[inline]
204    pub fn cell_count(&self) -> usize {
205        self.grid.len()
206    }
207
208    /// Returns whether the hash is empty.
209    #[inline]
210    pub fn is_empty(&self) -> bool {
211        self.entity_bounds.is_empty()
212    }
213
214    /// Returns whether an entity is in the hash.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use goud_engine::ecs::broad_phase::SpatialHash;
220    /// use goud_engine::ecs::Entity;
221    /// use goud_engine::core::math::Rect;
222    ///
223    /// let mut hash = SpatialHash::new(64.0);
224    /// let entity = Entity::new(0, 0);
225    ///
226    /// assert!(!hash.contains(entity));
227    /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
228    /// assert!(hash.contains(entity));
229    /// ```
230    #[inline]
231    pub fn contains(&self, entity: Entity) -> bool {
232        self.entity_bounds.contains_key(&entity)
233    }
234
235    /// Returns the AABB for an entity, if present.
236    #[inline]
237    pub fn get_aabb(&self, entity: Entity) -> Option<Rect> {
238        self.entity_bounds.get(&entity).copied()
239    }
240
241    /// Returns a reference to the current statistics.
242    #[inline]
243    pub fn stats(&self) -> &SpatialHashStats {
244        &self.stats
245    }
246}