1use std::collections::HashMap;
4
5use super::aabb::Aabb;
6use crate::math::IVec2;
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
54#[derive(Debug)]
63pub struct ColliderSet {
64 cell_size: f32,
65 colliders: Vec<Collider>,
66 grid: HashMap<IVec2, Vec<usize>>,
68}
69
70impl ColliderSet {
71 #[must_use]
78 pub fn new(cell_size: f32) -> Self {
79 assert!(
80 cell_size > 0.0 && cell_size.is_finite(),
81 "ColliderSet: cell size must be positive and finite, got {cell_size}"
82 );
83 Self {
84 cell_size,
85 colliders: Vec::new(),
86 grid: HashMap::new(),
87 }
88 }
89
90 #[must_use]
92 pub fn cell_size(&self) -> f32 {
93 self.cell_size
94 }
95
96 pub fn insert(&mut self, aabb: Aabb, flags: ColliderFlags) -> usize {
99 let index = self.colliders.len();
100 self.colliders.push(Collider { aabb, flags });
101 for cell in cells_for(&aabb, self.cell_size) {
102 self.grid.entry(cell).or_default().push(index);
103 }
104 index
105 }
106
107 #[must_use]
109 pub fn get(&self, index: usize) -> Option<&Collider> {
110 self.colliders.get(index)
111 }
112
113 #[must_use]
119 pub fn query(&self, region: &Aabb) -> Vec<usize> {
120 let mut found = Vec::new();
121 for cell in cells_for(region, self.cell_size) {
122 if let Some(bucket) = self.grid.get(&cell) {
123 found.extend_from_slice(bucket);
124 }
125 }
126 found.sort_unstable();
127 found.dedup();
128 found
129 }
130
131 #[must_use]
133 pub fn len(&self) -> usize {
134 self.colliders.len()
135 }
136
137 #[must_use]
139 pub fn is_empty(&self) -> bool {
140 self.colliders.is_empty()
141 }
142
143 pub fn clear(&mut self) {
146 self.colliders.clear();
147 self.grid.clear();
148 }
149
150 pub fn iter(&self) -> std::slice::Iter<'_, Collider> {
153 self.colliders.iter()
154 }
155}
156
157impl<'a> IntoIterator for &'a ColliderSet {
158 type Item = &'a Collider;
159 type IntoIter = std::slice::Iter<'a, Collider>;
160
161 fn into_iter(self) -> Self::IntoIter {
162 self.iter()
163 }
164}
165
166fn cells_for(aabb: &Aabb, cell_size: f32) -> impl Iterator<Item = IVec2> {
168 let min = aabb.min();
169 let max = aabb.max();
170 let (x0, x1, y0, y1) = if min.is_finite() && max.is_finite() {
173 (
174 cell_of(min.x, cell_size),
175 cell_of(max.x, cell_size),
176 cell_of(min.y, cell_size),
177 cell_of(max.y, cell_size),
178 )
179 } else {
180 (0, -1, 0, -1)
181 };
182 (y0..=y1).flat_map(move |y| (x0..=x1).map(move |x| IVec2::new(x, y)))
183}
184
185#[allow(clippy::cast_possible_truncation)]
188fn cell_of(value: f32, cell_size: f32) -> i32 {
189 (value / cell_size).floor() as i32
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::math::Vec2;
196 use wasm_bindgen_test::wasm_bindgen_test;
197
198 #[wasm_bindgen_test(unsupported = test)]
200 fn getters_report_the_configured_size_and_count() {
201 let mut set = ColliderSet::new(2.5);
202 assert_eq!(set.cell_size().to_bits(), 2.5f32.to_bits());
203 assert!(set.is_empty());
204 assert_eq!(set.len(), 0);
205 set.insert(aabb(0.0, 0.0, 1.0, 1.0), ColliderFlags::SOLID);
206 set.insert(aabb(9.0, 9.0, 1.0, 1.0), ColliderFlags::SENSOR);
207 assert!(!set.is_empty());
208 assert_eq!(set.len(), 2);
209 assert_eq!(set.iter().count(), 2);
210 assert_eq!((&set).into_iter().count(), 2);
211 }
212
213 #[wasm_bindgen_test(unsupported = test)]
217 fn negative_fractional_boxes_are_found_and_non_finite_regions_find_nothing() {
218 let mut set = ColliderSet::new(1.5);
219 let origin = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
220 let low = set.insert(aabb(-2.2, -3.7, 0.4, 0.4), ColliderFlags::SOLID);
221 assert_eq!(set.query(&aabb(-2.5, -4.0, 0.2, 0.2)), vec![low]);
222 assert_eq!(set.query(&aabb(-5.0, -5.0, 0.2, 0.2)), Vec::<usize>::new());
223 assert_eq!(set.query(&aabb(0.2, 0.2, 0.1, 0.1)), vec![origin]);
224 assert!(set.query(&aabb(f32::NAN, 0.0, 1.0, 1.0)).is_empty());
225 assert!(set.query(&aabb(0.0, 0.0, f32::INFINITY, 1.0)).is_empty());
226 }
227
228 fn aabb(cx: f32, cy: f32, hx: f32, hy: f32) -> Aabb {
229 Aabb::new(Vec2::new(cx, cy), Vec2::new(hx, hy))
230 }
231
232 #[wasm_bindgen_test(unsupported = test)]
237 fn configured_cell_size_decides_bucketing() {
238 let mut coarse = ColliderSet::new(100.0);
239 coarse.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
240 coarse.insert(aabb(50.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
241 assert_eq!(coarse.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![0, 1]);
242
243 let mut fine = ColliderSet::new(1.0);
244 fine.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
245 fine.insert(aabb(50.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
246 assert_eq!(fine.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![0]);
247 }
248
249 #[wasm_bindgen_test(unsupported = test)]
254 fn multi_cell_colliders_are_returned_once_in_index_order() {
255 let mut set = ColliderSet::new(0.25);
256 let floor = set.insert(aabb(0.0, 0.0, 1.0, 0.25), ColliderFlags::SOLID);
257 let far = set.insert(aabb(10.0, 10.0, 0.5, 0.5), ColliderFlags::SOLID);
258 let box_above = set.insert(aabb(0.5, 0.5, 0.3, 0.3), ColliderFlags::SOLID);
259
260 assert_eq!(set.query(&aabb(0.0, 0.0, 1.0, 1.0)), vec![floor, box_above]);
261 assert_eq!(set.query(&aabb(0.9, -0.2, 0.05, 0.02)), vec![floor]);
263 assert_eq!(set.query(&aabb(10.0, 10.0, 0.1, 0.1)), vec![far]);
264 }
265
266 #[wasm_bindgen_test(unsupported = test)]
270 fn clear_drops_the_grid_with_the_colliders() {
271 let mut set = ColliderSet::new(1.0);
272 set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
273 set.insert(aabb(1.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
274 set.clear();
275
276 assert!(set.is_empty());
277 assert_eq!(set.len(), 0);
278 assert!(set.query(&aabb(0.0, 0.0, 2.0, 2.0)).is_empty());
279
280 let reused = set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SENSOR);
281 assert_eq!(reused, 0, "indices restart after a clear");
282 assert_eq!(set.query(&aabb(0.0, 0.0, 0.5, 0.5)), vec![0]);
283 assert_eq!(
284 set.iter().next().map(|c| c.flags),
285 Some(ColliderFlags::SENSOR)
286 );
287 }
288
289 #[wasm_bindgen_test(unsupported = test)]
292 fn non_finite_regions_address_no_cells() {
293 let mut set = ColliderSet::new(1.0);
294 set.insert(aabb(0.0, 0.0, 0.5, 0.5), ColliderFlags::SOLID);
295 assert!(
296 set.query(&aabb(0.0, 0.0, f32::INFINITY, 1.0)).is_empty(),
297 "an infinite region must not be walked cell by cell"
298 );
299 assert!(set.query(&aabb(f32::NAN, 0.0, 1.0, 1.0)).is_empty());
300 }
301}