Skip to content

Commit

Permalink
Some work on spatial hash data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
mattkleiny committed Jul 10, 2024
1 parent 80f294d commit 5115ec4
Showing 1 changed file with 145 additions and 100 deletions.
245 changes: 145 additions & 100 deletions common/src/collections/spatialhash.rs
Original file line number Diff line number Diff line change
@@ -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<T, S = RandomState> {
entries: HashMap<IVec2, Vec<Entry<T>>, S>,
default_capacity: usize,
pub struct SpatialHashMap<T, S = RandomState> {
resolution: f32,
values: Arena<EntryIndex, Entry<T>>,
lookup: HashMap<IVec2, Vec<EntryIndex>, S>,
}

/// A single entry in a spatial hash grid.
struct Entry<T> {
value: T,
shape: SpatialShape,
}

impl<T> Default for SpatialHashGrid<T> {
impl<T> Default for SpatialHashMap<T> {
fn default() -> Self {
Self {
entries: HashMap::default(),
default_capacity: 1,
resolution: 100.0,
values: Arena::default(),
lookup: HashMap::default(),
}
}
}

impl<T> SpatialHashGrid<T> {
/// Creates a new spatial hash grid.
impl<T> SpatialHashMap<T> {
/// 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<Item = &T> {
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<Item = &T> {
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<Item = &T> {
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<Item = &mut T> {
self
.entries
.values_mut()
.flat_map(|contents| contents.iter_mut().map(|entry| &mut entry.value))
struct Iter<'a, T> {
indices: Vec<EntryIndex>,
values: &'a mut Arena<EntryIndex, Entry<T>>,
marker: std::marker::PhantomData<&'a T>,
}

impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a mut T;

fn next(&mut self) -> Option<Self::Item> {
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<T> {
impl<'a, T> IntoIterator for &'a SpatialHashMap<T> {
type Item = &'a T;
type IntoIter = impl Iterator<Item = Self::Item>;

Expand All @@ -108,7 +191,7 @@ impl<'a, T> IntoIterator for &'a SpatialHashGrid<T> {
}
}

impl<'a, T> IntoIterator for &'a mut SpatialHashGrid<T> {
impl<'a, T> IntoIterator for &'a mut SpatialHashMap<T> {
type Item = &'a mut T;
type IntoIter = impl Iterator<Item = Self::Item>;

Expand All @@ -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);
}
}

0 comments on commit 5115ec4

Please sign in to comment.