@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
455 lines (361 loc) • 10.3 kB
JavaScript
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);
}
}