@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
715 lines (559 loc) • 21.5 kB
JavaScript
import { assert } from "../../../assert.js";
import { UINT32_MAX } from "../../../binary/UINT32_MAX.js";
import { max2 } from "../../../math/max2.js";
import { min2 } from "../../../math/min2.js";
import { BinaryElementPool } from "../../3d/topology/struct/binary/BinaryElementPool.js";
export const QT_NULL_POINTER = UINT32_MAX;
/**
* Numeric data supplied by the user
* @type {number}
*/
const COLUMN_ELEMENT_USER_DATA = 0;
/**
* Singly-linked list, nest element
* can be NULL_POINTER to represent end of the list
* @type {number}
*/
const COLUMN_ELEMENT_NEXT = 1;
/**
* Tree node that contains this element
* @type {number}
*/
const COLUMN_ELEMENT_PARENT_NODE = 2;
const COLUMN_ELEMENT_X0 = 3;
const COLUMN_ELEMENT_Y0 = 4;
const COLUMN_ELEMENT_X1 = 5;
const COLUMN_ELEMENT_Y1 = 6;
const COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER = 0;
/**
* Number of elements stored in the node's element list
* @type {number}
*/
const COLUMN_TREE_NODE_ELEMENT_COUNT = 1
const COLUMN_TREE_NODE_PARENT = 2;
const COLUMN_TREE_NODE_CHILDREN_POINTER_TL = 3;
const COLUMN_TREE_NODE_CHILDREN_POINTER_TR = 4;
const COLUMN_TREE_NODE_CHILDREN_POINTER_BL = 5;
const COLUMN_TREE_NODE_CHILDREN_POINTER_BR = 6;
const THRESHOLD_SPLIT = 16;
const THRESHOLD_MERGE = 8;
const temp_array_element = new Uint32Array(4096);
/**
*
* NOTE: THIS CODE IS UNFINISHED, IT IS ONLY A SKETCH
* TODO finish implementation
*/
export class QuadTree {
#node_pool = new BinaryElementPool(28);
#element_pool = new BinaryElementPool(28);
#root = QT_NULL_POINTER
/**
* AABB of the entire tree
* @type {Float32Array}
*/
#dimensions = new Float32Array([0, 0, 0, 0]);
/**
* Parameters allow us to set initial bounds to prevent early resizing
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
constructor(x0 = 0, y0 = 0, x1 = 0, y1 = 0) {
this.#setDimensions(x0, y0, x1, y1);
}
get root() {
return this.#root;
}
/**
* This method is unsafe as it does not re-build the tree
* Make sure to rebuild the tree as necessary after calling this
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
#setDimensions(x0, y0, x1, y1) {
assert.isFiniteNumber(x0, 'x0');
assert.notNaN(x0, 'x0');
assert.isFiniteNumber(y0, 'y0');
assert.notNaN(y0, 'y0');
assert.isFiniteNumber(x1, 'x1');
assert.notNaN(x1, 'x1');
assert.isFiniteNumber(y1, 'y1');
assert.notNaN(y1, 'y1');
const dimensions = this.#dimensions;
dimensions[0] = x0;
dimensions[1] = y0;
dimensions[2] = x1;
dimensions[3] = y1;
}
/**
* Resize dimensions of the tree to tightly fit all inserted elements
*/
shrink() {
if (this.#root === QT_NULL_POINTER) {
// tree is empty
this.#setDimensions(0, 0, 0, 0);
}
const bounds = [0, 0, 0, 0];
this.compute_tight_bounds(bounds);
this.#setDimensions(...bounds);
this.rebuild();
}
/**
*
* @param {number[]|Float32Array} output
*/
compute_tight_bounds(output) {
const pool = this.#element_pool;
const data_pool_size = pool.size;
const float32 = pool.data_float32;
let bounds_x0 = Infinity;
let bounds_y0 = Infinity;
let bounds_x1 = -Infinity;
let bounds_y1 = -Infinity;
for (let id = 0; id < data_pool_size; id++) {
if (pool.is_allocated(id)) {
const word = pool.element_word(id);
const x0 = float32[word + COLUMN_ELEMENT_X0];
const y0 = float32[word + COLUMN_ELEMENT_Y0];
const x1 = float32[word + COLUMN_ELEMENT_X1];
const y1 = float32[word + COLUMN_ELEMENT_Y1];
bounds_x0 = min2(x0, bounds_x0);
bounds_y0 = min2(y0, bounds_y0);
bounds_x1 = max2(x1, bounds_x1);
bounds_y1 = max2(y1, bounds_y1);
}
}
output[0] = bounds_x0;
output[1] = bounds_y0;
output[2] = bounds_x1;
output[3] = bounds_y1;
}
/**
* Rebuild tree but keep all the data
*/
rebuild() {
// drop existing structure
this.#root = QT_NULL_POINTER;
this.#node_pool.clear();
// re-insert data elements
const pool = this.#element_pool;
const data_pool_size = pool.size;
for (let i = 0; i < data_pool_size; i++) {
if (pool.is_allocated(i)) {
this.element_insert(i);
}
}
}
/**
*
* @returns {number} ID of data in the tree
*/
element_allocate() {
return this.#element_pool.allocate();
}
/**
*
* @param {number} element
* @param {number} user_data
*/
element_set_user_data(element, user_data) {
assert.isNonNegativeInteger(user_data, 'user_data');
const pool = this.#element_pool;
const word = pool.element_word(element);
pool.data_uint32[word + COLUMN_ELEMENT_USER_DATA] = user_data;
}
/**
*
* @param {number} element
* @return {number}
*/
element_get_user_data(element) {
const pool = this.#element_pool;
const word = pool.element_word(element);
return pool.data_uint32[word + COLUMN_ELEMENT_USER_DATA];
}
/**
*
* @param {number} element
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
element_set_bounds_primitive(
element,
x0, y0,
x1, y1
) {
assert.isFiniteNumber(x0, 'x0');
assert.notNaN(x0, 'x0');
assert.isFiniteNumber(y0, 'y0');
assert.notNaN(y0, 'y0');
assert.isFiniteNumber(x1, 'x1');
assert.notNaN(x1, 'x1');
assert.isFiniteNumber(y1, 'y1');
assert.notNaN(y1, 'y1');
const pool = this.#element_pool;
const word = pool.element_word(element);
const float32 = pool.data_float32;
float32[word + COLUMN_ELEMENT_X0] = x0;
float32[word + COLUMN_ELEMENT_Y0] = y0;
float32[word + COLUMN_ELEMENT_X1] = x1;
float32[word + COLUMN_ELEMENT_Y1] = y1;
}
/**
*
* @param {number} element
* @return {number} next element in the node
*/
element_get_next(element) {
const pool = this.#element_pool;
const word = pool.element_word(element);
return pool.data_uint32[word + COLUMN_ELEMENT_NEXT];
}
/**
*
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @return {number}
*/
#find_parent_for_box(x0, y0, x1, y1) {
if (this.#root === QT_NULL_POINTER) {
// special case
return QT_NULL_POINTER;
}
let node = this.#root;
let bounds_x0 = this.#dimensions[0];
let bounds_y0 = this.#dimensions[1];
let bounds_x1 = this.#dimensions[2];
let bounds_y1 = this.#dimensions[3];
const node_pool = this.#node_pool;
const node_uint32 = node_pool.data_uint32;
for (; ;) {
const node_address = node_pool.element_word(node);
const bounds_mid_x = (bounds_x0 + bounds_x1) * 0.5;
const bounds_mid_y = (bounds_y0 + bounds_y1) * 0.5;
if (y1 < bounds_mid_y) {
//top
bounds_y1 = bounds_mid_y;
if (x1 < bounds_mid_x) {
//left
const child_tl = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_TL];
if (child_tl === QT_NULL_POINTER) {
break;
}
node = child_tl;
bounds_x1 = bounds_mid_x;
} else if (x0 >= bounds_mid_x) {
//right
const child_tr = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_TR];
if (child_tr === QT_NULL_POINTER) {
break;
}
node = child_tr;
bounds_x0 = bounds_mid_x;
} else {
break;
}
} else if (y0 >= bounds_mid_y) {
//bottom
bounds_y0 = bounds_mid_y;
if (x1 < bounds_mid_x) {
//left
const child_bl = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_BL];
if (child_bl === QT_NULL_POINTER) {
break;
}
node = child_bl;
bounds_x1 = bounds_mid_x;
} else if (x0 >= bounds_mid_x) {
//right
const child_br = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_BR];
if (child_br === QT_NULL_POINTER) {
break;
}
node = child_br;
bounds_x0 = bounds_mid_x;
} else {
break;
}
} else {
// violates child bounds
break;
}
}
return node;
}
/**
*
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
ensure_bounds(x0, y0, x1, y1) {
const dimensions = this.#dimensions;
const bounds_x0 = dimensions[0];
const bounds_y0 = dimensions[1];
const bounds_x1 = dimensions[2];
const bounds_y1 = dimensions[3];
if (
x0 >= bounds_x0
&& y0 >= bounds_y0
&& x1 < bounds_x1
&& y1 < bounds_y1
) {
// bounds are satisfied
return;
}
// dimensions violated
this.#setDimensions(
min2(x0, bounds_x0),
min2(y0, bounds_y0),
max2(x1, bounds_x1),
max2(y1, bounds_y1),
);
}
/**
* Assumes element is allocated and is not present in the tree yet
* @param {number} element
*/
element_insert(element) {
const element_pool = this.#element_pool;
const element_word = element_pool.element_word(element);
// clear out element pointers
element_pool.data_uint32[element_word + COLUMN_ELEMENT_NEXT] = QT_NULL_POINTER;
const x0 = element_pool.data_float32[element_word + COLUMN_ELEMENT_X0];
const y0 = element_pool.data_float32[element_word + COLUMN_ELEMENT_Y0];
const x1 = element_pool.data_float32[element_word + COLUMN_ELEMENT_X1];
const y1 = element_pool.data_float32[element_word + COLUMN_ELEMENT_Y1];
this.ensure_bounds(x0, y0, x1, y1);
let parent_node = this.#find_parent_for_box(x0, y0, x1, y1);
if (parent_node === QT_NULL_POINTER) {
this.#root = this.#node_allocate();
parent_node = this.#root;
}
this.#insert_element_into(parent_node, element);
this.#node_balance(parent_node);
}
#node_allocate() {
const pool = this.#node_pool;
const node = pool.allocate();
const address = pool.element_word(node);
const uint32 = pool.data_uint32;
// initialize pointers
uint32[address + COLUMN_TREE_NODE_PARENT] = QT_NULL_POINTER;
uint32[address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = QT_NULL_POINTER;
uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] = QT_NULL_POINTER;
uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] = QT_NULL_POINTER;
uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] = QT_NULL_POINTER;
uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] = QT_NULL_POINTER;
return node;
}
/**
* This operation may create new nodes
* @param {number} node
*/
#node_balance(node) {
// count owned elements
const address = this.#node_pool.element_word(node);
const element_count = this.#node_pool.data_uint32[address + COLUMN_TREE_NODE_ELEMENT_COUNT];
if (element_count > THRESHOLD_SPLIT) {
this.#node_push_data_down(node);
} else if (element_count < THRESHOLD_MERGE) {
this.#node_pull_data();
}
}
/**
*
* @param {number} node
* @param {number[]|Uint32Array} destination
* @param {number} destination_offset
* @return {number} number of elements read
*/
node_read_elements(node, destination, destination_offset) {
const node_address = this.#node_pool.element_word(node);
let element = this.#node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];
let count = 0;
while (element !== QT_NULL_POINTER) {
destination[destination_offset + count] = element;
count++;
const element_address = this.#element_pool.element_word(element);
element = this.#element_pool.data_uint32[element_address + COLUMN_ELEMENT_NEXT];
}
return count;
}
/**
* Take elements owned by the element and attempt to push them as deep as possible
* This operation may create new nodes
* @param {number} node
*/
#node_push_data_down(node) {
}
#node_pull_data() {
}
/**
*
* @param {number} node
* @param {number} element
*/
#insert_element_into(node, element) {
const node_pool = this.#node_pool;
const node_address = node_pool.element_word(node);
// increment node count
node_pool[node_address + COLUMN_TREE_NODE_ELEMENT_COUNT]++;
let _element = node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];
const element_pool = this.#element_pool;
if (_element === QT_NULL_POINTER) {
// first element
node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = element;
} else {
// scan to the last element
let _link = _element;
for (; ;) {
const address = element_pool.element_word(_link);
const next = element_pool.data_uint32[address + COLUMN_ELEMENT_NEXT];
if (next === QT_NULL_POINTER) {
// end of the list is found
element_pool.data_uint32[address + COLUMN_ELEMENT_NEXT] = element;
break;
}
_link = next;
}
}
// patch links on the element
const element_address = element_pool.element_word(element);
element_pool.data_uint32[element_address + COLUMN_ELEMENT_PARENT_NODE] = node;
}
/**
*
* @param {number} datum_id ID of data in tree
*/
element_remove(datum_id) {
const element_pool = this.#element_pool;
const element_word = element_pool.element_word(datum_id);
const tree_node_id = element_pool.data_uint32[element_word + COLUMN_ELEMENT_PARENT_NODE];
this.#tree_node_remove_element(tree_node_id, datum_id);
}
/**
*
* @param {number} node
* @return {boolean}
*/
#is_node_empty(node) {
const pool = this.#node_pool;
const word = pool.element_word(node);
const uint32 = pool.data_uint32;
const first_element_node = uint32[word + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER]
if (first_element_node !== QT_NULL_POINTER) {
return false;
}
const has_allocated_children = uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] !== QT_NULL_POINTER
|| uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] !== QT_NULL_POINTER
|| uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] !== QT_NULL_POINTER
|| uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] !== QT_NULL_POINTER;
return !has_allocated_children;
}
/**
* Assumes the node is empty
* Does not perform any checks
* Only updates parent node
* @param {number} node
* @returns {number} parent of this node
*/
#remove_node(node) {
const pool = this.#node_pool;
const word = pool.element_word(node);
const uint32 = pool.data_uint32;
// let's remove reference from the parent to this node
const parent = uint32[word + COLUMN_TREE_NODE_PARENT];
if (parent !== QT_NULL_POINTER) {
// not root
const parent_word = pool.element_word(parent);
if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] === node) {
uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] = QT_NULL_POINTER;
} else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] === node) {
uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] = QT_NULL_POINTER;
} else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] === node) {
uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] = QT_NULL_POINTER;
} else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] === node) {
uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] = QT_NULL_POINTER;
} else {
throw new Error(`specified 'parent' node(${parent}) does not point to this node(${node})`);
}
} else {
// we are root
this.#root = QT_NULL_POINTER;
}
// de-allocate self
pool.release(node);
return parent;
}
/**
* Remove node if it's empty, propagates up the parent chain if possible
* @param {number} node
* @returns {boolean} true if collapsed
*/
#try_remove_node(node) {
let n = node;
while (n !== QT_NULL_POINTER && this.#is_node_empty(n)) {
// at this point we store no data and have no children, node is useless and can be reclaimed
n = this.#remove_node(n);
}
return true;
}
/**
* NOTE: This method does NOT do de-allocation
* @param {number} node ID of tree node
* @param {number} element ID of inserted element
* @returns {boolean} if element was found and cut
*/
#tree_node_remove_element(
node,
element
) {
assert.isNonNegativeInteger(node, 'tree_node_id');
assert.isNonNegativeInteger(element, 'element_id');
const node_pool = this.#node_pool;
const node_address = node_pool.element_word(node);
let element_node_pointer = node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];
let previous_node_pointer = QT_NULL_POINTER;
const element_pool = this.#element_pool;
while (element_node_pointer !== QT_NULL_POINTER) {
const element_node_word = element_pool.element_word(element_node_pointer);
const next_element_pointer = element_pool.data_uint32[element_node_word + COLUMN_ELEMENT_NEXT];
if (element_node_pointer === element) {
// found the right element
node_pool.data_uint32[node_address + COLUMN_TREE_NODE_ELEMENT_COUNT]--;
if (previous_node_pointer === QT_NULL_POINTER) {
// this was the first element in the list
node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = next_element_pointer;
// this might be the last element in the list, try to clean-up node if possible
this.#try_remove_node(node);
} else {
// patch previous element to point to the next element
const previous_node_word = element_pool.element_word(previous_node_pointer);
element_pool.data_uint32[previous_node_word + COLUMN_ELEMENT_NEXT] = next_element_pointer;
}
return true;
}
previous_node_pointer = element_node_pointer;
element_node_pointer = next_element_pointer;
}
// element not found
return false;
}
/**
* Remove all data
*/
release_all() {
this.#root = QT_NULL_POINTER;
this.#element_pool.clear();
this.#node_pool.clear();
}
}