From 5115ec4619733d4de3c429dae8d48e840a8d9b34 Mon Sep 17 00:00:00 2001 From: Matt Kleinschafer Date: Wed, 10 Jul 2024 16:16:08 +1000 Subject: [PATCH] Some work on spatial hash data structure --- common/src/collections/spatialhash.rs | 245 +++++++++++++++----------- 1 file changed, 145 insertions(+), 100 deletions(-) diff --git a/common/src/collections/spatialhash.rs b/common/src/collections/spatialhash.rs index eccfb5f6..2540fd7d 100644 --- a/common/src/collections/spatialhash.rs +++ b/common/src/collections/spatialhash.rs @@ -1,105 +1,188 @@ use std::{collections::HashMap, hash::RandomState}; -use crate::maths::{ivec2, IVec2}; +use crate::{ + impl_arena_index, + maths::{ivec2, IVec2, Vec2}, + unsafe_mutable_alias, Arena, +}; + +// Internal index for a spatial hash map entry. +impl_arena_index!(EntryIndex); + +/// A shape in a spatial hash map. +pub enum SpatialShape { + Circle { center: Vec2, radius: f32 }, + AABB { min: Vec2, max: Vec2 }, +} -/// A sparse spatial hash grid implementation. +impl SpatialShape { + /// Determines if this shape intersects the given other shape + pub fn intersects(&self, other: &SpatialShape) -> bool { + todo!() + } +} + +/// A sparse spatial hash map implementation. /// /// This implementation uses a hashmap to bucket items based on their position. /// This allows for fast insertion and removal of items, but iteration is slower /// than a dense spatial hash grid. -pub struct SpatialHashGrid { - entries: HashMap>, S>, - default_capacity: usize, +pub struct SpatialHashMap { + resolution: f32, + values: Arena>, + lookup: HashMap, S>, } /// A single entry in a spatial hash grid. struct Entry { value: T, + shape: SpatialShape, } -impl Default for SpatialHashGrid { +impl Default for SpatialHashMap { fn default() -> Self { Self { - entries: HashMap::default(), - default_capacity: 1, + resolution: 100.0, + values: Arena::default(), + lookup: HashMap::default(), } } } -impl SpatialHashGrid { - /// Creates a new spatial hash grid. +impl SpatialHashMap { + /// Creates a new spatial hash map. pub fn new() -> Self { Self::default() } - /// Creates a new spatial hash grid with a default capacity. - pub fn with_default_capacity(capacity: usize) -> Self { + /// Creates a new spatial hash map with the specified resolution. + pub fn with_resolution(resolution: f32) -> Self { Self { - entries: HashMap::default(), - default_capacity: capacity, + resolution, + values: Arena::default(), + lookup: HashMap::default(), } } - /// Returns true if the spatial hash grid is empty. + /// Returns true if the spatial hash map is empty. pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Returns the number of elements in the spatial hash grid. + /// Returns the number of elements in the spatial hash map. pub fn len(&self) -> usize { - self.entries.values().map(|contents| contents.len()).sum() + self.lookup.values().map(|contents| contents.len()).sum() } - /// The number of cells in the spatial hash grid. + /// The number of cells in the spatial hash map. pub fn cells(&self) -> usize { - self.entries.len() - } - - /// Adds an item to the spatial hash grid. - pub fn add(&mut self, x: i32, y: i32, value: T) { - let point = ivec2(x, y); - let entry = Entry { value }; - + self.lookup.len() + } + + /// Adds an item to the spatial hash map. + pub fn add(&mut self, shape: SpatialShape, value: T) { + match &shape { + SpatialShape::Circle { center, radius } => { + let min = (*center - Vec2::splat(*radius)).floor(); + let max = (*center + Vec2::splat(*radius)).ceil(); + + self.add(SpatialShape::AABB { min, max }, value); + } + SpatialShape::AABB { min, max } => { + let min = (*min / self.resolution).floor(); + let max = (*max / self.resolution).ceil(); + let capacity = max.x as usize - min.x as usize; + + let entry = Entry { value, shape }; + let index = self.values.insert(entry); + + for x in min.x as i32..max.x as i32 { + for y in min.y as i32..max.y as i32 { + let point = ivec2(x, y); + + self + .lookup + .entry(point) + .or_insert_with(|| Vec::with_capacity(capacity)) + .push(index); + } + } + } + }; + } + + /// Queries for all entries in the given shape + pub fn query(&self, shape: SpatialShape) -> impl Iterator { self - .entries - .entry(point) - .or_insert_with(|| Vec::with_capacity(self.default_capacity)) - .push(entry); + .values + .iter() + .filter(move |it| it.shape.intersects(&shape)) + .map(|it| &it.value) } - /// Removes all items at the given cell in the list. - pub fn remove_all(&mut self, x: i32, y: i32) { - let point = ivec2(x, y); - - if let Some(entries) = self.entries.get_mut(&point) { - entries.clear(); - self.entries.remove(&point); - } + /// Mutably queries for all entries in the given shape + pub fn query_mut(&mut self, shape: SpatialShape) -> impl Iterator { + self + .values + .iter_mut() + .filter(move |it| it.shape.intersects(&shape)) + .map(|it| &it.value) } - /// Clears the spatial hash grid of all entries. + /// Clears the spatial hash map of all entries. pub fn clear(&mut self) { - self.entries.clear(); + self.lookup.clear(); } - /// Returns an iterator over the items in the spatial hash grid. + /// Returns an iterator over the items in the spatial hash map. pub fn iter(&self) -> impl Iterator { self - .entries + .lookup .values() - .flat_map(|contents| contents.iter().map(|entry| &entry.value)) + .flat_map(|contents| contents.iter()) + .map(|index| self.values.get(*index)) + .flat_map(|entry| entry.map(|it| &it.value)) } - /// Returns a mutable iterator over the items in the spatial hash grid. + /// Returns a mutable iterator over the items in the spatial hash map. pub fn iter_mut(&mut self) -> impl Iterator { - self - .entries - .values_mut() - .flat_map(|contents| contents.iter_mut().map(|entry| &mut entry.value)) + struct Iter<'a, T> { + indices: Vec, + values: &'a mut Arena>, + marker: std::marker::PhantomData<&'a T>, + } + + impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a mut T; + + fn next(&mut self) -> Option { + while let Some(index) = self.indices.pop() { + if let Some(entry) = self.values.get_mut(index) { + return Some(unsafe { unsafe_mutable_alias(&mut entry.value) }); + } + } + + None + } + } + + // collect indices in advance + let indices = self + .lookup + .values() + .flat_map(|contents| contents.iter()) + .map(|it| *it) + .collect(); + + Iter { + indices, + values: &mut self.values, + marker: std::marker::PhantomData, + } } } -impl<'a, T> IntoIterator for &'a SpatialHashGrid { +impl<'a, T> IntoIterator for &'a SpatialHashMap { type Item = &'a T; type IntoIter = impl Iterator; @@ -108,7 +191,7 @@ impl<'a, T> IntoIterator for &'a SpatialHashGrid { } } -impl<'a, T> IntoIterator for &'a mut SpatialHashGrid { +impl<'a, T> IntoIterator for &'a mut SpatialHashMap { type Item = &'a mut T; type IntoIter = impl Iterator; @@ -122,55 +205,17 @@ mod tests { use super::*; #[test] - fn test_grid_behaviour() { - let mut grid = SpatialHashGrid::default(); - - grid.add(0, 0, 1); - grid.add(0, 1, 2); - - assert_eq!(grid.len(), 2); // 2 items in the grid - } - - #[test] - fn test_is_empty() { - let mut grid = SpatialHashGrid::default(); - assert!(grid.is_empty()); - - grid.add(0, 0, 1); - assert!(!grid.is_empty()); - - grid.remove_all(0, 0); - assert!(grid.is_empty()); - } - - #[test] - fn test_cells() { - let mut grid = SpatialHashGrid::default(); - assert_eq!(grid.cells(), 0); - - grid.add(0, 0, 1); - assert_eq!(grid.cells(), 1); - - grid.add(1, 0, 2); - assert_eq!(grid.cells(), 2); - - grid.remove_all(0, 0); - assert_eq!(grid.cells(), 1); - } - - #[test] - fn test_clear() { - let mut grid = SpatialHashGrid::default(); - - grid.add(0, 0, 1); - grid.add(0, 1, 2); - - assert_eq!(grid.len(), 2); - assert_eq!(grid.cells(), 2); - - grid.clear(); - - assert_eq!(grid.len(), 0); - assert_eq!(grid.cells(), 0); + fn spatial_hash_map_should_insert_at_defined_resolution() { + let mut map = SpatialHashMap::new(); + + map.add( + SpatialShape::Circle { + center: Vec2::ZERO, + radius: 100.0, + }, + "Hello, World!", + ); + + assert_eq!(map.len(), 4); } }