import {clamp, intersects} from '@/util/math.js'; export default class SpatialHash { constructor({x, y}) { this.area = {x, y}; this.chunkSize = {x: 64, y: 64}; this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x)) .fill(0) .map(() => ( Array(Math.ceil(this.area.y / this.chunkSize.y)) .fill(0) .map(() => new Map()) )); this.data = new Map(); } chunkIndex(x, y) { return { x: clamp(Math.floor(x / this.chunkSize.x), 0, this.chunks.length - 1), y: clamp(Math.floor(y / this.chunkSize.y), 0, this.chunks[0].length - 1), }; } remove(datum) { if (!this.data.has(datum)) { return; } for (const {x, y} of this.data.get(datum).chunks) { this.chunks[x][y].delete(datum); } this.data.delete(datum); } update({x0, x1, y0, y1}, datum) { this.remove(datum); const [sx0, sx1] = x0 < x1 ? [x0, x1] : [x1, x0]; const [sy0, sy1] = y0 < y1 ? [y0, y1] : [y1, y0]; const {x: cx0, y: cy0} = this.chunkIndex(sx0, sy0); const {x: cx1, y: cy1} = this.chunkIndex(sx1, sy1); const chunks = []; for (let iy = cy0; iy <= cy1; ++iy) { for (let ix = cx0; ix <= cx1; ++ix) { const chunk = this.chunks[ix][iy]; if (!chunk.has(datum)) { chunk.set(datum, true); } chunks.push({x: ix, y: iy}); } } this.data.set( datum, { bounds: {x0: sx0, x1: sx1, y0: sy0, y1: sy1}, chunks, }, ); } within(query) { const {x0, x1, y0, y1} = query; const [sx0, sx1] = x0 < x1 ? [x0, x1] : [x1, x0]; const [sy0, sy1] = y0 < y1 ? [y0, y1] : [y1, y0]; const {x: cx0, y: cy0} = this.chunkIndex(sx0, sy0); const {x: cx1, y: cy1} = this.chunkIndex(sx1, sy1); const candidates = new Set(); const within = new Set(); for (let cy = cy0; cy <= cy1; ++cy) { for (let cx = cx0; cx <= cx1; ++cx) { for (const [datum] of this.chunks[cx][cy]) { candidates.add(datum); } } } for (const datum of candidates) { if (intersects(this.data.get(datum).bounds, query)) { within.add(datum); } } return within; } }