Files
Foundry-VTT-Docker/resources/app/common/utils/bitmask.mjs
2025-01-04 00:34:03 +01:00

260 lines
7.7 KiB
JavaScript

/**
* Create a new BitMask instance.
* @param {Record<string, boolean>} [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<string, number>}
*/
#validStates;
/**
* The enum associated with this structure.
* @type {Record<string, string>}
* @readonly
*/
states;
/* -------------------------------------------- */
/* Internals */
/* -------------------------------------------- */
/**
* Generates the valid states and their associated values.
* @param {Record<string, boolean>} [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<string, boolean>} [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;
}
}