UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

455 lines (361 loc) • 10.3 kB
import { assert } from "../../assert.js"; import { max2 } from "../../math/max2.js"; const UINT32_MAX = 4294967295; const DEFAULT_CAPACITY = 64; const ELEMENT_BYTE_SIZE = 8; /** * % to increase capacity by when growing * NOTE: Must be greater than 1 * @type {number} */ const RESIZE_GROW_FACTOR = 1.2; /** * Minimum number of elements to expand the size by when growing * NOTE: Must be an integer * NOTE: Must be greater than 0 * @type {number} */ const RESIZE_GROW_MIN_COUNT = 16; /** * * @param {number} i * @returns {number} */ function HEAP_PARENT(i) { return ((i) - 1) >> 1; } /** * * @param {number} i * @returns {number} */ function HEAP_LEFT(i) { return ((i) << 1) + 1; } /** * * @param {number} i * @returns {number} */ function HEAP_RIGHT(i) { return ((i) << 1) + 2; } /** * Binary Heap implementation that stores uin32 ID along with a floating point score value * Very fast and compact * Inspired by Blender's heap implementation found here: https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenlib/intern/BLI_heap.c */ export class Uint32Heap { /** * * @param {number} [initial_capacity] Can supply initial capacity, heap will still grow when necessary. This allows to prevent needless re-allocations when max heap size is known in advance */ constructor(initial_capacity = DEFAULT_CAPACITY) { assert.isNonNegativeInteger(initial_capacity, 'capacity'); this.__data_buffer = new ArrayBuffer(initial_capacity * ELEMENT_BYTE_SIZE); /** * Used to access stored IDs * @type {Uint32Array} * @private */ this.__data_uint32 = new Uint32Array(this.__data_buffer); /** * Used to access stored Score values * @type {Float32Array} * @private */ this.__data_float32 = new Float32Array(this.__data_buffer); /** * * @type {number} * @private */ this.__capacity = initial_capacity; /** * * @type {number} * @private */ this.__size = 0; } /** * * @private */ __capacity_grow() { const old_capacity = this.__capacity; const new_capacity = Math.ceil(max2( old_capacity * RESIZE_GROW_FACTOR, old_capacity + RESIZE_GROW_MIN_COUNT )); assert.greaterThan(new_capacity, old_capacity, 'invalid growth'); const new_buffer = new ArrayBuffer(new_capacity * ELEMENT_BYTE_SIZE); const new_uint32 = new Uint32Array(new_buffer); // copy old data into new container new_uint32.set(this.__data_uint32); this.__data_buffer = new_buffer; this.__data_uint32 = new_uint32; this.__data_float32 = new Float32Array(new_buffer); // update capacity this.__capacity = new_capacity; } /** * @private * @param {number} a index of an element * @param {number} b index of an element * @returns {boolean} */ compare(a, b) { const float32 = this.__data_float32; const a2 = a << 1; // same as a*2 const b2 = b << 1; // same as b*2 return float32[a2] < float32[b2]; } /** * Swap two elements * @private * @param {number} i element index * @param {number} j element index */ swap(i, j) { // fast multiplication by 2 const i2 = i << 1; // same as i*2 const j2 = j << 1; // same as j*2 const uint32 = this.__data_uint32; const mem_0 = uint32[i2]; uint32[i2] = uint32[j2]; uint32[j2] = mem_0; const i21 = i2 + 1; const j21 = j2 + 1; const mem_1 = uint32[i21]; uint32[i21] = uint32[j21]; uint32[j21] = mem_1; } /** * @private * @param {number} index */ heap_down(index) { let i = index; // size does not change, cache it for performance const size = this.__size; while (true) { const left = (i << 1) + 1; const right = left + 1; let smallest = i; if (left < size && this.compare(left, smallest)) { smallest = left; } if (right < size && this.compare(right, smallest)) { smallest = right; } if (smallest === i) { break; } this.swap(i, smallest); i = smallest; } } /** * Bubble up given element into its correct position * @private * @param {number} index */ heap_up(index) { let i = index; while (i > 0) { // get parent const p = ((i) - 1) >> 1; if (this.compare(p, i)) { break; } this.swap(p, i); i = p; } } /** * * @returns {number} */ get size() { return this.__size; } /** * * @returns {number} */ get capacity() { return this.__capacity; } /** * Node with the lowest score * @returns {number} */ get top_id() { return this.__data_uint32[1]; } /** * * @returns {boolean} */ is_empty() { return this.__size === 0; } peek_min() { return this.top_id; } pop_min() { assert.greaterThan(this.__size, 0, 'heap is empty'); const new_size = this.__size - 1; this.__size = new_size; const uint32 = this.__data_uint32; const top_id = uint32[1]; // move bottom element to the top. // the top doesn't need to be moved down as we have discarded it already const i2 = new_size << 1; // same as *2 uint32[0] = uint32[i2]; uint32[1] = uint32[i2 + 1]; // re-balance this.heap_down(0); return top_id; } /** * * @param {number} id * @returns {number} */ find_index_by_id(id) { const n = this.__size const n2 = n << 1; // fast *2 multiplication const uint32 = this.__data_uint32; for (let address = 1; address < n2; address += 2) { const _id = uint32[address]; if (_id === id) { // reverse address to index return (address >>> 1); } } // not found return -1; } /** * * @param {number} id * @returns {boolean} */ contains(id) { return this.find_index_by_id(id) !== -1; } /** * Clear out all the data, heap will be made empty */ clear() { this.__size = 0; } /** * * @param {number} id * @returns {boolean} */ remove(id) { const i = this.find_index_by_id(id); if (i === -1) { return false; } this.__remove_by_index(i); return true; } /** * * @param {number} index */ __remove_by_index(index) { assert.greaterThan(this.__size, 0, 'heap is empty'); let i = index; while (i > 0) { const p = HEAP_PARENT(i); this.swap(p, i); i = p; } this.pop_min(); } /** * * @param {number} id * @param {number} score */ update_score(id, score) { const index = this.find_index_by_id(id); if (index === -1) { throw new Error('Not found'); } this.__update_score_by_index(index, score); } /** * Update score of an element referenced directly by index, this is a fast method, but you're generally not going to know the index so in most cases it's best to use "update_score" instead * @param {number} index * @param {number} score */ __update_score_by_index(index, score) { const float32 = this.__data_float32; const index2 = index << 1; // fast *2 multiplication const existing_score = float32[index2]; if (score < existing_score) { float32[index2] = score; this.heap_up(index); } else if (score > existing_score) { float32[index2] = score; this.heap_down(index); } } /** * * @param {number} id * @returns {number} */ get_score(id) { const index = this.find_index_by_id(id); if (index === -1) { return Number.NaN; } const index2 = index << 1; // fast *2 multiplication return this.__data_float32[index2]; } /** * * @param {number} id * @param {number} score */ insert_or_update(id, score) { const i = this.find_index_by_id(id); if (i === -1) { this.insert(id, score); } else { this.__update_score_by_index(i, score); } } /** * * @param {number} id * @param {number} score */ insert(id, score) { assert.isNonNegativeInteger(id, 'value'); assert.lessThanOrEqual(id, UINT32_MAX - 1, 'must be less than or equal to (2^32 - 2)'); assert.isNumber(score, 'score'); const current_size = this.__size; if (current_size >= this.__capacity) { // need to re-allocate this.__capacity_grow(); } // insert at the end const index = current_size; const address = index << 1; // same as *2 // write data this.__data_float32[address] = score; this.__data_uint32[address + 1] = id; // record increased size this.__size = current_size + 1; this.heap_up(index); } }