UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

623 lines (485 loc) • 17.8 kB
import { assert } from "../assert.js"; import { max3 } from "../math/max3.js"; import { min2 } from "../math/min2.js"; import { align_32 } from "./align_32.js"; import { lsb_32 } from "./lsb_32.js"; /** * Used for overallocating space when bit set needs to grow * @constant * @type {number} */ const GROW_FACTOR = 1.3; /** * Used to allow some un-assigned space to be retained when shrinking * @constant * @type {number} */ const DEFAULT_SHRINK_FACTOR = 0.5; /** * Minimum number of bits to increase the capacity by * @type {number} */ const RESIZE_COUNT_THRESHOLD = 128; /** * @readonly * @type {number} */ const DEFAULT_INITIAL_CAPACITY = 64; /** * A dynamically resizable bitset (bit array) implementation. * The bitset automatically grows and shrinks as bits are set and cleared. * * @example * const bits = new BitSet(); * bits.set(10); * * bits.get(11) // false * bits.get(10) // true * bits.get(9) // false * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class BitSet { /** * @constructor * @param {number} [initial_capacity] optional, can supply initial capacity in bits to avoid resizing. */ constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) { assert.isNonNegativeInteger(initial_capacity, 'initial_capacity'); /** * Number of bits currently in use (highest set bit + 1). * @private * @type {number} */ this.__length = 0; /** * Current capacity in bits, this is at least equal to length * Always a multiple of 32. * @private * @type {number} */ this.__capacity = align_32(initial_capacity); /** * Uint32-backed storage * @type {Uint32Array} * @private */ this.__data_uint32 = new Uint32Array(this.__capacity >> 5); /** * The fraction of capacity at which to trigger a shrink operation. * @type {number} */ this.__shrinkFactor = DEFAULT_SHRINK_FACTOR; } /** * Prevents the BitSet from shrinking automatically. * Useful for fixed-size bitsets. * @returns {void} */ preventShrink() { this.setShrinkFactor(0); } /** * * @param {number} x The new shrink factor (must be between 0 and 1, inclusive of 0, exclusive of 1). */ setShrinkFactor(x) { assert.greaterThanOrEqual(x, 0, 'x >= 0'); assert.lessThan(x, 1, 'x < 1'); this.__shrinkFactor = x; } /** * Sets the capacity of the bitset, ensuring enough space. * Does NOT change set bits. * @param {number} bit_count The new capacity, in bits. * @throws {Error} If the requested capacity is smaller than the current {@link size}. * @returns {void} */ setCapacity(bit_count) { assert.isNonNegativeInteger(bit_count, "bit_count"); if (this.__length > bit_count) { throw new Error(`Current length(=${this.__length}) is greater than requested size(=${bit_count})`); } this.__resize(bit_count); } /** * Returns the number of bits currently "used" by the BitSet. * This is equivalent to the index of the highest set bit plus one. * @returns {number} The size of the BitSet (in bits). */ size() { return this.__length; } /** * Returns the current capacity (allocated bits) of the BitSet. * This is always a multiple of 32. * @returns {number} The capacity of the BitSet (in bits). */ capacity() { return this.__capacity; } /** * Resizes the internal Uint32Array to the specified capacity. * @param {number} bitCapacity New capacity in bits. * @private */ __resize(bitCapacity) { assert.isNonNegativeInteger(bitCapacity, 'bitCapacity'); const uint32_capacity = Math.ceil(bitCapacity / 32); const oldData = this.__data_uint32; const newData = new Uint32Array(uint32_capacity); //copy data from old container const oldDataSize = oldData.length; if (oldDataSize < uint32_capacity) { newData.set(oldData); } else { //creating a sub-array is using heap memory, so we prefer to avoid it newData.set(oldData.subarray(0, uint32_capacity)); } this.__data_uint32 = newData; this.__capacity = uint32_capacity * 32; } /** * Updates the internal length of the bitset after clearing bits. * @private */ __updateLength() { const found_length = this.previousSetBit(this.__length) + 1; if (found_length < this.__length) { this.__setLength(found_length); } } /** * Sets the internal length of the BitSet and manages capacity. * Grows or shrinks if necessary * @param {number} new_length The new length of the BitSet. * @private */ __setLength(new_length) { this.__length = new_length; const capacity = this.__capacity; if (new_length > capacity) { const growSize = Math.ceil(max3( new_length, capacity + RESIZE_COUNT_THRESHOLD, capacity * GROW_FACTOR )); this.__resize(growSize); } else { if ( new_length < capacity - RESIZE_COUNT_THRESHOLD && new_length < capacity * this.__shrinkFactor ) { this.__resize(new_length); } } } /** * Returns the index of the nearest bit that is set to true that occurs on or before the specified starting index * @param {number} from_index The index to start searching from (inclusive). * @returns {number} Index of previous set bit, or -1 if no set bit found */ previousSetBit(from_index) { assert.isNonNegativeInteger(from_index, `fromIndex`); const index = min2(from_index, this.__length - 1); let word_index = index >> 5; let bit_index = index & 31; // modulo operation is slow, bitwise and is fast, this is the same as %8 const data = this.__data_uint32; let word = data[word_index]; //handle first byte separately due to potential partial traversal for (; bit_index >= 0; bit_index--) { if ((word & (1 << bit_index)) !== 0) { return word_index * 32 + bit_index; } } //unwind first byte word_index--; //scan the rest for (; word_index >= 0; word_index--) { word = data[word_index]; for (bit_index = 31; bit_index >= 0; bit_index--) { if ((word & (1 << bit_index)) !== 0) { return word_index * 32 + bit_index; } } } return -1; } /** * Returns the index of the first bit that is set to false that occurs on or after the specified starting index. * @param {number} from_index The index to start searching from (inclusive). * @returns {number} index of the next set bit, or -1 if no bits are set beyond supplied index */ nextSetBit(from_index) { // assert.ok(fromIndex >= 0, `fromIndex must be greater or equal to 0, instead was ${fromIndex}`); // assert.ok(Number.isInteger(fromIndex), `fromIndex must be an integer, instead was ${fromIndex}`); const bit_length = this.__length; if (from_index >= bit_length) { //index is out of bounds, return -1 return -1; } const data = this.__data_uint32; let word_index = from_index >> 5; let word; let bit_address; let bit_index = from_index & 31; if (bit_index !== 0) { // bit offset boundary in inside the word, we need to mask part of the word const fill_mask = ~((1 << bit_index) - 1); const masked_word = data[word_index] & fill_mask; if (masked_word !== 0) { bit_index = lsb_32(masked_word); return (word_index << 5) + bit_index; } word_index++; } //scan the rest of the words const word_count = (bit_length + 31) >> 5; // Math.ceil(x /32) for (; word_index < word_count; word_index++) { word = data[word_index]; if (word === 0) { // all 0s continue; } bit_index = lsb_32(word); bit_address = (word_index << 5) + bit_index; return bit_address; } return -1; } /** * Returns the index of the first bit that is set to false that occurs on or after the specified starting index. * @param {number} from_index The index to start searching from (inclusive). * @returns {number} Index of the next clear bit (bit set to false), or the current length of BitSet if all the bits are set. */ nextClearBit(from_index) { //assert.ok(fromIndex >= 0, `fromIndex must be greater or equal to 0, instead was ${fromIndex}`); //assert.ok(Number.isInteger(fromIndex), `fromIndex must be an integer, instead was ${fromIndex}`); let word_index = from_index >> 5; let word; // treat first word specially, as we may need to mask out portion of a word to skip certain number of bits let bit_index = from_index & 31; const data = this.__data_uint32; if (bit_index !== 0) { // bit offset boundary in inside the word, we need to mask part of the word word = data[word_index]; const fill_mask = (1 << bit_index) - 1; // apply mask and convert to uint32 via shift const masked_word = (word | fill_mask) >>> 0; if (masked_word !== 4294967295) { bit_index = lsb_32(~masked_word); return bit_index + word_index * 32; } word_index++; } const set_length = this.__length; const word_count = (set_length + 31) >> 5; // Math.ceil(x /32) //scan the rest for (; word_index < word_count; word_index++) { word = data[word_index]; if (word === 4294967295) { // no 0s continue; } bit_index = lsb_32(~word); return bit_index + word_index * 32; } return set_length; } /** * Sets the bit at the specified index to the specified value. * @param {number} bit_index The index of the bit to set. * @param {boolean} value The value to set the bit to (true for 1, false for 0). * @returns {void} */ set(bit_index, value) { assert.isNonNegativeInteger(bit_index, 'bit_index'); assert.isBoolean(value, 'value'); const word_offset = bit_index >> 5; const bit_offset = bit_index & 31; //const oldLength = this.__length; const word_mask = 1 << bit_offset; if (value) { const bitIndexInc = bit_index + 1; if (bitIndexInc > this.__length) { // ensure capacity this.__setLength(bitIndexInc); } //set this.__data_uint32[word_offset] |= word_mask; } else if (bit_index < this.__length) { //clear this.__data_uint32[word_offset] &= ~word_mask; // trim down set size potentially this.__updateLength(); } //DEBUG validate firstClearBit value //assert.ok(this.__firstClearBitIndex === -1 || this.__firstClearBitIndex === scanToFirstClearBit(this), `Invalid first clear bit index, oldLength=${oldLength}, bitIndex=${bitIndex}`); } /** * Sets the bit specified by the index to false (0). * @param {number} bit_index The index of the bit to clear. */ clear(bit_index) { this.set(bit_index, false); } /** * Set all bits in a given range * @param {number} start_index first bit to be set (inclusive) * @param {number} end_index last bit to be set (inclusive) */ setRange(start_index, end_index) { assert.greaterThanOrEqual(start_index, 0, "invalid start index"); assert.greaterThanOrEqual(end_index, 0, "invalid end index"); for (let i = start_index; i <= end_index; i++) { this.set(i, true); } } /** * Clears bit values in a given (inclusive) range * @param {number} start_index first bit to be cleared (inclusive) * @param {number} end_index clear up to here, excluding this position */ clearRange(start_index, end_index) { assert.greaterThanOrEqual(start_index, 0, "invalid start index"); assert.greaterThanOrEqual(end_index, 0, "invalid end index"); for (let i = start_index; i < end_index; i++) { this.set(i, false); } } /** * Returns the value of the bit with the specified index. * @param {int} bit_index The index of the bit to get. * @returns {boolean} The value of the bit (true for 1, false for 0). Returns `false` if `bitIndex` is out of bounds. */ get(bit_index) { assert.isNonNegativeInteger(bit_index, "bitIndex"); if (bit_index >= this.__length) { //bit is outside the recorded region return false; } const byteOffset = bit_index >> 5; const bitOffset = bit_index & 31; const word = this.__data_uint32[byteOffset]; const masked_word = word & (1 << bitOffset); return masked_word !== 0; } /** * Set a bit at the specified index and return its previous value * @param {number} index * @returns {boolean} */ getAndSet(index) { const v = this.get(index); if (!v) { this.set(index, true); } return v; } /** * Clear a bit at the specified index and return its previous value * @param {number} index * @returns {boolean} */ getAndClear(index) { const v = this.get(index); if (v) { this.set(index, false); } return v; } /** * * @param {number} distance * @param {number} start_index * @param {number} end_index */ shift_right(distance, start_index, end_index) { assert.isNonNegativeInteger(distance, 'distance'); for (let i = end_index; i >= start_index; i--) { const v = this.get(i); this.set(i + distance, v); } } /** * * @param {number} distance * @param {number} start_index * @param {number} end_index */ shift_left(distance, start_index, end_index) { assert.isNonNegativeInteger(distance, 'distance'); for (let i = start_index; i <= end_index; i++) { const v = this.get(i); this.set(i - distance, v); } } /** * * @param {number} distance * @param {number} start_index * @param {number} end_index */ shift(distance, start_index, end_index) { if (distance > 0) { this.shift_right(distance, start_index, end_index); } else { this.shift_right(-distance, start_index, end_index); } } /** * Sets all the bits in this BitSet to false. */ reset() { const current_length = this.__length; if (current_length <= 0) { // no action required return; } if (current_length <= 32) { // one word, set first word to 0 manually this.__data_uint32[0] = 0; } else { this.__data_uint32.fill(0, 0, Math.ceil(current_length / 32)); } this.__length = 0; } /** * Copy contents of another bit set into this one * @param {BitSet} other */ copy(other) { const length = other.__length; const uint32_size = length >> 5; const old_length = this.__length; if (old_length !== length) { if (old_length < length) { this.__resize(length); } else { // clear everything past the target region this.__data_uint32.fill(0, uint32_size); } this.__length = length; } for (let i = 0; i < uint32_size; i++) { this.__data_uint32[i] = other.__data_uint32[i]; } const byte_aligned_bit_size = uint32_size << 5; const overflow_bits = length - byte_aligned_bit_size; for (let i = 0; i < overflow_bits; i++) { const bitIndex = byte_aligned_bit_size + i; this.set(bitIndex, other.get(bitIndex)); } } /** * * @param {number} x * @returns {BitSet} */ static fixedSize(x) { const r = new BitSet(x); r.preventShrink(); return r; } }