use std::collections::HashSet;
use crate::{BoundingRect, PointI32};
pub struct TileMap<T> {
tiles: Vec<Vec<T>>, tile_size: i32,
cols: i32, rows: i32, }
impl<T> TileMap<T> {
pub fn new(width: i32, height: i32) -> Self {
let tile_size = width.max(height) / 10;
Self::with_tile_size(width, height, tile_size)
}
pub fn with_tile_size(width: i32, height: i32, tile_size: i32) -> Self {
assert!(tile_size > 0, "tile_size must be positive");
let cols = (width + tile_size - 1) / tile_size;
let rows = (height + tile_size - 1) / tile_size;
let count = (cols * rows) as usize;
Self {
tiles: (0..count).map(|_| Vec::new()).collect(),
tile_size,
cols,
rows,
}
}
pub fn add_point(&mut self, p: PointI32, value: T) {
let (tx, ty) = self.tile_coords(p);
let idx = self.tile_index(tx, ty);
self.tiles[idx].push(value);
}
pub fn add_rect(&mut self, rect: &BoundingRect, value: T) where T: Copy {
let tx_min = (rect.left / self.tile_size).clamp(0, self.cols - 1);
let tx_max = (rect.right / self.tile_size).clamp(0, self.cols - 1);
let ty_min = (rect.top / self.tile_size).clamp(0, self.rows - 1);
let ty_max = (rect.bottom / self.tile_size).clamp(0, self.rows - 1);
let mut seen: HashSet<usize> = HashSet::new();
for tx in tx_min..=tx_max {
seen.insert(self.tile_index(tx, ty_min));
seen.insert(self.tile_index(tx, ty_max));
}
for ty in ty_min..=ty_max {
seen.insert(self.tile_index(tx_min, ty));
seen.insert(self.tile_index(tx_max, ty));
}
for idx in seen {
self.tiles[idx].push(value);
}
}
pub fn query_point(&self, p: &PointI32) -> &[T] {
let (tx, ty) = self.tile_coords(*p);
let idx = self.tile_index(tx, ty);
&self.tiles[idx]
}
pub fn query_rect(&self, rect: &BoundingRect) -> Vec<&[T]> {
let tx_min = (rect.left / self.tile_size).clamp(0, self.cols - 1);
let tx_max = (rect.right / self.tile_size).clamp(0, self.cols - 1);
let ty_min = (rect.top / self.tile_size).clamp(0, self.rows - 1);
let ty_max = (rect.bottom / self.tile_size).clamp(0, self.rows - 1);
let mut seen: HashSet<usize> = HashSet::new();
for tx in tx_min..=tx_max {
seen.insert(self.tile_index(tx, ty_min));
seen.insert(self.tile_index(tx, ty_max));
}
for ty in ty_min..=ty_max {
seen.insert(self.tile_index(tx_min, ty));
seen.insert(self.tile_index(tx_max, ty));
}
seen.into_iter()
.filter_map(|idx| {
let bucket = &self.tiles[idx];
if bucket.is_empty() { None } else { Some(bucket.as_slice()) }
})
.collect()
}
fn tile_coords(&self, p: PointI32) -> (i32, i32) {
let tx = (p.x / self.tile_size).clamp(0, self.cols - 1);
let ty = (p.y / self.tile_size).clamp(0, self.rows - 1);
(tx, ty)
}
fn tile_index(&self, tx: i32, ty: i32) -> usize {
(ty * self.cols + tx) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(left: i32, top: i32, right: i32, bottom: i32) -> BoundingRect {
BoundingRect { left, top, right, bottom }
}
#[test]
fn new_computes_grid_dimensions() {
let map = TileMap::<u32>::new(1920, 1080);
assert_eq!(map.tile_size, 192);
assert_eq!(map.cols, 10);
assert_eq!(map.rows, 6);
assert_eq!(map.tiles.len(), 60);
}
#[test]
fn with_tile_size_override() {
let map = TileMap::<u32>::with_tile_size(100, 100, 10);
assert_eq!(map.tile_size, 10);
assert_eq!(map.cols, 10);
assert_eq!(map.rows, 10);
}
#[test]
fn add_point_and_query_point_roundtrip() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(5, 5), 42u32);
assert_eq!(map.query_point(&PointI32::new(5, 5)), &[42]);
}
#[test]
fn query_point_same_tile_returns_all() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(0, 0), 1u32);
map.add_point(PointI32::new(9, 9), 2u32); map.add_point(PointI32::new(10, 0), 3u32); let result = map.query_point(&PointI32::new(0, 0));
assert_eq!(result, &[1, 2]);
}
#[test]
fn query_point_empty_tile() {
let map = TileMap::<u32>::with_tile_size(100, 100, 10);
assert!(map.query_point(&PointI32::new(50, 50)).is_empty());
}
#[test]
fn point_on_tile_boundary_goes_to_lower_tile() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(10, 0), 99u32);
assert!(map.query_point(&PointI32::new(0, 0)).is_empty());
assert_eq!(map.query_point(&PointI32::new(10, 0)), &[99]);
}
#[test]
fn out_of_bounds_point_clamped() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(200, 200), 7u32); assert_eq!(map.query_point(&PointI32::new(99, 99)), &[7]);
}
#[test]
fn query_rect_single_tile() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(5, 5), 1u32);
let result = map.query_rect(&rect(0, 0, 9, 9));
assert_eq!(result.len(), 1);
assert_eq!(result[0], &[1]);
}
#[test]
fn query_rect_spans_multiple_tiles() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(5, 5), 1u32); map.add_point(PointI32::new(15, 5), 2u32); map.add_point(PointI32::new(25, 5), 3u32); map.add_point(PointI32::new(50, 50), 9u32); let result = map.query_rect(&rect(0, 0, 29, 9));
let mut flat: Vec<u32> = result.iter().flat_map(|s| s.iter().copied()).collect();
flat.sort();
assert_eq!(flat, vec![1, 2, 3]);
}
#[test]
fn query_rect_skips_interior_tiles() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(55, 55), 42u32); let result = map.query_rect(&rect(0, 0, 99, 99));
let flat: Vec<u32> = result.iter().flat_map(|s| s.iter().copied()).collect();
assert!(!flat.contains(&42), "interior tile should not appear in query_rect");
}
#[test]
fn add_rect_inserts_into_perimeter_tiles() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_rect(&rect(0, 0, 29, 29), 7u32);
assert!(!map.query_point(&PointI32::new(15, 15)).is_empty() == false,
"interior tile (1,1) should be empty");
assert_eq!(map.query_point(&PointI32::new(5, 5)), &[7]); assert_eq!(map.query_point(&PointI32::new(25, 5)), &[7]); assert_eq!(map.query_point(&PointI32::new(5, 25)), &[7]); assert_eq!(map.query_point(&PointI32::new(25, 25)), &[7]); assert_eq!(map.query_point(&PointI32::new(15, 5)), &[7]); assert_eq!(map.query_point(&PointI32::new(5, 15)), &[7]); }
#[test]
fn add_rect_single_tile_no_duplicate() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_rect(&rect(0, 0, 5, 5), 3u32);
assert_eq!(map.query_point(&PointI32::new(0, 0)), &[3]);
}
#[test]
fn add_rect_skips_interior() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_rect(&rect(0, 0, 99, 99), 1u32);
assert!(map.query_point(&PointI32::new(55, 55)).is_empty());
}
#[test]
fn intersecting_large_rects_no_shared_corner_tile() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_rect(&rect(0, 0, 49, 49), 1u32); map.add_rect(&rect(70, 70, 89, 89), 2u32);
let results = map.query_rect(&rect(15, 15, 59, 59)); let flat: Vec<u32> = results.iter().flat_map(|s| s.iter().copied()).collect();
assert!(flat.contains(&1), "rect A should be found via shared perimeter tile (4,1)");
assert!(!flat.contains(&2), "rect C is disjoint from B and must not appear");
}
#[test]
fn query_rect_no_duplicates() {
let mut map = TileMap::with_tile_size(100, 100, 10);
map.add_point(PointI32::new(0, 0), 1u32);
let result = map.query_rect(&rect(0, 0, 5, 5));
assert_eq!(result.len(), 1);
}
}