Test optimizing quadtree

This commit is contained in:
Peaceultime 2024-06-13 18:53:17 +02:00
parent 958ffa9ae9
commit 1d907c8ab9
6 changed files with 428 additions and 171 deletions

View File

@ -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
{ {

View File

@ -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]]); });

View File

@ -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"))
{ {
@ -25,4 +25,8 @@ 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;
} }

View File

@ -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,107 +43,184 @@ 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);
}
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.#dirty = true;
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
{ {
//Only cleanup if it's dirty. //Only cleanup if it's dirty.
//Allows the system to call the function at each loop iteration. //Allows the system to call the function at each loop iteration.
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);
_nodeProcess.pop();
//If the node contains elements const count = this.#nodes.get(node, QuadConsts.NodeICount);
if (this.#nodes[nodeProp.index].count === -1)
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(stack.length > 0) while (_nodeProcess.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; const mx = nodeLeft + (nodeRight - nodeLeft) / 2, my = nodeTop + (nodeBottom - nodeTop) / 2;
if(elmtBounds.y1 <= my) if(etop <= my)
{ {
if(elmtBounds.x1 <= mx) //Add a new if(eleft <= 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 }}); insert(_nodeProcess, fc + 0, nodeDepth + 1, nodeLeft, mx, nodeTop, my);
if(elmtBounds.x2 > mx) if(eright > mx)
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, nodeDepth + 1, mx, nodeRight, nodeTop, my);
} }
if(elmtBounds.y2 > my) if(ebottom > my)
{ {
if(elmtBounds.x1 <= mx) if(eleft <= mx)
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, nodeDepth + 1, nodeLeft, mx, my, nodeBottom);
if(elmtBounds.x2 > mx) if(eright > mx)
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, nodeDepth + 1, mx, nodeRight, my, nodeBottom);
} }
} }
} }
return result;
return leaves;
} }
#leaf_insert(index: number, depth: number, bounds: AABB, elmt: number): void #insertLeaf(index: number, depth: number, left: number, top: number, right: number, bottom: number, elmt: number): void
{ {
const node = this.#nodes[index]; const fc = this.#nodes.get(index, QuadConsts.NodeIFirst);
node.children.add(elmt); 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);
//Split if the max amount of element is reached and the max depth isn't if(this.#nodes.get(index, QuadConsts.NodeICount) === this.#maxElmts && depth < this.#maxDepth)
if(node.count == this.#maxElmts && depth < this.#maxDepth)
{ {
const elmts = node.children.toArray(); //Get every children _process.clear();
node.children.clear(); //For each element of the leaf
node.count = -1; 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);
for(let i = 0; i < 4; i++) //Split in 4 nodes //Remove the element
{ this.#nodes.set(index, QuadConsts.NodeIFirst, next);
node.children.add(this.#nodes.push({ children: new LinkedList<number>(), count: 0 }) - 1); this.#enodes.erase(current);
}
for(let i = 0; i < elmts.length; i++) //Insert back the children //Store it for reprocess
{ _process.set(_process.push(), 0, value);
this.#node_insert(index, depth, bounds, elmts[i]);
} }
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 //Otherwise, just increase the count else
{ {
node.count++; this.#nodes.set(index, QuadConsts.NodeICount, this.#nodes.get(index, QuadConsts.NodeICount) + 1);
} }
} }
#node_insert(index: number, depth: number, bounds: AABB, elmt: number): void #insertNode(index: number, depth: number, left: number, top: number, right: number, bottom: number, elmt: number): void
{ {
const aabb = this.#content[elmt]; 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));
const leaves = this.#find_leaves(index, depth, bounds, aabb);
for(let i = 0; i < leaves.length; i++) 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);
this.#leaf_insert(leaves[i].index, leaves[i].depth, leaves[i].bounds, 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;
} }

View File

@ -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);
} }
} }

View File

@ -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);
@ -25,7 +29,7 @@ export default class Selector
Renderer.scene.add(Selector.#previewMesh); Renderer.scene.add(Selector.#previewMesh);
Renderer.scene.add(Selector.#ghostMesh); Renderer.scene.add(Selector.#ghostMesh);
Renderer.scene.add(Selector.#selectionMesh); Renderer.scene.add(Selector.#selectionMesh);
Selector.hide(); Selector.hide();
} }
static preview(start: Point, end: Point): void static preview(start: Point, end: Point): void