spatial-grid-2d
Version:
A spatial partitioning 2D grid for efficient many-to-many collision detection and such.
263 lines (262 loc) • 12 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _SpatialGrid_grid;
/**
* A spatial grid for efficiently managing and querying objects in a 2D space.
*
* The grid divides the space into tiles of a fixed size, allowing objects to be registered
* to specific tiles. This enables efficient spatial queries, such as finding objects
* within a circle, rectangle, or along a line segment.
*/
export class SpatialGrid {
constructor({ width, height, tileSize, debug = false }) {
this.objects = [];
this.debug = false;
this.lastCheckedTiles = [];
/**
* A flattened grid of xTiles * yTiles that contains list of the objects registered to each tile
*/
_SpatialGrid_grid.set(this, []);
this.width = width;
this.height = height;
this.tileSize = tileSize;
this.debug = debug;
}
/**
* Adds objects to the grid.
*/
add(...objects) {
this.objects.push(...objects);
}
/**
* Removes objects from the grid.
*/
remove(...objects) {
this.objects = this.objects.filter(obj => !objects.includes(obj));
}
/**
* Returns the number of horizontal tiles.
*/
get xTiles() {
return Math.floor(this.width / this.tileSize);
}
/**
* Returns the number of vertical tiles.
*/
get yTiles() {
return Math.floor(this.height / this.tileSize);
}
/**
* Updates the grid by assigning objects to their respective tiles.
*/
update() {
// Clear the grid
__classPrivateFieldSet(this, _SpatialGrid_grid, Array.from({ length: this.xTiles * this.yTiles }, () => []), "f");
// Iterate over all objects and place them in the appropriate tiles
for (const obj of this.objects) {
const startX = Math.floor((obj.left ?? (obj.x - (obj.radius ?? 0))) / this.tileSize);
const startY = Math.floor((obj.top ?? (obj.y - (obj.radius ?? 0))) / this.tileSize);
const endX = Math.floor((obj.right ?? (obj.x + (obj.radius ?? 0))) / this.tileSize);
const endY = Math.floor((obj.bottom ?? (obj.y + (obj.radius ?? 0))) / this.tileSize);
for (let x = startX; x <= endX; x++) {
for (let y = startY; y <= endY; y++) {
if (x >= 0 && x < this.xTiles && y >= 0 && y < this.yTiles) {
const tileIndex = y * this.xTiles + x;
__classPrivateFieldGet(this, _SpatialGrid_grid, "f")[tileIndex].push(obj);
}
}
}
}
}
/**
* Returns objects in the tile at (x, y) and adjacent tiles based on neighborTiles.
*
* Example:
* `getNeighbors(126, 72, 1)` for a tileSize of `10` returns objects in tiles:
* ```
* [11,6][12,6][13,6]
* [11,7][12,7][13,7]
* [11,8][12,8][13,8]
* ```
*/
getNeighbors(x, y, neighborTiles = 1) {
// Use a Set to ensure unique objects
const uniqueNeighbors = new Set();
if (this.debug)
this.lastCheckedTiles = [];
// Calculate the tile coordinates of the given point
const tileX = Math.floor(x / this.tileSize);
const tileY = Math.floor(y / this.tileSize);
// Iterate over the range of tiles based on neighborTiles
for (let offsetX = -neighborTiles; offsetX <= neighborTiles; offsetX++) {
for (let offsetY = -neighborTiles; offsetY <= neighborTiles; offsetY++) {
const neighborX = tileX + offsetX;
const neighborY = tileY + offsetY;
// Ensure the neighbor tile is within bounds
if (neighborX >= 0 && neighborX < this.xTiles && neighborY >= 0 && neighborY < this.yTiles) {
if (this.debug)
this.lastCheckedTiles.push({ x: neighborX, y: neighborY });
const tileIndex = neighborY * this.xTiles + neighborX;
for (const obj of __classPrivateFieldGet(this, _SpatialGrid_grid, "f")[tileIndex]) {
uniqueNeighbors.add(obj);
}
}
}
}
return Array.from(uniqueNeighbors);
}
/**
* Returns objects intersecting a circle with the given center and radius.
*
* Note: Objects must have a `radius` property. If missing, they are treated as points.
*/
getObjectsIntersectingCircle(x, y, radius) {
if (this.debug)
this.lastCheckedTiles = [];
const candidates = this.getNeighbors(x, y, Math.ceil(radius / this.tileSize));
const intersectingObjects = [];
for (const obj of candidates) {
const objRadius = obj.radius || 0; // Treat missing radius as 0
const dx = obj.x - x;
const dy = obj.y - y;
const distanceSquared = dx * dx + dy * dy;
const radiiSum = objRadius + radius;
if (distanceSquared <= radiiSum * radiiSum) {
intersectingObjects.push(obj);
}
}
return intersectingObjects;
}
/**
* Returns objects intersecting a rectangle with the given position and dimensions.
*
* Note: Objects must have `left`, `right`, `top`, and `bottom` properties. If missing, they are treated as points using `x` and `y`.
*/
getObjectsIntersectingRect(x, y, width, height) {
if (this.debug)
this.lastCheckedTiles = [];
const intersectingObjects = [];
// Calculate the tile range for the rectangle
const startX = Math.floor(x / this.tileSize);
const startY = Math.floor(y / this.tileSize);
const endX = Math.floor((x + width) / this.tileSize);
const endY = Math.floor((y + height) / this.tileSize);
// Iterate over the tiles within the rectangle's bounds
for (let tileX = startX; tileX <= endX; tileX++) {
for (let tileY = startY; tileY <= endY; tileY++) {
if (tileX >= 0 && tileX < this.xTiles && tileY >= 0 && tileY < this.yTiles) {
if (this.debug)
this.lastCheckedTiles.push({ x: tileX, y: tileY });
const tileIndex = tileY * this.xTiles + tileX;
for (const obj of __classPrivateFieldGet(this, _SpatialGrid_grid, "f")[tileIndex]) {
const objLeft = obj.left ?? obj.x;
const objRight = obj.right ?? obj.x;
const objTop = obj.top ?? obj.y;
const objBottom = obj.bottom ?? obj.y;
// Check if the object intersects the rectangle
if (objRight >= x &&
objLeft <= x + width &&
objBottom >= y &&
objTop <= y + height) {
intersectingObjects.push(obj);
}
}
}
}
}
return intersectingObjects;
}
/**
* Returns objects intersecting a line segment with the given start, end, and width.
*/
getObjectsIntersectingLine(fromX, fromY, toX, toY, width = 0) {
if (this.debug)
this.lastCheckedTiles = [];
const neighborTiles = new Set();
// Bresenham's line algorithm to determine the tiles intersected by the line
let x0 = Math.floor(fromX / this.tileSize);
let y0 = Math.floor(fromY / this.tileSize);
const x1 = Math.floor(toX / this.tileSize);
const y1 = Math.floor(toY / this.tileSize);
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = x0 < x1 ? 1 : -1;
const sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
while (true) {
// Add the current tile to the set of neighbor tiles
if (x0 >= 0 && x0 < this.xTiles && y0 >= 0 && y0 < this.yTiles) {
neighborTiles.add({ x: x0, y: y0 });
if (this.debug)
this.lastCheckedTiles.push({ x: x0, y: y0 });
}
// Check if we've reached the end of the line
if (x0 === x1 && y0 === y1)
break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
// Expand the neighbor tiles based on the width
const expandedTiles = new Set();
const tileExpansion = Math.ceil(width / (2 * this.tileSize));
for (const tile of neighborTiles) {
for (let offsetX = -tileExpansion; offsetX <= tileExpansion; offsetX++) {
for (let offsetY = -tileExpansion; offsetY <= tileExpansion; offsetY++) {
const neighborX = tile.x + offsetX;
const neighborY = tile.y + offsetY;
if (neighborX >= 0 && neighborX < this.xTiles && neighborY >= 0 && neighborY < this.yTiles) {
expandedTiles.add({ x: neighborX, y: neighborY });
if (this.debug)
this.lastCheckedTiles.push({ x: neighborX, y: neighborY });
}
}
}
}
// Collect candidates from the expanded tiles, ensuring no duplicates
const candidatesSet = new Set();
for (const tile of expandedTiles) {
const tileIndex = tile.y * this.xTiles + tile.x;
for (const obj of __classPrivateFieldGet(this, _SpatialGrid_grid, "f")[tileIndex]) {
candidatesSet.add(obj);
}
}
const candidates = Array.from(candidatesSet);
const intersectingObjects = [];
const lineLengthSquared = (toX - fromX) ** 2 + (toY - fromY) ** 2;
for (const obj of candidates) {
const objRadius = obj.radius || 0;
const objX = obj.x;
const objY = obj.y;
// Calculate the closest point on the line segment to the object's center
const t = Math.max(0, Math.min(1, ((objX - fromX) * (toX - fromX) + (objY - fromY) * (toY - fromY)) / lineLengthSquared));
const closestX = fromX + t * (toX - fromX);
const closestY = fromY + t * (toY - fromY);
// Check if the distance from the closest point to the object's center is within the radius + width
const dx = objX - closestX;
const dy = objY - closestY;
const distanceSquared = dx * dx + dy * dy;
const effectiveRadius = objRadius + width / 2;
if (distanceSquared <= effectiveRadius * effectiveRadius) {
intersectingObjects.push(obj);
}
}
return intersectingObjects;
}
}
_SpatialGrid_grid = new WeakMap();