UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

715 lines (559 loc) • 21.5 kB
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(); } }