/** * Create a new BitMask instance. * @param {Record} [states=null] An object containing valid states and their corresponding initial boolean values (default is null). */ export default class BitMask extends Number { constructor(states=null) { super(); this.#generateValidStates(states); this.#generateEnum(); this.#value = this.#computeValue(states); } /** * The real value behind the bitmask instance. * @type {number} */ #value; /** * The structure of valid states and their associated values. * @type {Map} */ #validStates; /** * The enum associated with this structure. * @type {Record} * @readonly */ states; /* -------------------------------------------- */ /* Internals */ /* -------------------------------------------- */ /** * Generates the valid states and their associated values. * @param {Record} [states=null] The structure defining the valid states and their associated values. */ #generateValidStates(states) { this.#validStates = new Map(); let bitIndex = 0; for ( const state of Object.keys(states || {}) ) { if ( bitIndex >= 32 ) throw new Error("A bitmask can't handle more than 32 states"); this.#validStates.set(state, 1 << bitIndex++); } } /* -------------------------------------------- */ /** * Generates an enum based on the provided valid states. */ #generateEnum() { this.states = {}; for ( const state of this.#validStates.keys() ) this.states[state] = state; Object.freeze(this.states); } /* -------------------------------------------- */ /** * Calculate the default value of the bitmask based on the initial states * @param {Record} [initialStates={}] The structure defining the valid states and their associated values. * @returns {number} */ #computeValue(initialStates={}) { let defaultValue = 0; for ( const state in initialStates ) { if ( !initialStates.hasOwnProperty(state) ) continue; this.#checkState(state); if ( initialStates[state] ) defaultValue |= this.#validStates.get(state); } return defaultValue; } /* -------------------------------------------- */ /** * Checks a state and throws an error if it doesn't exist. * @param {string} state Name of the state to check. */ #checkState(state) { if ( !this.#validStates.has(state) ) { throw new Error(`${state} is an invalid state for this BitMask instance: ${this.toJSON()}`); } } /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /** * True if this bitmask is empty (no active states). * @type {boolean} */ get isEmpty() { return this.#value === 0; } /* -------------------------------------------- */ /* Methods for Handling states */ /* -------------------------------------------- */ /** * Check if a specific state is active. * @param {string} state The state to check. * @returns {boolean} True if the state is active, false otherwise. */ hasState(state) { return (this.#value & this.#validStates.get(state)) !== 0; } /* -------------------------------------------- */ /** * Add a state to the bitmask. * @param {string} state The state to add. * @throws {Error} Throws an error if the provided state is not valid. */ addState(state) { this.#checkState(state); this.#value |= this.#validStates.get(state); } /* -------------------------------------------- */ /** * Remove a state from the bitmask. * @param {string} state The state to remove. * @throws {Error} Throws an error if the provided state is not valid. */ removeState(state) { this.#checkState(state); this.#value &= ~this.#validStates.get(state); } /* -------------------------------------------- */ /** * Toggle the state of a specific state in the bitmask. * @param {string} state The state to toggle. * @param {boolean} [enabled] Toggle on (true) or off (false)? If undefined, the state is switched automatically. * @throws {Error} Throws an error if the provided state is not valid. */ toggleState(state, enabled) { this.#checkState(state); if ( enabled === undefined ) return (this.#value ^= this.#validStates.get(state)); if ( enabled ) this.addState(state); else this.removeState(state); } /* -------------------------------------------- */ /** * Clear the bitmask, setting all states to inactive. */ clear() { this.#value = 0; } /* -------------------------------------------- */ /* bitmask representations */ /* -------------------------------------------- */ /** * Get the current value of the bitmask. * @returns {number} The current value of the bitmask. */ valueOf() { return this.#value; } /* -------------------------------------------- */ /** * Get a string representation of the bitmask in binary format. * @returns {string} The string representation of the bitmask. */ toString() { return String(this.#value.toString(2)).padStart(this.#validStates.size, '0'); } /* -------------------------------------------- */ /** * Checks if two bitmasks structures are compatible (the same valid states). * @param {BitMask} otherBitMask The bitmask structure to compare with. * @returns {boolean} True if the two bitmasks have the same structure, false otherwise. */ isCompatible(otherBitMask) { const states1 = Array.from(this.#validStates.keys()).sort().join(','); const states2 = Array.from(otherBitMask.#validStates.keys()).sort().join(','); return states1 === states2; } /* -------------------------------------------- */ /** * Serializes the bitmask to a JSON string. * @returns {string} The JSON string representing the bitmask. */ toJSON() { return JSON.stringify(this.toObject()); } /* -------------------------------------------- */ /** * Creates a new BitMask instance from a JSON string. * @param {string} jsonString The JSON string representing the bitmask. * @returns {BitMask} A new BitMask instance created from the JSON string. */ static fromJSON(jsonString) { const data = JSON.parse(jsonString); return new BitMask(data); } /* -------------------------------------------- */ /** * Convert value of this BitMask to object representation according to structure. * @returns {Object} The data represented by the bitmask. */ toObject() { const result = {}; for ( const [validState, value] of this.#validStates ) result[validState] = ((this.#value & value) !== 0); return result; } /* -------------------------------------------- */ /** * Creates a clone of this BitMask instance. * @returns {BitMask} A new BitMask instance with the same value and valid states as this instance. */ clone() { return new BitMask(this.toObject()); } /* -------------------------------------------- */ /* Static Helpers */ /* -------------------------------------------- */ /** * Generates shader constants based on the provided states. * @param {string[]} states An array containing valid states. * @returns {string} Shader bit mask constants generated from the states. */ static generateShaderBitMaskConstants(states) { let shaderConstants = ''; let bitIndex = 0; for ( const state of states ) { shaderConstants += `const uint ${state.toUpperCase()} = 0x${(1 << bitIndex).toString(16).toUpperCase()}U;\n`; bitIndex++; } return shaderConstants; } }