178 lines
4.4 KiB
TypeScript
178 lines
4.4 KiB
TypeScript
import type { Coord2D, Dimension2D } from "../interfaces";
|
|
|
|
interface BoxedEntry {
|
|
id: number;
|
|
dimension: Dimension2D;
|
|
center: Coord2D;
|
|
}
|
|
|
|
enum Quadrant {
|
|
I,
|
|
II,
|
|
III,
|
|
IV,
|
|
}
|
|
|
|
export class QuadTree {
|
|
private maxLevels: number;
|
|
private splitThreshold: number;
|
|
private level: number;
|
|
private topLeft: Coord2D;
|
|
private dimension: Dimension2D;
|
|
|
|
private children: Map<Quadrant, QuadTree>;
|
|
private objects: BoxedEntry[];
|
|
|
|
constructor(
|
|
topLeft: Coord2D,
|
|
dimension: Dimension2D,
|
|
maxLevels: number,
|
|
splitThreshold: number,
|
|
level?: number,
|
|
) {
|
|
this.children = new Map<Quadrant, QuadTree>();
|
|
this.objects = [];
|
|
|
|
this.maxLevels = maxLevels;
|
|
this.splitThreshold = splitThreshold;
|
|
this.level = level ?? 0;
|
|
|
|
this.topLeft = topLeft;
|
|
this.dimension = dimension;
|
|
}
|
|
|
|
public insert(id: number, dimension: Dimension2D, center: Coord2D): void {
|
|
const box: BoxedEntry = { id, center, dimension };
|
|
if (this.hasChildren()) {
|
|
this.getQuadrants(box).forEach((quadrant) => {
|
|
const quadrantBox = this.children.get(quadrant);
|
|
quadrantBox?.insert(id, dimension, center);
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.objects.push({ id, dimension, center });
|
|
|
|
if (
|
|
this.objects.length > this.splitThreshold &&
|
|
this.level < this.maxLevels
|
|
) {
|
|
if (!this.hasChildren()) {
|
|
this.performSplit();
|
|
}
|
|
this.realignObjects();
|
|
}
|
|
}
|
|
|
|
public clear(): void {
|
|
this.objects = [];
|
|
if (this.hasChildren()) {
|
|
this.children.forEach((child) => child.clear());
|
|
this.children.clear();
|
|
}
|
|
}
|
|
|
|
public getNeighborIds(boxedEntry: BoxedEntry): number[] {
|
|
const neighbors: number[] = this.objects.map(({ id }) => id);
|
|
|
|
if (this.hasChildren()) {
|
|
this.getQuadrants(boxedEntry).forEach((quadrant) => {
|
|
const quadrantBox = this.children.get(quadrant);
|
|
|
|
quadrantBox
|
|
?.getNeighborIds(boxedEntry)
|
|
.forEach((id) => neighbors.push(id));
|
|
});
|
|
}
|
|
|
|
return neighbors;
|
|
}
|
|
|
|
private performSplit(): void {
|
|
const halfWidth = this.dimension.width / 2;
|
|
const halfHeight = this.dimension.height / 2;
|
|
|
|
(
|
|
[
|
|
[Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }],
|
|
[Quadrant.II, { ...this.topLeft }],
|
|
[Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
|
|
[
|
|
Quadrant.IV,
|
|
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight },
|
|
],
|
|
] as [[Quadrant, Coord2D]]
|
|
).forEach(([quadrant, pos]) => {
|
|
this.children.set(
|
|
quadrant,
|
|
new QuadTree(
|
|
pos,
|
|
{ width: halfWidth, height: halfHeight },
|
|
this.maxLevels,
|
|
this.splitThreshold,
|
|
this.level + 1,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
|
|
const treeCenter: Coord2D = {
|
|
x: this.topLeft.x + this.dimension.width / 2,
|
|
y: this.topLeft.y + this.dimension.height / 2,
|
|
};
|
|
|
|
return (
|
|
[
|
|
[
|
|
Quadrant.I,
|
|
(x: number, y: number) => x >= treeCenter.x && y < treeCenter.y,
|
|
],
|
|
[
|
|
Quadrant.II,
|
|
(x: number, y: number) => x < treeCenter.x && y < treeCenter.y,
|
|
],
|
|
[
|
|
Quadrant.III,
|
|
(x: number, y: number) => x < treeCenter.x && y >= treeCenter.y,
|
|
],
|
|
[
|
|
Quadrant.IV,
|
|
(x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y,
|
|
],
|
|
] as [[Quadrant, (x: number, y: number) => boolean]]
|
|
)
|
|
.filter(
|
|
([_quadrant, condition]) =>
|
|
condition(
|
|
boxedEntry.center.x + boxedEntry.dimension.width / 2,
|
|
boxedEntry.center.y + boxedEntry.dimension.height / 2,
|
|
) ||
|
|
condition(
|
|
boxedEntry.center.x - boxedEntry.dimension.width / 2,
|
|
boxedEntry.center.y - boxedEntry.dimension.height / 2,
|
|
),
|
|
)
|
|
.map(([quadrant]) => quadrant);
|
|
}
|
|
|
|
private realignObjects(): void {
|
|
this.objects.forEach((boxedEntry) => {
|
|
this.getQuadrants(boxedEntry).forEach((direction) => {
|
|
const quadrant = this.children.get(direction);
|
|
quadrant?.insert(
|
|
boxedEntry.id,
|
|
boxedEntry.dimension,
|
|
boxedEntry.center,
|
|
);
|
|
});
|
|
});
|
|
|
|
this.objects = [];
|
|
}
|
|
|
|
private hasChildren() {
|
|
return this.children && this.children.size > 0;
|
|
}
|
|
}
|