jumpstorm/engine/structures/QuadTree.ts

185 lines
4.8 KiB
TypeScript
Raw Normal View History

2023-08-25 18:48:17 -04:00
import type { Coord2D, Dimension2D } from '../interfaces';
import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.';
2023-07-19 23:38:24 -04:00
enum Quadrant {
I,
II,
III,
2023-08-25 18:48:17 -04:00
IV
2023-07-19 23:38:24 -04:00
}
/*
unused due to performance problems. here anyways, in case it _really_ is necessary at some point
(and to justify the amount of time i spent here).
*/
export class QuadTree implements RefreshingCollisionFinderBehavior {
private static readonly QUADTREE_MAX_LEVELS = 3;
private static readonly QUADTREE_SPLIT_THRESHOLD = 2000;
2023-07-19 23:38:24 -04:00
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 = { x: 0, y: 0 },
2023-07-19 23:38:24 -04:00
dimension: Dimension2D,
maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS,
splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD,
2023-08-21 16:19:02 -04:00
level: number = 0
2023-07-19 23:38:24 -04:00
) {
2023-08-12 15:49:16 -04:00
this.children = new Map<Quadrant, QuadTree>();
2023-07-19 23:38:24 -04:00
this.objects = [];
this.maxLevels = maxLevels;
this.splitThreshold = splitThreshold;
this.level = level;
2023-08-12 15:49:16 -04:00
this.topLeft = topLeft;
this.dimension = dimension;
2023-07-19 23:38:24 -04:00
}
2023-08-13 18:47:58 -04:00
public insert(boxedEntry: BoxedEntry): void {
2023-07-19 23:38:24 -04:00
if (this.hasChildren()) {
2023-08-13 18:47:58 -04:00
this.getQuadrants(boxedEntry).forEach((quadrant) => {
2023-08-12 15:49:16 -04:00
const quadrantBox = this.children.get(quadrant);
quadrantBox!.insert(boxedEntry);
2023-08-12 15:49:16 -04:00
});
2023-07-19 23:38:24 -04:00
return;
}
2023-08-13 18:47:58 -04:00
this.objects.push(boxedEntry);
2023-07-19 23:38:24 -04:00
if (
this.objects.length > this.splitThreshold &&
this.level < this.maxLevels
) {
if (!this.hasChildren()) {
this.performSplit();
}
this.realignObjects();
}
}
public clear(): void {
this.objects = [];
2023-08-13 18:47:58 -04:00
2023-07-19 23:38:24 -04:00
if (this.hasChildren()) {
this.children.forEach((child) => child.clear());
this.children.clear();
}
}
2023-08-21 16:19:02 -04:00
public getNeighborIds(boxedEntry: BoxedEntry): Set<string> {
const neighbors = new Set<string>(
2023-08-21 16:19:02 -04:00
this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id)
);
2023-07-19 23:38:24 -04:00
if (this.hasChildren()) {
this.getQuadrants(boxedEntry).forEach((quadrant) => {
2023-08-12 15:49:16 -04:00
const quadrantBox = this.children.get(quadrant);
quadrantBox
?.getNeighborIds(boxedEntry)
.forEach((id) => neighbors.add(id));
2023-07-19 23:38:24 -04:00
});
}
return neighbors;
}
private performSplit(): void {
const halfWidth = this.dimension.width / 2;
const halfHeight = this.dimension.height / 2;
2023-08-12 15:49:16 -04:00
(
2023-07-19 23:38:24 -04:00
[
2023-08-12 15:49:16 -04:00
[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,
2023-08-25 18:48:17 -04:00
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }
]
2023-08-21 16:19:02 -04:00
] as [Quadrant, Coord2D][]
2023-08-12 15:49:16 -04:00
).forEach(([quadrant, pos]) => {
2023-07-19 23:38:24 -04:00
this.children.set(
quadrant,
new QuadTree(
pos,
{ width: halfWidth, height: halfHeight },
this.maxLevels,
this.splitThreshold,
2023-08-21 16:19:02 -04:00
this.level + 1
)
2023-07-19 23:38:24 -04:00
);
});
}
2023-08-12 15:49:16 -04:00
private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
2023-07-19 23:38:24 -04:00
const treeCenter: Coord2D = {
x: this.topLeft.x + this.dimension.width / 2,
2023-08-25 18:48:17 -04:00
y: this.topLeft.y + this.dimension.height / 2
2023-07-19 23:38:24 -04:00
};
2023-08-12 15:49:16 -04:00
return (
[
[
Quadrant.I,
2023-08-25 18:48:17 -04:00
(x: number, y: number) => x >= treeCenter.x && y < treeCenter.y
2023-08-12 15:49:16 -04:00
],
[
Quadrant.II,
2023-08-25 18:48:17 -04:00
(x: number, y: number) => x < treeCenter.x && y < treeCenter.y
2023-08-12 15:49:16 -04:00
],
[
Quadrant.III,
2023-08-25 18:48:17 -04:00
(x: number, y: number) => x < treeCenter.x && y >= treeCenter.y
2023-08-12 15:49:16 -04:00
],
[
Quadrant.IV,
2023-08-25 18:48:17 -04:00
(x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y
]
2023-08-21 16:19:02 -04:00
] as [Quadrant, (x: number, y: number) => boolean][]
2023-08-12 15:49:16 -04:00
)
2023-07-19 23:38:24 -04:00
.filter(
([_quadrant, condition]) =>
condition(
boxedEntry.center.x + boxedEntry.dimension.width / 2,
2023-08-21 16:19:02 -04:00
boxedEntry.center.y + boxedEntry.dimension.height / 2
2023-07-19 23:38:24 -04:00
) ||
condition(
boxedEntry.center.x - boxedEntry.dimension.width / 2,
2023-08-21 16:19:02 -04:00
boxedEntry.center.y - boxedEntry.dimension.height / 2
)
2023-07-19 23:38:24 -04:00
)
.map(([quadrant]) => quadrant);
}
private realignObjects(): void {
this.objects.forEach((boxedEntry) => {
this.getQuadrants(boxedEntry).forEach((quadrant) => {
const quadrantBox = this.children.get(quadrant);
quadrantBox!.insert(boxedEntry);
2023-07-19 23:38:24 -04:00
});
});
this.objects = [];
}
private hasChildren() {
2023-08-12 15:49:16 -04:00
return this.children && this.children.size > 0;
2023-07-19 23:38:24 -04:00
}
public setTopLeft(topLeft: Coord2D) {
this.topLeft = topLeft;
}
public setDimension(dimension: Dimension2D) {
this.dimension = dimension;
}
2023-07-19 23:38:24 -04:00
}