478 lines
17 KiB
TypeScript
478 lines
17 KiB
TypeScript
import type { CanvasContent } from "~/types/canvas";
|
|
import { clamp } from "./general.utils";
|
|
|
|
export function intersects(aLeft: number, aTop: number, aRight: number, aBottom: number, bLeft: number, bTop: number, bRight: number, bBottom: number): boolean
|
|
{
|
|
return aLeft <= bRight && aRight >= bLeft && aTop <= bBottom && aBottom >= bTop;
|
|
}
|
|
|
|
enum QuadConsts
|
|
{
|
|
ENodeINext = 0,
|
|
ENodeIElmt = 1,
|
|
ElmtILft = 0, ElmtIRgt = 1, ElmtITop = 2, ElmtIBtm = 3,
|
|
ElmtIId = 4,
|
|
NodeIFirst = 0,
|
|
NodeICount = 1,
|
|
PropCount = 6,
|
|
PropILft = 0, PropIRgt = 1, PropITop = 2, PropIBtm = 3,
|
|
PropIIdx = 4,
|
|
PropIDpt = 5,
|
|
}
|
|
|
|
const DEFAULT_SIZE = 128;
|
|
const MAX_BYTE_LENGTH = 1024*1024*16;
|
|
|
|
export interface AABB {
|
|
x1: number;
|
|
x2: number;
|
|
y1: number;
|
|
y2: number;
|
|
}
|
|
|
|
export class IntList
|
|
{
|
|
#data: Int32Array;
|
|
#fields: number;
|
|
#capacity: number = DEFAULT_SIZE;
|
|
#length: number = 0;
|
|
#free: number = -1;
|
|
|
|
constructor(fields: number)
|
|
{
|
|
if(fields <= 0)
|
|
throw new Error("Invalid field count");
|
|
|
|
this.#data = new Int32Array(new ArrayBuffer(this.#capacity * fields * Int32Array.BYTES_PER_ELEMENT, { maxByteLength: MAX_BYTE_LENGTH }));
|
|
this.#fields = fields;
|
|
}
|
|
get length(): number
|
|
{
|
|
return this.#length;
|
|
}
|
|
|
|
get(index: number, field: number): number
|
|
{
|
|
if(index >= this.#length || index < 0)
|
|
throw new Error("Invalid index");
|
|
|
|
if(field >= this.#fields || field < 0)
|
|
throw new Error("Invalid field");
|
|
|
|
return this.#data[index*this.#fields + field];
|
|
}
|
|
set(index: number, field: number, value: number): void
|
|
{
|
|
if(index >= this.#length || index < 0)
|
|
throw new Error("Invalid index");
|
|
|
|
if(field >= this.#fields || field < 0)
|
|
throw new Error("Invalid field");
|
|
|
|
this.#data[index*this.#fields + field] = value;
|
|
}
|
|
clear(): void
|
|
{
|
|
//Never edit the array during clear, we'll keep every data as they *must* be override before being read.
|
|
this.#length = 0;
|
|
this.#free = -1;
|
|
}
|
|
push(): number
|
|
{
|
|
const pos = (this.#length + 1) * this.#fields;
|
|
|
|
if(pos > this.#capacity * this.#fields)
|
|
{
|
|
this.#capacity *= 2;
|
|
|
|
if((this.#data.buffer as ArrayBuffer).resizable)
|
|
{
|
|
(this.#data.buffer as ArrayBuffer).resize(clamp(this.#capacity * this.#fields * Int32Array.BYTES_PER_ELEMENT, 1, MAX_BYTE_LENGTH));
|
|
}
|
|
else
|
|
{
|
|
throw new Error("Cannot resize the buffer");
|
|
}
|
|
}
|
|
|
|
return this.#length++;
|
|
}
|
|
pop(): void
|
|
{
|
|
if(this.#length <= 0)
|
|
return;
|
|
|
|
--this.#length;
|
|
}
|
|
insert(): number
|
|
{
|
|
if(this.#free !== -1)
|
|
{
|
|
const index = this.#free;
|
|
this.#free = this.#data[index * this.#fields];
|
|
return index;
|
|
}
|
|
else
|
|
return this.push();
|
|
}
|
|
erase(index: number): void
|
|
{
|
|
if(index >= this.#length || index < 0)
|
|
throw new Error("Invalid index");
|
|
|
|
this.#data[index * this.#fields] = this.#free;
|
|
this.#free = index;
|
|
}
|
|
|
|
toArray(): number[]
|
|
{
|
|
return [...this.#data.slice(0, this.#length)];
|
|
}
|
|
|
|
//DEBUG
|
|
/* printReadable(names: string[] = []): void
|
|
{
|
|
const size = this.#length * this.#fields;
|
|
for(let i = 0; i < size; i)
|
|
{
|
|
const obj: Record<string, number> = {};
|
|
for(let j = 0; j < this.#fields; ++i, ++j)
|
|
{
|
|
obj[names[j] ?? j] = this.#data[i];
|
|
}
|
|
console.log(obj);
|
|
}
|
|
} */
|
|
}
|
|
|
|
const _process = new IntList(1);
|
|
const _nodes = new IntList(QuadConsts.PropCount);
|
|
|
|
/**
|
|
* A node that contains elments is called a leaf. Its count is equals to the amount of children it holds.
|
|
* A node that contains other nodes is called a branch. Its count is equals to -1.
|
|
* The AABB of each node isn't stored and is computed on the go.
|
|
*/
|
|
export class Quadtree
|
|
{
|
|
#bounds: AABB;
|
|
#maxDepth: number = 8;
|
|
#maxElmts: number = 4;
|
|
#nodes: IntList = new IntList(2);
|
|
#enodes: IntList = new IntList(2);
|
|
#content: IntList = new IntList(5);
|
|
|
|
#ids: string[] = [];
|
|
|
|
ref: Ref<CanvasContent>;
|
|
|
|
constructor(bounds: AABB, ref: Ref<CanvasContent>, maxDepth?: number, maxElmts?: number)
|
|
{
|
|
this.#bounds = bounds;
|
|
this.#bounds.x1 = Math.round(this.#bounds.x1);
|
|
this.#bounds.x2 = Math.round(this.#bounds.x2);
|
|
this.#bounds.y1 = Math.round(this.#bounds.y1);
|
|
this.#bounds.y2 = Math.round(this.#bounds.y2);
|
|
|
|
this.#maxDepth = maxDepth ?? this.#maxDepth;
|
|
this.#maxElmts = maxElmts ?? this.#maxElmts;
|
|
|
|
this.ref = ref;
|
|
|
|
this.#nodes.insert();
|
|
this.#nodes.set(0, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(0, QuadConsts.NodeICount, 0);
|
|
}
|
|
fetch(x: number, y: number): string[]
|
|
{
|
|
return this.query({x1: x, x2: x, y1: y, y2: y});
|
|
}
|
|
query(aabb: AABB): string[]
|
|
{
|
|
_process.clear();
|
|
|
|
const leaves = this.#findLeaves(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, Math.floor(aabb.x1), Math.floor(aabb.x2), Math.floor(aabb.y1), Math.floor(aabb.y2));
|
|
|
|
const tmp: Record<number, boolean> = {};
|
|
|
|
for(let i = 0; i < leaves.length; ++i)
|
|
{
|
|
const index = leaves.get(i, QuadConsts.PropIIdx);
|
|
|
|
let node = this.#nodes.get(index, QuadConsts.NodeIFirst);
|
|
while(node !== -1)
|
|
{
|
|
const elmt = this.#enodes.get(node, QuadConsts.ENodeIElmt);
|
|
|
|
const idIndex = this.#content.get(elmt, QuadConsts.ElmtIId);
|
|
const id = this.#ids[idIndex];
|
|
const canvasNode = this.ref.value.nodes?.find(e => e.id === id)!;
|
|
|
|
if (!tmp[elmt] && intersects(aabb.x1, aabb.y1, aabb.x2, aabb.y2, canvasNode.x, canvasNode.y, canvasNode.x + canvasNode.width, canvasNode.y + canvasNode.height))
|
|
{
|
|
_process.set(_process.push(), 0, idIndex);
|
|
tmp[elmt] = true;
|
|
}
|
|
node = this.#enodes.get(node, QuadConsts.ENodeINext);
|
|
}
|
|
}
|
|
|
|
return _process.toArray().map(e => this.#ids[e]);
|
|
}
|
|
insert(id: string, aabb: AABB): number
|
|
{
|
|
const index = this.#content.insert();
|
|
const idIndex = this.#ids.push(id) - 1;
|
|
|
|
this.#content.set(index, QuadConsts.ElmtILft, Math.floor(aabb.x1));
|
|
this.#content.set(index, QuadConsts.ElmtIRgt, Math.floor(aabb.x2));
|
|
this.#content.set(index, QuadConsts.ElmtITop, Math.floor(aabb.y1));
|
|
this.#content.set(index, QuadConsts.ElmtIBtm, Math.floor(aabb.y2));
|
|
this.#content.set(index, QuadConsts.ElmtIId, idIndex);
|
|
|
|
this.#insertNode(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, index);
|
|
return index;
|
|
}
|
|
remove(index: number): void
|
|
{
|
|
if(index >= this.#content.length || index < 0)
|
|
throw new Error("Provided index is out of bounds.");
|
|
|
|
const leaves = this.#findLeaves(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, this.#content.get(index, QuadConsts.ElmtILft), this.#content.get(index, QuadConsts.ElmtIRgt), this.#content.get(index, QuadConsts.ElmtITop), this.#content.get(index, QuadConsts.ElmtIBtm))
|
|
|
|
for(let i = 0; i < leaves.length; ++i)
|
|
{
|
|
const prop = leaves.get(i, QuadConsts.PropIIdx);
|
|
|
|
let node = this.#nodes.get(prop, QuadConsts.NodeIFirst), prev = -1;
|
|
while(node !== -1 && this.#enodes.get(node, QuadConsts.ENodeIElmt) !== index)
|
|
{
|
|
prev = node;
|
|
node = this.#enodes.get(node, QuadConsts.ENodeINext);
|
|
}
|
|
|
|
if(node !== -1)
|
|
{
|
|
const next = this.#enodes.get(node, QuadConsts.ENodeINext);
|
|
|
|
if(prev == -1)
|
|
this.#nodes.set(prop, QuadConsts.NodeIFirst, next);
|
|
else
|
|
this.#enodes.set(prev, QuadConsts.ENodeINext, next);
|
|
|
|
this.#nodes.set(prop, QuadConsts.NodeICount, this.#nodes.get(prop, QuadConsts.NodeICount) - 1);
|
|
}
|
|
}
|
|
|
|
this.#content.erase(index);
|
|
}
|
|
clear(): void
|
|
{
|
|
this.#nodes.clear();
|
|
this.#content.clear();
|
|
this.#enodes.clear();
|
|
this.#ids.length = 0;
|
|
|
|
this.#nodes.insert();
|
|
this.#nodes.set(0, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(0, QuadConsts.NodeICount, 0);
|
|
}
|
|
cleanup(): void
|
|
{
|
|
let updated = false;
|
|
_process.clear();
|
|
|
|
if(this.#nodes.get(0, QuadConsts.NodeICount) === -1)
|
|
_process.set(_process.push(), 0, 0);
|
|
|
|
while(_process.length > 0)
|
|
{
|
|
const node = _process.get(_process.length - 1, 0);
|
|
const fc = this.#nodes.get(node, QuadConsts.NodeIFirst);
|
|
|
|
let empty = 0;
|
|
_process.pop();
|
|
|
|
for(let i = 0; i < 4; ++i)
|
|
{
|
|
const current = fc + i;
|
|
|
|
const count = this.#nodes.get(current, QuadConsts.NodeICount);
|
|
|
|
if(count === 0) //Count the amount of empty leaves
|
|
++empty
|
|
else if(count === -1) //Add this node to the check process if it's a branch
|
|
_process.set(_process.push(), 0, current);
|
|
}
|
|
|
|
if(empty === 4)
|
|
{
|
|
//Because of the way the IntList is made, it's preferable to erase in the reversed order as the last erased index becomes the first available index.
|
|
this.#nodes.erase(fc + 3);
|
|
this.#nodes.erase(fc + 2);
|
|
this.#nodes.erase(fc + 1);
|
|
this.#nodes.erase(fc + 0);
|
|
|
|
//The branch becomes a empty leaf
|
|
this.#nodes.set(node, QuadConsts.NodeICount, 0);
|
|
this.#nodes.set(node, QuadConsts.NodeIFirst, -1);
|
|
|
|
updated = true;
|
|
}
|
|
}
|
|
}
|
|
traverse(cb: (index: number, depth: number, left: number, top: number, right: number, bottom: number, leaf: boolean) => void): void
|
|
{
|
|
_nodes.clear();
|
|
|
|
insert(_nodes, 0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2);
|
|
|
|
while(_nodes.length > 0)
|
|
{
|
|
const last = _nodes.length - 1;
|
|
|
|
const node = _nodes.get(last, QuadConsts.PropIIdx);
|
|
const left = _nodes.get(last, QuadConsts.PropILft);
|
|
const right = _nodes.get(last, QuadConsts.PropIRgt);
|
|
const top = _nodes.get(last, QuadConsts.PropITop);
|
|
const bottom = _nodes.get(last, QuadConsts.PropIBtm);
|
|
const depth = _nodes.get(last, QuadConsts.PropIDpt);
|
|
|
|
_nodes.pop();
|
|
|
|
const count = this.#nodes.get(node, QuadConsts.NodeICount);
|
|
|
|
cb(node, depth, left, top, right, bottom, count !== -1);
|
|
|
|
//If it's a branch
|
|
if (count === -1)
|
|
{
|
|
const fc = this.#nodes.get(node, QuadConsts.NodeIFirst);
|
|
const mx = left + (right - left) / 2, my = top + (bottom - top) / 2;
|
|
|
|
insert(_nodes, fc + 0, depth + 1, left, mx, top, my);
|
|
insert(_nodes, fc + 1, depth + 1, mx, right, top, my);
|
|
insert(_nodes, fc + 2, depth + 1, left, mx, my, bottom);
|
|
insert(_nodes, fc + 3, depth + 1, mx, right, my, bottom);
|
|
}
|
|
}
|
|
}
|
|
|
|
#findLeaves(index: number, depth: number, lleft: number, lright: number, ltop: number, lbottom: number, eleft: number, eright: number, etop: number, ebottom: number): IntList
|
|
{
|
|
const leaves = new IntList(QuadConsts.PropCount);
|
|
_nodes.clear();
|
|
insert(_nodes, index, depth, lleft, lright, ltop, lbottom);
|
|
|
|
while (_nodes.length > 0)
|
|
{
|
|
const last = _nodes.length - 1;
|
|
|
|
const nodeLeft = _nodes.get(last, QuadConsts.PropILft);
|
|
const nodeRight = _nodes.get(last, QuadConsts.PropIRgt);
|
|
const nodeTop = _nodes.get(last, QuadConsts.PropITop);
|
|
const nodeBottom = _nodes.get(last, QuadConsts.PropIBtm);
|
|
const nodeIndex = _nodes.get(last, QuadConsts.PropIIdx);
|
|
const nodeDepth = _nodes.get(last, QuadConsts.PropIDpt);
|
|
|
|
_nodes.pop();
|
|
|
|
if(this.#nodes.get(nodeIndex, QuadConsts.NodeICount) !== -1)
|
|
insert(leaves, nodeIndex, nodeDepth, nodeLeft, nodeRight, nodeTop, nodeBottom);
|
|
else
|
|
{
|
|
const fc = this.#nodes.get(nodeIndex, QuadConsts.NodeIFirst);
|
|
|
|
const mx = nodeLeft + (nodeRight - nodeLeft) / 2, my = nodeTop + (nodeBottom - nodeTop) / 2;
|
|
|
|
if(etop <= my)
|
|
{
|
|
if(eleft <= mx) //Add a new
|
|
insert(_nodes, fc + 0, nodeDepth + 1, nodeLeft, mx, nodeTop, my);
|
|
if(eright > mx)
|
|
insert(_nodes, fc + 1, nodeDepth + 1, mx, nodeRight, nodeTop, my);
|
|
}
|
|
if(ebottom > my)
|
|
{
|
|
if(eleft <= mx)
|
|
insert(_nodes, fc + 2, nodeDepth + 1, nodeLeft, mx, my, nodeBottom);
|
|
if(eright > mx)
|
|
insert(_nodes, fc + 3, nodeDepth + 1, mx, nodeRight, my, nodeBottom);
|
|
}
|
|
}
|
|
}
|
|
|
|
return leaves;
|
|
}
|
|
#insertLeaf(index: number, depth: number, left: number, top: number, right: number, bottom: number, elmt: number): void
|
|
{
|
|
const fc = this.#nodes.get(index, QuadConsts.NodeIFirst);
|
|
this.#nodes.set(index, QuadConsts.NodeIFirst, this.#enodes.insert());
|
|
this.#enodes.set(this.#nodes.get(index, QuadConsts.NodeIFirst), QuadConsts.ENodeINext, fc);
|
|
this.#enodes.set(this.#nodes.get(index, QuadConsts.NodeIFirst), QuadConsts.ENodeIElmt, elmt);
|
|
|
|
if(this.#nodes.get(index, QuadConsts.NodeICount) === this.#maxElmts && depth < this.#maxDepth)
|
|
{
|
|
_process.clear();
|
|
//For each element of the leaf
|
|
while(this.#nodes.get(index, QuadConsts.NodeIFirst) !== -1)
|
|
{
|
|
const current = this.#nodes.get(index, QuadConsts.NodeIFirst);
|
|
const next = this.#enodes.get(current, QuadConsts.ENodeINext);
|
|
const value = this.#enodes.get(current, QuadConsts.ENodeIElmt);
|
|
|
|
//Remove the element
|
|
this.#nodes.set(index, QuadConsts.NodeIFirst, next);
|
|
this.#enodes.erase(current);
|
|
|
|
//Store it for reprocess
|
|
_process.set(_process.push(), 0, value);
|
|
}
|
|
|
|
const newFc = this.#nodes.insert();
|
|
this.#nodes.set(newFc + 0, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(newFc + 0, QuadConsts.NodeICount, 0);
|
|
|
|
this.#nodes.insert();
|
|
this.#nodes.set(newFc + 1, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(newFc + 1, QuadConsts.NodeICount, 0);
|
|
|
|
this.#nodes.insert();
|
|
this.#nodes.set(newFc + 2, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(newFc + 2, QuadConsts.NodeICount, 0);
|
|
|
|
this.#nodes.insert();
|
|
this.#nodes.set(newFc + 3, QuadConsts.NodeIFirst, -1);
|
|
this.#nodes.set(newFc + 3, QuadConsts.NodeICount, 0);
|
|
|
|
this.#nodes.set(index, QuadConsts.NodeIFirst, newFc);
|
|
|
|
this.#nodes.set(index, QuadConsts.NodeICount, -1);
|
|
|
|
for(let i = 0; i < _process.length; ++i)
|
|
this.#insertNode(index, depth, left, right, top, bottom, _process.get(i, 0));
|
|
}
|
|
else
|
|
{
|
|
this.#nodes.set(index, QuadConsts.NodeICount, this.#nodes.get(index, QuadConsts.NodeICount) + 1);
|
|
}
|
|
}
|
|
#insertNode(index: number, depth: number, left: number, right: number, top: number, bottom: number, elmt: number): void
|
|
{
|
|
const leaves = this.#findLeaves(index, depth, left, right, top, bottom, this.#content.get(elmt, QuadConsts.ElmtILft), this.#content.get(elmt, QuadConsts.ElmtIRgt), this.#content.get(elmt, QuadConsts.ElmtITop), this.#content.get(elmt, QuadConsts.ElmtIBtm));
|
|
|
|
for(let i = 0; i < leaves.length; ++i)
|
|
this.#insertLeaf(leaves.get(i, QuadConsts.PropIIdx), leaves.get(i, QuadConsts.PropIDpt), leaves.get(i, QuadConsts.PropILft), leaves.get(i, QuadConsts.PropITop), leaves.get(i, QuadConsts.PropIRgt), leaves.get(i, QuadConsts.PropIBtm), elmt);
|
|
}
|
|
}
|
|
|
|
function insert(list: IntList, index: number, depth: number, left: number, right: number, top: number, bottom: number): void
|
|
{
|
|
const idx = list.push();
|
|
list.set(idx, QuadConsts.PropILft, left);
|
|
list.set(idx, QuadConsts.PropIRgt, right);
|
|
list.set(idx, QuadConsts.PropITop, top);
|
|
list.set(idx, QuadConsts.PropIBtm, bottom);
|
|
list.set(idx, QuadConsts.PropIIdx, index);
|
|
list.set(idx, QuadConsts.PropIDpt, depth);
|
|
} |