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}