Test optimizing quadtree
This commit is contained in:
parent
958ffa9ae9
commit
1d907c8ab9
194
src/common.ts
194
src/common.ts
|
|
@ -1,7 +1,149 @@
|
||||||
const DEFAULT_BUCKET_SIZE = 64;
|
const DEFAULT_SIZE = 128;
|
||||||
|
|
||||||
|
export class FreeList<T>
|
||||||
|
{
|
||||||
|
#data: T[] = [];
|
||||||
|
#free: LinkedList<number> = new LinkedList<number>();
|
||||||
|
|
||||||
export class FastStack<T>
|
#length: number = 0;
|
||||||
|
|
||||||
|
constructor(arr?: T[])
|
||||||
|
{
|
||||||
|
if(arr && arr.length > 0)
|
||||||
|
this.#data = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(element: T): number
|
||||||
|
{
|
||||||
|
if(this.#free.empty)
|
||||||
|
{
|
||||||
|
return this.#data.push(element) - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const index = this.#free.pop()!;
|
||||||
|
this.#data[index] = element;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
erase(n: number): void
|
||||||
|
{
|
||||||
|
delete this.#data[n];
|
||||||
|
this.#free.add(n);
|
||||||
|
}
|
||||||
|
clear(): void
|
||||||
|
{
|
||||||
|
this.#data.length = 0;
|
||||||
|
this.#free.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T
|
||||||
|
{
|
||||||
|
return this.#data[index];
|
||||||
|
}
|
||||||
|
set(index: number, value: T): void
|
||||||
|
{
|
||||||
|
this.#data[index] = value;
|
||||||
|
}
|
||||||
|
get length(): number
|
||||||
|
{
|
||||||
|
return this.#length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class IntList
|
||||||
|
{
|
||||||
|
#data: Uint32Array;
|
||||||
|
#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 Uint32Array(this.#capacity * fields);
|
||||||
|
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 since they *must* always 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.#capacity *= 2;
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
this.#data.buffer.transferToFixedLength(this.#capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Array.from(this.#data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Stack<T>
|
||||||
{
|
{
|
||||||
#arr: T[];
|
#arr: T[];
|
||||||
|
|
||||||
|
|
@ -12,7 +154,7 @@ export class FastStack<T>
|
||||||
|
|
||||||
constructor(size?: number)
|
constructor(size?: number)
|
||||||
{
|
{
|
||||||
this.#bucketSize = size ?? DEFAULT_BUCKET_SIZE;
|
this.#bucketSize = size ?? DEFAULT_SIZE;
|
||||||
|
|
||||||
this.#arr = new Array(this.#bucketSize * this.#bucketCount);
|
this.#arr = new Array(this.#bucketSize * this.#bucketCount);
|
||||||
this.#pos = 0;
|
this.#pos = 0;
|
||||||
|
|
@ -56,7 +198,7 @@ export class FastStack<T>
|
||||||
this.#arr.length = this.#bucketSize * this.#bucketCount;
|
this.#arr.length = this.#bucketSize * this.#bucketCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class FastQueue<T>
|
export class Queue<T>
|
||||||
{
|
{
|
||||||
#arr: T[];
|
#arr: T[];
|
||||||
|
|
||||||
|
|
@ -68,7 +210,7 @@ export class FastQueue<T>
|
||||||
|
|
||||||
constructor(size?: number)
|
constructor(size?: number)
|
||||||
{
|
{
|
||||||
this.#bucketSize = size ?? DEFAULT_BUCKET_SIZE;
|
this.#bucketSize = size ?? DEFAULT_SIZE;
|
||||||
|
|
||||||
this.#arr = new Array(this.#bucketSize * this.#bucketCount);
|
this.#arr = new Array(this.#bucketSize * this.#bucketCount);
|
||||||
this.#idx = 0;
|
this.#idx = 0;
|
||||||
|
|
@ -127,20 +269,19 @@ export class FastQueue<T>
|
||||||
export class LinkedList<T>
|
export class LinkedList<T>
|
||||||
{
|
{
|
||||||
#head: LinkedElmt<T> | null;
|
#head: LinkedElmt<T> | null;
|
||||||
#tail: LinkedElmt<T> | null; //Extension
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.#head = null;
|
this.#head = null;
|
||||||
this.#tail = null;
|
}
|
||||||
|
get empty(): boolean
|
||||||
|
{
|
||||||
|
return this.#head === null;
|
||||||
}
|
}
|
||||||
add(item: T): void
|
add(item: T): void
|
||||||
{
|
{
|
||||||
const tail = this.#tail;
|
const head = this.#head;
|
||||||
|
|
||||||
this.#tail = { elmt: item, next: null, prev: tail };
|
this.#head = { elmt: item, next: head };
|
||||||
|
|
||||||
if(tail) tail.next = this.#tail;
|
|
||||||
else this.#head = this.#tail;
|
|
||||||
}
|
}
|
||||||
pop(): T | null
|
pop(): T | null
|
||||||
{
|
{
|
||||||
|
|
@ -150,31 +291,11 @@ export class LinkedList<T>
|
||||||
const head = this.#head;
|
const head = this.#head;
|
||||||
this.#head = head.next;
|
this.#head = head.next;
|
||||||
|
|
||||||
if(head === this.#tail) this.#tail = null;
|
|
||||||
if(head.next) head.next.prev = null;
|
|
||||||
if(head.next && head.next === this.#tail) this.#tail = head.next;
|
|
||||||
|
|
||||||
return head.elmt;
|
return head.elmt;
|
||||||
}
|
}
|
||||||
remove(item: T): boolean
|
peek(): T | null
|
||||||
{
|
{
|
||||||
let current = this.#head;
|
return this.#head?.elmt ?? null;
|
||||||
while(current)
|
|
||||||
{
|
|
||||||
if(current.elmt === item)
|
|
||||||
{
|
|
||||||
const prev = current.prev;
|
|
||||||
const next = current.next;
|
|
||||||
|
|
||||||
if(prev) prev.next = next;
|
|
||||||
if(next) next.prev = prev;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
current = current.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
forEach(cb: (e: T, i: number) => void): void
|
forEach(cb: (e: T, i: number) => void): void
|
||||||
{
|
{
|
||||||
|
|
@ -194,11 +315,7 @@ export class LinkedList<T>
|
||||||
}
|
}
|
||||||
clear()
|
clear()
|
||||||
{
|
{
|
||||||
if(this.#head) this.#head.next = null;
|
|
||||||
if(this.#tail) this.#tail.prev = null;
|
|
||||||
|
|
||||||
this.#head = null;
|
this.#head = null;
|
||||||
this.#tail = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface LinkedElmt<T>
|
interface LinkedElmt<T>
|
||||||
|
|
@ -206,7 +323,6 @@ interface LinkedElmt<T>
|
||||||
elmt: T;
|
elmt: T;
|
||||||
|
|
||||||
next: LinkedElmt<T> | null;
|
next: LinkedElmt<T> | null;
|
||||||
prev: LinkedElmt<T> | null;
|
|
||||||
}
|
}
|
||||||
export function clamp(x: number, min: number, max: number): number
|
export function clamp(x: number, min: number, max: number): number
|
||||||
{
|
{
|
||||||
|
|
|
||||||
13
src/main.ts
13
src/main.ts
|
|
@ -7,6 +7,11 @@ import Input from './renderer/input.class';
|
||||||
import { Random, clamp } from './common';
|
import { Random, clamp } from './common';
|
||||||
import Selector from './renderer/selector.class';
|
import Selector from './renderer/selector.class';
|
||||||
|
|
||||||
|
if(!ArrayBuffer.prototype.hasOwnProperty("transferToFixedLength"))
|
||||||
|
{
|
||||||
|
throw new Error("Your web browser doesn't includes the latest features needed to make this website work properly.\nPlease upgrade your browser and try again.");
|
||||||
|
}
|
||||||
|
|
||||||
Renderer.init();
|
Renderer.init();
|
||||||
Input.init(Renderer.canvas);
|
Input.init(Renderer.canvas);
|
||||||
Selector.init();
|
Selector.init();
|
||||||
|
|
@ -26,7 +31,7 @@ for(let i = 0; i < 10000; i++)
|
||||||
.scale(r.nextFloat(0.01, 0.15), r.nextFloat(0.01, 0.15))
|
.scale(r.nextFloat(0.01, 0.15), r.nextFloat(0.01, 0.15))
|
||||||
|
|
||||||
Asset.instance.setMatrixAt(i, assets[i].mat);
|
Asset.instance.setMatrixAt(i, assets[i].mat);
|
||||||
quad.insert(assets[i]);
|
quad.insert(i, assets[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset.instance.count = assets.length;
|
Asset.instance.count = assets.length;
|
||||||
|
|
@ -43,7 +48,7 @@ Input.onDragEnd((start, end, button) => {
|
||||||
if(button & 1)
|
if(button & 1)
|
||||||
{
|
{
|
||||||
const s = performance.now();
|
const s = performance.now();
|
||||||
const selection = quad.query({x1: Math.min(start.x, end.x), x2: Math.max(start.x, end.x), y1: Math.min(start.y, end.y), y2: Math.max(start.y, end.y)}) as Asset[];
|
const selection = quad.query({x1: Math.min(start.x, end.x), x2: Math.max(start.x, end.x), y1: Math.min(start.y, end.y), y2: Math.max(start.y, end.y)}).map(e => assets[e]);
|
||||||
console.log("Fetching %s out of %s elements in %sms", selection.length, assets.length, performance.now() - s);
|
console.log("Fetching %s out of %s elements in %sms", selection.length, assets.length, performance.now() - s);
|
||||||
|
|
||||||
if(Input.keys['Shift']) Selector.toggle(selection);
|
if(Input.keys['Shift']) Selector.toggle(selection);
|
||||||
|
|
@ -54,11 +59,11 @@ Input.onDrag((delta, start, end, button) => { if(button & 1) Selector.preview(st
|
||||||
Input.onClick((point, button) => {
|
Input.onClick((point, button) => {
|
||||||
if(button & 1)
|
if(button & 1)
|
||||||
{
|
{
|
||||||
const selection = quad.fetch(point.x, point.y) as Asset[];
|
const selection = quad.fetch(point.x, point.y).map(e => assets[e]);
|
||||||
|
|
||||||
if(Input.keys['Shift']) Selector.toggle(selection);
|
if(Input.keys['Shift']) Selector.toggle(selection);
|
||||||
else Selector.select(selection);
|
else Selector.select(selection);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 1, 5));
|
Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 1, 5));
|
||||||
Input.onMove(p => { if(!Input.dragging) Selector.ghost(quad.fetch(p.x, p.y)[0] as Asset); });
|
Input.onMove(p => { if(!Input.dragging) Selector.ghost(assets[quad.fetch(p.x, p.y)[0]]); });
|
||||||
|
|
@ -11,7 +11,7 @@ export interface AABB
|
||||||
y2: number;
|
y2: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function intersects(a: AABB, b: AABB | Point): Boolean
|
export function intersectsObj(a: AABB, b: AABB | Point): Boolean
|
||||||
{
|
{
|
||||||
if(b.hasOwnProperty("x") && b.hasOwnProperty("y"))
|
if(b.hasOwnProperty("x") && b.hasOwnProperty("y"))
|
||||||
{
|
{
|
||||||
|
|
@ -26,3 +26,7 @@ export function intersects(a: AABB, b: AABB | Point): Boolean
|
||||||
return a.x1 <= b.x2 && a.x2 >= b.x1 && a.y1 <= b.y2 && a.y2 >= b.y1;
|
return a.x1 <= b.x2 && a.x2 >= b.x1 && a.y1 <= b.y2 && a.y2 >= b.y1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { LinkedList } from "../common";
|
import { FreeList, IntList, LinkedList } from "../common";
|
||||||
import Renderer from "../renderer/renderer.class";
|
import Renderer from "../renderer/renderer.class";
|
||||||
import { AABB, intersects } from "./common";
|
import { AABB, intersects } from "./common";
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
@ -8,13 +8,31 @@ import * as THREE from 'three';
|
||||||
* A node that contains other nodes is called a branch. Its count is equals to -1.
|
* 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. (Not memory friendly in JS ?)
|
* The AABB of each node isn't stored and is computed on the go. (Not memory friendly in JS ?)
|
||||||
*/
|
*/
|
||||||
export default class Quadtree<T extends AABB>
|
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 _process = new IntList(1);
|
||||||
|
const _nodeProcess = new IntList(QuadConsts.PropCount);
|
||||||
|
|
||||||
|
export default class Quadtree
|
||||||
{
|
{
|
||||||
#bounds: AABB;
|
#bounds: AABB;
|
||||||
#maxDepth: number = 8;
|
#maxDepth: number = 8;
|
||||||
#maxElmts: number = 4;
|
#maxElmts: number = 4;
|
||||||
#nodes: Node[];
|
#nodes: IntList = new IntList(2);
|
||||||
#content: T[];
|
#enodes: IntList = new IntList(2);
|
||||||
|
#content: IntList = new IntList(5);
|
||||||
|
|
||||||
#dirty: boolean = true;
|
#dirty: boolean = true;
|
||||||
#debugRect: THREE.Box3Helper[] = [];
|
#debugRect: THREE.Box3Helper[] = [];
|
||||||
|
|
@ -25,51 +43,104 @@ export default class Quadtree<T extends AABB>
|
||||||
this.#maxDepth = maxDepth ?? this.#maxDepth;
|
this.#maxDepth = maxDepth ?? this.#maxDepth;
|
||||||
this.#maxElmts = maxElmts ?? this.#maxElmts;
|
this.#maxElmts = maxElmts ?? this.#maxElmts;
|
||||||
|
|
||||||
this.#nodes = [];
|
this.#nodes.insert();
|
||||||
this.#content = [];
|
this.#nodes.set(0, QuadConsts.NodeIFirst, -1);
|
||||||
|
this.#nodes.set(0, QuadConsts.NodeICount, 0);
|
||||||
this.#nodes.push({ children: new LinkedList<number>(), count: 0 });
|
|
||||||
}
|
}
|
||||||
fetch(x: number, y: number): T[]
|
fetch(x: number, y: number): number[]
|
||||||
{
|
{
|
||||||
return this.query({x1: x, x2: x, y1: y, y2: y});
|
return this.query({x1: x, x2: x, y1: y, y2: y});
|
||||||
}
|
}
|
||||||
query(aabb: AABB): T[]
|
query(aabb: AABB): number[]
|
||||||
{
|
{
|
||||||
const result: number[] = [];
|
_process.clear();
|
||||||
|
|
||||||
const leaves = this.#find_leaves(0, 0, this.#bounds, aabb); //Search every mathed leaves
|
const leaves = this.#findLeaves(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, aabb.x1, aabb.x2, aabb.y1, aabb.y2);
|
||||||
for(let i = 0; i < leaves.length; i++)
|
|
||||||
|
const tmp: Record<number, boolean> = {};
|
||||||
|
|
||||||
|
for(let i = 0; i < leaves.length; ++i)
|
||||||
{
|
{
|
||||||
const node = this.#nodes[leaves[i].index];
|
const index = leaves.get(i, QuadConsts.PropIIdx);
|
||||||
node.children.forEach(e => { //Test every leaves elements
|
|
||||||
if(!result.includes(e) && intersects(this.#content[e], aabb))
|
let node = this.#nodes.get(index, QuadConsts.NodeIFirst);
|
||||||
result.push(e);
|
while(node !== -1)
|
||||||
});
|
{
|
||||||
|
const elmt = this.#enodes.get(node, QuadConsts.ENodeIElmt);
|
||||||
|
|
||||||
|
const left = this.#content.get(elmt, QuadConsts.ElmtILft),
|
||||||
|
right = this.#content.get(elmt, QuadConsts.ElmtIRgt),
|
||||||
|
top = this.#content.get(elmt, QuadConsts.ElmtITop),
|
||||||
|
bottom = this.#content.get(elmt, QuadConsts.ElmtIBtm);
|
||||||
|
|
||||||
|
if(!tmp[elmt] && intersects(aabb.x1, aabb.y1, aabb.x2, aabb.y2, left, top, right, bottom))
|
||||||
|
{
|
||||||
|
_process.set(_process.push(), 0, elmt);
|
||||||
|
tmp[elmt] = true;
|
||||||
|
}
|
||||||
|
node = this.#enodes.get(node, QuadConsts.ENodeINext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map(e => this.#content[e]);
|
return _process.toArray();
|
||||||
}
|
}
|
||||||
insert(item: T): number
|
insert(id: number, aabb: AABB): number
|
||||||
{
|
{
|
||||||
const idx = this.#content.push(item) - 1;
|
const index = this.#content.insert();
|
||||||
this.#node_insert(0, 0, this.#bounds, idx);
|
|
||||||
return idx;
|
this.#content.set(index, QuadConsts.ElmtILft, aabb.x1);
|
||||||
|
this.#content.set(index, QuadConsts.ElmtIRgt, aabb.x2);
|
||||||
|
this.#content.set(index, QuadConsts.ElmtITop, aabb.y1);
|
||||||
|
this.#content.set(index, QuadConsts.ElmtIBtm, aabb.y2);
|
||||||
|
this.#content.set(index, QuadConsts.ElmtIId, id);
|
||||||
|
|
||||||
|
this.#insertNode(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, index);
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
remove(index: number): void
|
remove(index: number): void
|
||||||
{
|
{
|
||||||
if(index >= this.#content.length)
|
if(index >= this.#content.length || index < 0)
|
||||||
throw new Error("Out of bound exception.");
|
throw new Error("Provided index is out of bounds.");
|
||||||
|
|
||||||
const elmt = this.#content[index];
|
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))
|
||||||
const leaves = this.#find_leaves(0, 0, this.#bounds, elmt);
|
|
||||||
|
|
||||||
for(let i = 0; i < leaves.length; i++)
|
for(let i = 0; i < leaves.length; ++i)
|
||||||
{
|
{
|
||||||
if(this.#nodes[leaves[i].index].children.remove(index))
|
const prop = leaves.get(i, QuadConsts.PropIIdx);
|
||||||
this.#nodes[leaves[i].index].count--;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
this.#dirty = true;
|
|
||||||
|
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.#dirty = false;
|
||||||
|
|
||||||
|
//DEBUG
|
||||||
|
this.#debugRect.forEach(e => e.parent?.remove(e));
|
||||||
|
this.#debugRect = [];
|
||||||
}
|
}
|
||||||
cleanup(): void
|
cleanup(): void
|
||||||
{
|
{
|
||||||
|
|
@ -78,54 +149,78 @@ export default class Quadtree<T extends AABB>
|
||||||
if(!this.#dirty)
|
if(!this.#dirty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const stack: number[] = [];
|
_process.clear();
|
||||||
|
|
||||||
if(this.#nodes[0].count)
|
if(this.#nodes.get(0, QuadConsts.NodeICount) === -1)
|
||||||
stack.push(0);
|
_process.set(_process.push(), 0, 0);
|
||||||
|
|
||||||
while(stack.length !== 0)
|
while(_process.length > 0)
|
||||||
{
|
{
|
||||||
const node = this.#nodes[stack.pop()!];
|
const node = _process.get(_process.length - 1, 0);
|
||||||
|
const fc = this.#nodes.get(node, QuadConsts.NodeIFirst);
|
||||||
|
|
||||||
let empty_leaves = 0;
|
let empty = 0;
|
||||||
|
_process.pop();
|
||||||
|
|
||||||
node.children.forEach(e => {
|
for(let i = 0; i < 4; ++i)
|
||||||
const child = this.#nodes[e];
|
|
||||||
|
|
||||||
if(child.count === 0)
|
|
||||||
empty_leaves++;
|
|
||||||
else if(child.count === -1)
|
|
||||||
stack.push(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(empty_leaves === 4)
|
|
||||||
{
|
{
|
||||||
node.count = 0;
|
const current = fc + i;
|
||||||
node.children.clear();
|
|
||||||
|
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
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
traverse(cb: (prop: NodeProp) => void): void
|
traverse(cb: (index: number, depth: number, left: number, top: number, right: number, bottom: number, leaf: boolean) => void): void
|
||||||
{
|
{
|
||||||
const stack: NodeProp[] = [];
|
_nodeProcess.clear();
|
||||||
stack.push({ index: 0, depth: 0, bounds: this.#bounds });
|
|
||||||
|
|
||||||
while(stack.length > 0)
|
insert(_nodeProcess, 0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2);
|
||||||
|
|
||||||
|
while(_nodeProcess.length > 0)
|
||||||
{
|
{
|
||||||
const nodeProp = stack.pop()!;
|
const last = _nodeProcess.length - 1;
|
||||||
|
|
||||||
cb(nodeProp);
|
const node = _nodeProcess.get(last, QuadConsts.PropIIdx);
|
||||||
|
const left = _nodeProcess.get(last, QuadConsts.PropILft);
|
||||||
|
const right = _nodeProcess.get(last, QuadConsts.PropIRgt);
|
||||||
|
const top = _nodeProcess.get(last, QuadConsts.PropITop);
|
||||||
|
const bottom = _nodeProcess.get(last, QuadConsts.PropIBtm);
|
||||||
|
const depth = _nodeProcess.get(last, QuadConsts.PropIDpt);
|
||||||
|
|
||||||
//If the node contains elements
|
_nodeProcess.pop();
|
||||||
if (this.#nodes[nodeProp.index].count === -1)
|
|
||||||
|
const count = this.#nodes.get(node, QuadConsts.NodeICount);
|
||||||
|
|
||||||
|
cb(left, top, right, bottom, node, depth, count !== -1);
|
||||||
|
|
||||||
|
//If it's a branch
|
||||||
|
if (count === -1)
|
||||||
{
|
{
|
||||||
const children = this.#nodes[nodeProp.index].children.toArray();
|
const fc = this.#nodes.get(node, QuadConsts.NodeIFirst);
|
||||||
const mx = nodeProp.bounds.x1 + (nodeProp.bounds.x2 - nodeProp.bounds.x1) / 2, my = nodeProp.bounds.y1 + (nodeProp.bounds.y2 - nodeProp.bounds.y1) / 2;
|
const mx = left + (right - left) / 2, my = top + (bottom - top) / 2;
|
||||||
|
|
||||||
stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my } });
|
insert(_nodeProcess, fc + 0, depth + 1, left, mx, top, my);
|
||||||
stack.push({ index: children[1], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: nodeProp.bounds.y1, y2: my } });
|
insert(_nodeProcess, fc + 1, depth + 1, mx, right, top, my);
|
||||||
stack.push({ index: children[2], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: my, y2: nodeProp.bounds.y2 } });
|
insert(_nodeProcess, fc + 2, depth + 1, left, mx, my, bottom);
|
||||||
stack.push({ index: children[3], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: my, y2: nodeProp.bounds.y2 } });
|
insert(_nodeProcess, fc + 3, depth + 1, mx, right, my, bottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,95 +229,128 @@ export default class Quadtree<T extends AABB>
|
||||||
Renderer.scene.remove(...this.#debugRect);
|
Renderer.scene.remove(...this.#debugRect);
|
||||||
this.#debugRect = [];
|
this.#debugRect = [];
|
||||||
|
|
||||||
this.traverse((prop) => {
|
this.traverse((index, depth, left, top, right, bottom, leaf) => {
|
||||||
this.#debugRect.push(new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3(prop.bounds.x1, prop.bounds.y1, 0), new THREE.Vector3(prop.bounds.x2, prop.bounds.y2, 0))));
|
this.#debugRect.push(new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3(left, top, 0), new THREE.Vector3(right, bottom, 0))));
|
||||||
});
|
});
|
||||||
|
|
||||||
Renderer.scene.add(...this.#debugRect);
|
Renderer.scene.add(...this.#debugRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
#find_leaves(index: number, depth: number, bounds: AABB, elmtBounds: AABB): NodeProp[]
|
#findLeaves(index: number, depth: number, lleft: number, lright: number, ltop: number, lbottom: number, eleft: number, eright: number, etop: number, ebottom: number): IntList
|
||||||
{
|
{
|
||||||
const stack: NodeProp[] = [], result: NodeProp[] = [];
|
const leaves = new IntList(QuadConsts.PropCount);
|
||||||
stack.push({index: index, depth: depth, bounds: bounds});
|
_nodeProcess.clear();
|
||||||
|
insert(_nodeProcess, index, depth, lleft, lright, ltop, lbottom);
|
||||||
|
|
||||||
//Fetch every nodes to search for leaves that match the element bounds
|
while (_nodeProcess.length > 0)
|
||||||
while(stack.length > 0)
|
|
||||||
{
|
{
|
||||||
const nodeProp = stack.pop()!;
|
const last = _nodeProcess.length - 1;
|
||||||
|
|
||||||
//If this is a leaf
|
const nodeLeft = _nodeProcess.get(last, QuadConsts.PropILft);
|
||||||
if(this.#nodes[nodeProp.index].count !== -1)
|
const nodeRight = _nodeProcess.get(last, QuadConsts.PropIRgt);
|
||||||
result.push(nodeProp);
|
const nodeTop = _nodeProcess.get(last, QuadConsts.PropITop);
|
||||||
|
const nodeBottom = _nodeProcess.get(last, QuadConsts.PropIBtm);
|
||||||
|
const nodeIndex = _nodeProcess.get(last, QuadConsts.PropIIdx);
|
||||||
|
const nodeDepth = _nodeProcess.get(last, QuadConsts.PropIDpt);
|
||||||
|
|
||||||
|
_nodeProcess.pop();
|
||||||
|
|
||||||
|
if(this.#nodes.get(nodeIndex, QuadConsts.NodeICount) !== -1)
|
||||||
|
insert(leaves, nodeIndex, nodeDepth, nodeLeft, nodeRight, nodeTop, nodeBottom);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Otherwise, check intersection on each 4 sides of the node
|
const fc = this.#nodes.get(nodeIndex, QuadConsts.NodeIFirst);
|
||||||
const children = this.#nodes[nodeProp.index].children.toArray();
|
|
||||||
const mx = nodeProp.bounds.x1 + (nodeProp.bounds.x2 - nodeProp.bounds.x1) / 2, my = nodeProp.bounds.y1 + (nodeProp.bounds.y2 - nodeProp.bounds.y1) / 2;
|
|
||||||
|
|
||||||
if(elmtBounds.y1 <= my)
|
const mx = nodeLeft + (nodeRight - nodeLeft) / 2, my = nodeTop + (nodeBottom - nodeTop) / 2;
|
||||||
{
|
|
||||||
if(elmtBounds.x1 <= mx) //Add a new
|
|
||||||
stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my }});
|
|
||||||
if(elmtBounds.x2 > mx)
|
|
||||||
stack.push({ index: children[1], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: nodeProp.bounds.y1, y2: my }});
|
|
||||||
}
|
|
||||||
if(elmtBounds.y2 > my)
|
|
||||||
{
|
|
||||||
if(elmtBounds.x1 <= mx)
|
|
||||||
stack.push({ index: children[2], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: my, y2: nodeProp.bounds.y2 }});
|
|
||||||
if(elmtBounds.x2 > mx)
|
|
||||||
stack.push({ index: children[3], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: my, y2: nodeProp.bounds.y2 }});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
#leaf_insert(index: number, depth: number, bounds: AABB, elmt: number): void
|
|
||||||
{
|
|
||||||
const node = this.#nodes[index];
|
|
||||||
node.children.add(elmt);
|
|
||||||
|
|
||||||
//Split if the max amount of element is reached and the max depth isn't
|
if(etop <= my)
|
||||||
if(node.count == this.#maxElmts && depth < this.#maxDepth)
|
|
||||||
{
|
{
|
||||||
const elmts = node.children.toArray(); //Get every children
|
if(eleft <= mx) //Add a new
|
||||||
node.children.clear();
|
insert(_nodeProcess, fc + 0, nodeDepth + 1, nodeLeft, mx, nodeTop, my);
|
||||||
node.count = -1;
|
if(eright > mx)
|
||||||
|
insert(_nodeProcess, fc + 1, nodeDepth + 1, mx, nodeRight, nodeTop, my);
|
||||||
|
}
|
||||||
|
if(ebottom > my)
|
||||||
|
{
|
||||||
|
if(eleft <= mx)
|
||||||
|
insert(_nodeProcess, fc + 2, nodeDepth + 1, nodeLeft, mx, my, nodeBottom);
|
||||||
|
if(eright > mx)
|
||||||
|
insert(_nodeProcess, fc + 3, nodeDepth + 1, mx, nodeRight, my, nodeBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for(let i = 0; i < 4; i++) //Split in 4 nodes
|
return leaves;
|
||||||
|
}
|
||||||
|
#insertLeaf(index: number, depth: number, left: number, top: number, right: number, bottom: number, elmt: number): void
|
||||||
{
|
{
|
||||||
node.children.add(this.#nodes.push({ children: new LinkedList<number>(), count: 0 }) - 1);
|
const fc = this.#nodes.get(index, QuadConsts.NodeIFirst);
|
||||||
}
|
this.#nodes.set(index, QuadConsts.NodeIFirst, this.#enodes.insert());
|
||||||
for(let i = 0; i < elmts.length; i++) //Insert back the children
|
this.#enodes.set(this.#nodes.get(index, QuadConsts.NodeIFirst), QuadConsts.ENodeINext, fc);
|
||||||
{
|
this.#enodes.set(this.#nodes.get(index, QuadConsts.NodeIFirst), QuadConsts.ENodeIElmt, elmt);
|
||||||
this.#node_insert(index, depth, bounds, elmts[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else //Otherwise, just increase the count
|
|
||||||
{
|
|
||||||
node.count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#node_insert(index: number, depth: number, bounds: AABB, elmt: number): void
|
|
||||||
{
|
|
||||||
const aabb = this.#content[elmt];
|
|
||||||
const leaves = this.#find_leaves(index, depth, bounds, aabb);
|
|
||||||
|
|
||||||
for(let i = 0; i < leaves.length; i++)
|
if(this.#nodes.get(index, QuadConsts.NodeICount) === this.#maxElmts && depth < this.#maxDepth)
|
||||||
{
|
{
|
||||||
this.#leaf_insert(leaves[i].index, leaves[i].depth, leaves[i].bounds, elmt);
|
_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, top, right, 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, top: number, right: 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface NodeProp
|
|
||||||
|
function insert(list: IntList, index: number, depth: number, left: number, right: number, top: number, bottom: number): void
|
||||||
{
|
{
|
||||||
index: number;
|
const idx = list.push();
|
||||||
depth: number;
|
list.set(idx, QuadConsts.PropILft, left);
|
||||||
bounds: AABB;
|
list.set(idx, QuadConsts.PropIRgt, right);
|
||||||
}
|
list.set(idx, QuadConsts.PropITop, top);
|
||||||
interface Node
|
list.set(idx, QuadConsts.PropIBtm, bottom);
|
||||||
{
|
list.set(idx, QuadConsts.PropIIdx, index);
|
||||||
children: LinkedList<number>;
|
list.set(idx, QuadConsts.PropIDpt, depth);
|
||||||
count: number;
|
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ export default class Input
|
||||||
static #dragCb?: (delta: Point, start: Point, end: Point, button: number) => void;
|
static #dragCb?: (delta: Point, start: Point, end: Point, button: number) => void;
|
||||||
static #dragEndCb?: (start: Point, end: Point, button: number) => void;
|
static #dragEndCb?: (start: Point, end: Point, button: number) => void;
|
||||||
static #wheelCb?: (delta: number) => void;
|
static #wheelCb?: (delta: number) => void;
|
||||||
static #inputCb?: () => void;
|
static #inputCb?: (input: string) => void;
|
||||||
|
|
||||||
static init(canvas: HTMLCanvasElement)
|
static init(canvas: HTMLCanvasElement)
|
||||||
{
|
{
|
||||||
|
|
@ -95,7 +95,7 @@ export default class Input
|
||||||
{
|
{
|
||||||
Input.#wheelCb = cb;
|
Input.#wheelCb = cb;
|
||||||
}
|
}
|
||||||
static onInput(cb: () => void): void
|
static onInput(cb: (input: string) => void): void
|
||||||
{
|
{
|
||||||
Input.#inputCb = cb;
|
Input.#inputCb = cb;
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +173,6 @@ export default class Input
|
||||||
{
|
{
|
||||||
Input.#keys[e.key] = false;
|
Input.#keys[e.key] = false;
|
||||||
|
|
||||||
Input.#inputCb && Input.#inputCb();
|
Input.#inputCb && Input.#inputCb(e.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,10 @@ export default class Selector
|
||||||
{
|
{
|
||||||
return Selector.#selected;
|
return Selector.#selected;
|
||||||
}
|
}
|
||||||
|
static get selection(): Asset[]
|
||||||
|
{
|
||||||
|
return Selector.#assets;
|
||||||
|
}
|
||||||
static init(): void
|
static init(): void
|
||||||
{
|
{
|
||||||
Selector.#previewMesh = new Three.Box3Helper(new Three.Box3(), 0x2980B9);
|
Selector.#previewMesh = new Three.Box3Helper(new Three.Box3(), 0x2980B9);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue