#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use bevy::{
math::bounding::Aabb2d,
platform::collections::{HashMap, HashSet},
prelude::{Entity, Vec2},
reflect::Reflect,
};
use smallvec::SmallVec;
type Key = (i32, i32);
#[derive(Default, Reflect, Debug, Clone)]
pub struct SparseGrid2d<const TILE_SIZE: usize = 1> {
map: HashMap<Key, SmallVec<[Entity; 5]>>,
}
impl<const TILE_SIZE: usize> SparseGrid2d<TILE_SIZE> {
pub fn insert_aabb(&mut self, aabb: impl Into<Aabb2d>, entity: Entity) {
for key in KeyIter::new::<TILE_SIZE>(aabb) {
self.map.entry(key).or_default().push(entity);
}
}
pub fn insert_point(&mut self, point: Vec2, entity: Entity) {
let key = Self::key_from_point(point);
self.map.entry(key).or_default().push(entity);
}
#[inline]
pub fn aabb_iter(&'_ self, aabb: impl Into<Aabb2d>) -> impl Iterator<Item = Entity> + '_ {
KeyIter::new::<TILE_SIZE>(aabb)
.filter_map(|key| self.map.get(&key))
.flatten()
.copied()
}
#[inline]
pub fn point_iter(&'_ self, point: Vec2) -> impl Iterator<Item = Entity> + '_ {
let key = Self::key_from_point(point);
std::iter::once(key)
.filter_map(|key| self.map.get(&key))
.flatten()
.copied()
}
#[inline]
pub fn query_aabb(&self, aabb: impl Into<Aabb2d>) -> HashSet<Entity> {
self.aabb_iter(aabb).collect()
}
pub fn clear(&mut self) {
self.map.clear();
}
pub fn soft_clear(&mut self) {
for (_, vec) in self.map.iter_mut() {
vec.clear()
}
}
fn key_from_point(point: Vec2) -> Key {
(
(point.x / TILE_SIZE as f32).floor() as i32,
(point.y / TILE_SIZE as f32).floor() as i32,
)
}
}
struct KeyIter {
width: i32,
start: Key,
current: i32,
count: i32,
}
impl KeyIter {
fn new<const TILE_SIZE: usize>(aabb: impl Into<Aabb2d>) -> Self {
let Aabb2d { min, max } = aabb.into();
let s = TILE_SIZE as f32;
let min = ((min.x / s).floor() as i32, (min.y / s).floor() as i32);
let max = ((max.x / s).ceil() as i32, (max.y / s).ceil() as i32);
let width = max.0 - min.0;
let height = max.1 - min.1;
let count = width * height;
Self {
start: min,
current: -1,
width,
count,
}
}
}
impl Iterator for KeyIter {
type Item = Key;
fn next(&mut self) -> Option<Self::Item> {
self.current += 1;
if self.current < self.count {
Some((
self.start.0 + self.current.rem_euclid(self.width),
self.start.1 + self.current / self.width,
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use bevy::math::{bounding::Aabb2d, vec2};
use bevy::prelude::default;
use super::*;
const TILE_SIZE: usize = 1;
#[test]
fn keys_single() {
let keys: Vec<Key> = KeyIter::new::<TILE_SIZE>(Aabb2d {
min: vec2(0.001, 0.001),
max: vec2(0.001, 0.001),
})
.collect();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], (0, 0));
}
#[test]
fn keys_four_around_origin() {
let keys: Vec<Key> = KeyIter::new::<TILE_SIZE>(Aabb2d {
min: vec2(-0.001, -0.001),
max: vec2(0.001, 0.001),
})
.collect();
assert!(keys.contains(&(0, 0)));
assert!(keys.contains(&(0, -1)));
assert!(keys.contains(&(-1, 0)));
assert!(keys.contains(&(-1, -1)));
assert_eq!(keys.len(), 4);
}
#[test]
fn matches() {
let entity = Entity::from_raw_u32(123).unwrap();
let mut db = SparseGrid2d::<TILE_SIZE>::default();
db.insert_aabb(
Aabb2d {
min: vec2(-0.001, -0.001),
max: vec2(0.001, 0.001),
},
entity,
);
let matches: Vec<Entity> = db
.aabb_iter(Aabb2d {
min: vec2(0.001, 0.001),
max: vec2(0.001, 0.001),
})
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0], entity);
}
#[test]
fn key_negative() {
let h = TILE_SIZE as f32 / 2.0;
let keys: Vec<Key> = KeyIter::new::<TILE_SIZE>(Aabb2d {
min: vec2(-h, -h),
max: vec2(-h, -h),
})
.collect();
assert!(keys.contains(&(-1, -1)));
assert_eq!(keys.len(), 1);
}
#[test]
fn query_points() {
let mut db = SparseGrid2d::<TILE_SIZE>::default();
let e1 = Entity::from_raw_u32(1).unwrap();
let e2 = Entity::from_raw_u32(2).unwrap();
db.insert_point(vec2(0.5, 0.5), e1);
db.insert_point(vec2(0.499, 0.501), e2);
let matches: HashSet<_> = db.point_iter(vec2(0.499, 0.501)).collect();
assert!(matches.contains(&e1));
assert!(matches.contains(&e2));
assert_eq!(matches.len(), 2);
}
#[test]
fn query_points_negative() {
let mut db = SparseGrid2d::<TILE_SIZE>::default();
let e1 = Entity::from_raw_u32(1).unwrap();
let e2 = Entity::from_raw_u32(2).unwrap();
db.insert_point(vec2(0.5, 0.5), e1);
db.insert_point(vec2(-0.5, -0.5), e2);
let matches: HashSet<_> = db.point_iter(vec2(-0.5, -0.5)).collect();
assert!(!matches.contains(&e1));
assert!(matches.contains(&e2));
assert_eq!(matches.len(), 1);
}
#[test]
fn matches_complex() {
let h = TILE_SIZE as f32 / 2.0;
let e1 = Entity::from_raw_u32(1).unwrap();
let e2 = Entity::from_raw_u32(2).unwrap();
let e3 = Entity::from_raw_u32(3).unwrap();
let mut db: SparseGrid2d = default();
db.insert_aabb(
Aabb2d {
min: vec2(-h, -h),
max: vec2(h, h),
},
e1,
);
db.insert_aabb(
Aabb2d {
min: vec2(h, h),
max: vec2(h, h),
},
e2,
);
db.insert_aabb(
Aabb2d {
min: vec2(-h, -h),
max: vec2(-h, -h),
},
e3,
);
let matches: Vec<Entity> = db
.aabb_iter(Aabb2d {
min: vec2(-h, -h),
max: vec2(h, h),
})
.collect();
assert!(matches.contains(&e1));
assert!(matches.contains(&e2));
assert!(matches.contains(&e3));
let matches = db.query_aabb(Aabb2d {
min: vec2(-0.001, -0.001),
max: vec2(-0.001, -0.001),
});
assert_eq!(matches.len(), 2);
assert!(matches.contains(&e1));
assert!(matches.contains(&e3));
let matches: Vec<Entity> = db
.aabb_iter(Aabb2d {
min: vec2(-0.001, -0.001),
max: vec2(-0.001, -0.001),
})
.collect();
assert_eq!(matches[0], e1);
}
#[test]
fn query_points_tilesize_10() {
let mut db = SparseGrid2d::<10>::default();
let e1 = Entity::from_raw_u32(1).unwrap();
let e2 = Entity::from_raw_u32(2).unwrap();
let e3 = Entity::from_raw_u32(3).unwrap();
db.insert_point(vec2(12f32, 15f32), e1);
db.insert_point(vec2(15f32, 12f32), e2);
db.insert_point(vec2(15f32, 20f32), e3);
let matches: HashSet<_> = db.point_iter(vec2(19.9, 19.9)).collect();
assert!(matches.contains(&e1));
assert!(matches.contains(&e2));
assert!(!matches.contains(&e3));
assert_eq!(matches.len(), 2);
}
}