1use std::collections::HashMap;
4
5use super::aabb::Aabb;
6use crate::math::{IVec2, Vec2};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct ColliderFlags {
11 pub solid: bool,
14 pub one_way: bool,
17 pub sensor: bool,
20}
21
22impl ColliderFlags {
23 pub const SOLID: Self = Self {
25 solid: true,
26 one_way: false,
27 sensor: false,
28 };
29
30 pub const ONE_WAY: Self = Self {
32 solid: true,
33 one_way: true,
34 sensor: false,
35 };
36
37 pub const SENSOR: Self = Self {
39 solid: false,
40 one_way: false,
41 sensor: true,
42 };
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct Collider {
48 pub aabb: Aabb,
50 pub flags: ColliderFlags,
52}
53
54const MAX_COLLIDER_CELLS: u64 = 1024;
59
60#[derive(Debug)]
76pub struct ColliderSet {
77 cell_size: f32,
78 colliders: Vec<Collider>,
79 grid: HashMap<IVec2, Vec<usize>>,
81 oversized: Vec<usize>,
83}
84
85impl ColliderSet {
86 #[must_use]
93 pub fn new(cell_size: f32) -> Self {
94 assert!(
95 cell_size > 0.0 && cell_size.is_finite(),
96 "ColliderSet: cell size must be positive and finite, got {cell_size}"
97 );
98 Self {
99 cell_size,
100 colliders: Vec::new(),
101 grid: HashMap::new(),
102 oversized: Vec::new(),
103 }
104 }
105
106 #[must_use]
108 pub fn cell_size(&self) -> f32 {
109 self.cell_size
110 }
111
112 pub fn insert(&mut self, aabb: Aabb, flags: ColliderFlags) -> usize {
117 let index = self.colliders.len();
118 self.colliders.push(Collider { aabb, flags });
119 if let Some(cells) = CellRange::of(&aabb, self.cell_size) {
120 if cells.count() > MAX_COLLIDER_CELLS {
121 self.oversized.push(index);
122 } else {
123 for cell in cells.iter() {
124 self.grid.entry(cell).or_default().push(index);
125 }
126 }
127 }
128 index
129 }
130
131 #[must_use]
133 pub fn get(&self, index: usize) -> Option<&Collider> {
134 self.colliders.get(index)
135 }
136
137 #[must_use]
147 pub fn query(&self, region: &Aabb) -> Vec<usize> {
148 self.query_corners(region.min(), region.max())
149 }
150
151 pub(super) fn query_corners(&self, min: Vec2, max: Vec2) -> Vec<usize> {
153 let Some(cells) = CellRange::of_corners(min, max, self.cell_size) else {
154 return Vec::new();
155 };
156 let meets = |index: &usize| {
157 CellRange::of(&self.colliders[*index].aabb, self.cell_size)
158 .is_some_and(|other| other.meets(&cells))
159 };
160 let mut found = Vec::new();
161 if cells.count() > self.colliders.len() as u64 {
162 found.extend((0..self.colliders.len()).filter(meets));
163 return found;
164 }
165 for cell in cells.iter() {
166 if let Some(bucket) = self.grid.get(&cell) {
167 found.extend_from_slice(bucket);
168 }
169 }
170 found.extend(self.oversized.iter().copied().filter(meets));
171 found.sort_unstable();
172 found.dedup();
173 found
174 }
175
176 #[must_use]
178 pub fn len(&self) -> usize {
179 self.colliders.len()
180 }
181
182 #[must_use]
184 pub fn is_empty(&self) -> bool {
185 self.colliders.is_empty()
186 }
187
188 pub fn clear(&mut self) {
191 self.colliders.clear();
192 self.grid.clear();
193 self.oversized.clear();
194 }
195
196 pub fn iter(&self) -> std::slice::Iter<'_, Collider> {
199 self.colliders.iter()
200 }
201}
202
203impl<'a> IntoIterator for &'a ColliderSet {
204 type Item = &'a Collider;
205 type IntoIter = std::slice::Iter<'a, Collider>;
206
207 fn into_iter(self) -> Self::IntoIter {
208 self.iter()
209 }
210}
211
212#[derive(Debug, Clone, Copy)]
214struct CellRange {
215 min: IVec2,
216 max: IVec2,
217}
218
219impl CellRange {
220 fn of(aabb: &Aabb, cell_size: f32) -> Option<Self> {
224 Self::of_corners(aabb.min(), aabb.max(), cell_size)
225 }
226
227 fn of_corners(min: Vec2, max: Vec2, cell_size: f32) -> Option<Self> {
229 if !(min.is_finite() && max.is_finite()) {
230 return None;
231 }
232 let range = Self {
233 min: IVec2::new(cell_of(min.x, cell_size), cell_of(min.y, cell_size)),
234 max: IVec2::new(cell_of(max.x, cell_size), cell_of(max.y, cell_size)),
235 };
236 range.min.cmple(range.max).all().then_some(range)
238 }
239
240 fn count(&self) -> u64 {
242 let span = |lo: i32, hi: i32| u64::try_from(i64::from(hi) - i64::from(lo) + 1).unwrap_or(0);
243 span(self.min.x, self.max.x).saturating_mul(span(self.min.y, self.max.y))
244 }
245
246 fn meets(&self, other: &Self) -> bool {
248 self.min.cmple(other.max).all() && other.min.cmple(self.max).all()
249 }
250
251 fn iter(self) -> impl Iterator<Item = IVec2> {
253 (self.min.y..=self.max.y)
254 .flat_map(move |y| (self.min.x..=self.max.x).map(move |x| IVec2::new(x, y)))
255 }
256}
257
258#[allow(clippy::cast_possible_truncation)]
261fn cell_of(value: f32, cell_size: f32) -> i32 {
262 (value / cell_size).floor() as i32
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::math::Vec2;
269 use wasm_bindgen_test::wasm_bindgen_test;
270
271 #[wasm_bindgen_test(unsupported = test)]
273 fn getters_report_the_configured_size_and_count() {
274 let mut set = ColliderSet::new(2.5);
275 assert_eq!(set.cell_size().to_bits(), 2.5f32.to_bits());
276 assert!(set.is_empty());
277 assert_eq!(set.len(), 0);
278 set.insert(aabb(0.0, 0.0, 1.0, 1.0), ColliderFlags::SOLID);
279 set.insert(aabb(9.0, 9.0, 1.0, 1.0), ColliderFlags::SENSOR);
280 assert!(!set.is_empty());
281 assert_eq!(set.len(), 2);
282 assert_eq!(set.iter().count(), 2);
283 assert_eq!((&set).into_iter().count(), 2);
284 }
285
286 #[wasm_bindgen_test(unsupported = test)]
290 fn negative_fractional_boxes_are_found_and_non_finite_regions_find_nothing() {
291 let mut set = ColliderSet::new(1.5);
292 let origin = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
293 let low = set.insert(aabb(-2.2, -3.7, 0.4, 0.4), ColliderFlags::SOLID);
294 assert_eq!(set.query(&aabb(-2.5, -4.0, 0.2, 0.2)), vec![low]);
295 assert_eq!(set.query(&aabb(-5.0, -5.0, 0.2, 0.2)), Vec::<usize>::new());
296 assert_eq!(set.query(&aabb(0.2, 0.2, 0.1, 0.1)), vec![origin]);
297 assert!(set.query(&aabb(f32::NAN, 0.0, 1.0, 1.0)).is_empty());
298 assert!(set.query(&aabb(0.0, 0.0, f32::INFINITY, 1.0)).is_empty());
299 }
300
301 fn aabb(cx: f32, cy: f32, hx: f32, hy: f32) -> Aabb {
302 Aabb::new(Vec2::new(cx, cy), Vec2::new(hx, hy))
303 }
304
305 #[wasm_bindgen_test(unsupported = test)]
310 fn configured_cell_size_decides_bucketing() {
311 let mut coarse = ColliderSet::new(100.0);
312 coarse.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
313 coarse.insert(aabb(50.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
314 assert_eq!(coarse.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![0, 1]);
315
316 let mut fine = ColliderSet::new(1.0);
317 fine.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
318 fine.insert(aabb(50.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
319 assert_eq!(fine.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![0]);
320 }
321
322 #[wasm_bindgen_test(unsupported = test)]
327 fn multi_cell_colliders_are_returned_once_in_index_order() {
328 let mut set = ColliderSet::new(0.25);
329 let floor = set.insert(aabb(0.0, 0.0, 1.0, 0.25), ColliderFlags::SOLID);
330 let far = set.insert(aabb(10.0, 10.0, 0.5, 0.5), ColliderFlags::SOLID);
331 let box_above = set.insert(aabb(0.5, 0.5, 0.3, 0.3), ColliderFlags::SOLID);
332
333 assert_eq!(set.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![floor, box_above]);
334 assert_eq!(set.query(&aabb(0.9, -0.2, 0.05, 0.02)), vec![floor]);
336 assert_eq!(set.query(&aabb(10.0, 10.0, 0.1, 0.1)), vec![far]);
337 }
338
339 #[wasm_bindgen_test(unsupported = test)]
343 fn clear_drops_the_grid_with_the_colliders() {
344 let mut set = ColliderSet::new(1.0);
345 set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
346 set.insert(aabb(1.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
347 set.clear();
348
349 assert!(set.is_empty());
350 assert_eq!(set.len(), 0);
351 assert!(set.query(&aabb(0.0, 0.0, 2.0, 2.0)).is_empty());
352
353 let reused = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SENSOR);
354 assert_eq!(reused, 0, "indices restart after a clear");
355 assert_eq!(set.query(&aabb(0.0, 0.0, 0.5, 0.5)), vec![0]);
356 assert_eq!(
357 set.iter().next().map(|c| c.flags),
358 Some(ColliderFlags::SENSOR)
359 );
360 }
361
362 #[wasm_bindgen_test(unsupported = test)]
365 fn a_huge_query_region_is_answered_without_walking_its_cells() {
366 let mut set = ColliderSet::new(1.0);
367 let near = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
368 let far = set.insert(aabb(9e5, -9e5, 0.5, 0.5), ColliderFlags::SOLID);
369 set.insert(aabb(3e6, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
370 assert_eq!(set.query(&aabb(0.0, 0.0, 1e6, 1e6)), vec![near, far]);
371 }
372
373 #[wasm_bindgen_test(unsupported = test)]
377 fn inverted_boxes_address_no_cells() {
378 let mut set = ColliderSet::new(1.0);
379 let solid = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
380 set.insert(aabb(0.0, 0.0, -3.0, -3.0), ColliderFlags::SOLID);
381 for i in 0..8u8 {
382 set.insert(
383 aabb(-50.0, f32::from(i) * 4.0, 0.5, 0.5),
384 ColliderFlags::SOLID,
385 );
386 }
387 assert_eq!(set.query(&aabb(0.0, 0.0, 0.5, 0.5)), vec![solid]);
389 assert_eq!(set.query(&aabb(0.0, 0.0, 500.0, 500.0)).len(), 9);
390 assert!(set.query(&aabb(0.0, 0.0, -2.0, -2.0)).is_empty());
391 assert!(set.query(&aabb(0.0, 0.0, -500.0, -500.0)).is_empty());
392 }
393
394 #[wasm_bindgen_test(unsupported = test)]
398 fn a_huge_collider_is_found_from_the_cells_it_covers() {
399 let mut set = ColliderSet::new(1.0);
400 let small = set.insert(aabb(3e6, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
401 let block = set.insert(aabb(0.0, 0.0, 1e6, 1e6), ColliderFlags::SOLID);
402 for i in 0..8u8 {
404 set.insert(
405 aabb(-3e6, f32::from(i) * 4.0, 0.5, 0.5),
406 ColliderFlags::SOLID,
407 );
408 }
409 assert_eq!(set.query(&aabb(5e5, -5e5, 0.5, 0.5)), vec![block]);
410 assert_eq!(set.query(&aabb(3e6, 0.0, 0.5, 0.5)), vec![small]);
411 set.clear();
412 assert!(set.query(&aabb(5e5, -5e5, 0.5, 0.5)).is_empty());
413 }
414
415 #[wasm_bindgen_test(unsupported = test)]
418 fn non_finite_regions_address_no_cells() {
419 let mut set = ColliderSet::new(1.0);
420 set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
421 assert!(
422 set.query(&aabb(0.0, 0.0, f32::INFINITY, 1.0)).is_empty(),
423 "an infinite region must not be walked cell by cell"
424 );
425 assert!(set.query(&aabb(f32::NAN, 0.0, 1.0, 1.0)).is_empty());
426 }
427}