playcanvas
Version:
PlayCanvas WebGL game engine
1,437 lines (1,426 loc) • 19.7 MB
JavaScript
/**
* @license
* PlayCanvas Engine v2.14.4 revision a8e9f39 (DEBUG)
* Copyright 2011-2025 PlayCanvas Ltd. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Logs a frame number.
*
* @category Debug
*/ const TRACEID_RENDER_FRAME = 'RenderFrame';
/**
* Logs a frame time.
*
* @category Debug
*/ const TRACEID_RENDER_FRAME_TIME = 'RenderFrameTime';
/**
* Logs basic information about generated render passes.
*
* @category Debug
*/ const TRACEID_RENDER_PASS = 'RenderPass';
/**
* Logs additional detail for render passes.
*
* @category Debug
*/ const TRACEID_RENDER_PASS_DETAIL = 'RenderPassDetail';
/**
* Logs render actions created by the layer composition. Only executes when the
* layer composition changes.
*
* @category Debug
*/ const TRACEID_RENDER_ACTION = 'RenderAction';
/**
* Logs the allocation of render targets.
*
* @category Debug
*/ const TRACEID_RENDER_TARGET_ALLOC = 'RenderTargetAlloc';
/**
* Logs the allocation of textures.
*
* @category Debug
*/ const TRACEID_TEXTURE_ALLOC = 'TextureAlloc';
/**
* Logs the creation of shaders.
*
* @category Debug
*/ const TRACEID_SHADER_ALLOC = 'ShaderAlloc';
/**
* Logs the compilation time of shaders.
*
* @category Debug
*/ const TRACEID_SHADER_COMPILE = 'ShaderCompile';
/**
* Logs the vram use by the textures.
*
* @category Debug
*/ const TRACEID_VRAM_TEXTURE = 'VRAM.Texture';
/**
* Logs the vram use by the vertex buffers.
*
* @category Debug
*/ const TRACEID_VRAM_VB = 'VRAM.Vb';
/**
* Logs the vram use by the index buffers.
*
* @category Debug
*/ const TRACEID_VRAM_IB = 'VRAM.Ib';
/**
* Logs the vram use by the storage buffers.
*
* @category Debug
*/ const TRACEID_VRAM_SB = 'VRAM.Sb';
/**
* Logs the creation of bind groups.
*
* @category Debug
*/ const TRACEID_BINDGROUP_ALLOC = 'BindGroupAlloc';
/**
* Logs the creation of bind group formats.
*
* @category Debug
*/ const TRACEID_BINDGROUPFORMAT_ALLOC = 'BindGroupFormatAlloc';
/**
* Logs the creation of render pipelines. WebBPU only.
*
* @category Debug
*/ const TRACEID_RENDERPIPELINE_ALLOC = 'RenderPipelineAlloc';
/**
* Logs the creation of compute pipelines. WebGPU only.
*
* @category Debug
*/ const TRACEID_COMPUTEPIPELINE_ALLOC = 'ComputePipelineAlloc';
/**
* Logs the creation of pipeline layouts. WebBPU only.
*
* @category Debug
*/ const TRACEID_PIPELINELAYOUT_ALLOC = 'PipelineLayoutAlloc';
/**
* Logs the internal debug information for Elements.
*
* @category Debug
*/ const TRACEID_ELEMENT = 'Element';
/**
* Logs the vram use by all textures in memory.
*
* @category Debug
*/ const TRACEID_TEXTURES = 'Textures';
/**
* Logs all assets in the asset registry.
*
* @category Debug
*/ const TRACEID_ASSETS = 'Assets';
/**
* Logs the render queue commands.
*
* @category Debug
*/ const TRACEID_RENDER_QUEUE = 'RenderQueue';
/**
* Logs the loaded GSplat resources for individual LOD levels of an octree.
*
* @category Debug
*/ const TRACEID_OCTREE_RESOURCES = 'OctreeResources';
/**
* Logs the GPU timings.
*
* @category Debug
*/ const TRACEID_GPU_TIMINGS = 'GpuTimings';
/**
* The engine version number. This is in semantic versioning format (MAJOR.MINOR.PATCH).
*/ const version = '2.14.4';
/**
* The engine revision number. This is the Git hash of the last commit made to the branch
* from which the engine was built.
*/ const revision = 'a8e9f39';
/**
* Merge the contents of two objects into a single object.
*
* @param {object} target - The target object of the merge.
* @param {object} ex - The object that is merged with target.
* @returns {object} The target object.
* @example
* const A = {
* a: function () {
* console.log(this.a);
* }
* };
* const B = {
* b: function () {
* console.log(this.b);
* }
* };
*
* extend(A, B);
* A.a();
* // logs "a"
* A.b();
* // logs "b"
* @ignore
*/ function extend(target, ex) {
for(const prop in ex){
const copy = ex[prop];
if (Array.isArray(copy)) {
target[prop] = extend([], copy);
} else if (copy && typeof copy === 'object') {
target[prop] = extend({}, copy);
} else {
target[prop] = copy;
}
}
return target;
}
/**
* Basically a very large random number (128-bit) which means the probability of creating two that
* clash is vanishingly small. GUIDs are used as the unique identifiers for Entities.
*
* @namespace
*/ const guid = {
/**
* Create an RFC4122 version 4 compliant GUID.
*
* @returns {string} A new GUID.
*/ create () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=>{
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
}
};
/**
* File path API.
*
* @namespace
*/ const path = {
/**
* The character that separates path segments.
*
* @type {string}
*/ delimiter: '/',
/**
* Join two or more sections of file path together, inserting a delimiter if needed.
*
* @param {...string} sections - Sections of the path to join.
* @returns {string} The joined file path.
* @example
* const path = pc.path.join('foo', 'bar');
* console.log(path); // Prints 'foo/bar'
* @example
* const path = pc.path.join('alpha', 'beta', 'gamma');
* console.log(path); // Prints 'alpha/beta/gamma'
*/ join (...sections) {
let result = sections[0];
for(let i = 0; i < sections.length - 1; i++){
const one = sections[i];
const two = sections[i + 1];
if (two[0] === path.delimiter) {
result = two;
continue;
}
if (one && two && one[one.length - 1] !== path.delimiter && two[0] !== path.delimiter) {
result += path.delimiter + two;
} else {
result += two;
}
}
return result;
},
/**
* Normalize the path by removing '.' and '..' instances.
*
* @param {string} pathname - The path to normalize.
* @returns {string} The normalized path.
*/ normalize (pathname) {
const lead = pathname.startsWith(path.delimiter);
const trail = pathname.endsWith(path.delimiter);
const parts = pathname.split('/');
let result = '';
let cleaned = [];
for(let i = 0; i < parts.length; i++){
if (parts[i] === '') continue;
if (parts[i] === '.') continue;
if (parts[i] === '..' && cleaned.length > 0) {
cleaned = cleaned.slice(0, cleaned.length - 2);
continue;
}
if (i > 0) cleaned.push(path.delimiter);
cleaned.push(parts[i]);
}
result = cleaned.join('');
if (!lead && result[0] === path.delimiter) {
result = result.slice(1);
}
if (trail && result[result.length - 1] !== path.delimiter) {
result += path.delimiter;
}
return result;
},
/**
* Split the pathname path into a pair [head, tail] where tail is the final part of the path
* after the last delimiter and head is everything leading up to that. tail will never contain
* a slash.
*
* @param {string} pathname - The path to split.
* @returns {string[]} The split path which is an array of two strings, the path and the
* filename.
*/ split (pathname) {
const lastDelimiterIndex = pathname.lastIndexOf(path.delimiter);
if (lastDelimiterIndex !== -1) {
return [
pathname.substring(0, lastDelimiterIndex),
pathname.substring(lastDelimiterIndex + 1)
];
}
return [
'',
pathname
];
},
/**
* Return the basename of the path. That is the second element of the pair returned by passing
* path into {@link path.split}.
*
* @param {string} pathname - The path to process.
* @returns {string} The basename.
* @example
* pc.path.getBasename("/path/to/file.txt"); // returns "file.txt"
* pc.path.getBasename("/path/to/dir"); // returns "dir"
*/ getBasename (pathname) {
return path.split(pathname)[1];
},
/**
* Get the directory name from the path. This is everything up to the final instance of
* {@link path.delimiter}.
*
* @param {string} pathname - The path to get the directory from.
* @returns {string} The directory part of the path.
*/ getDirectory (pathname) {
return path.split(pathname)[0];
},
/**
* Return the extension of the path. Pop the last value of a list after path is split by
* question mark and comma.
*
* @param {string} pathname - The path to process.
* @returns {string} The extension.
* @example
* pc.path.getExtension("/path/to/file.txt"); // returns ".txt"
* pc.path.getExtension("/path/to/file.jpg"); // returns ".jpg"
* pc.path.getExtension("/path/to/file.txt?function=getExtension"); // returns ".txt"
*/ getExtension (pathname) {
const ext = pathname.split('?')[0].split('.').pop();
if (ext !== pathname) {
return `.${ext}`;
}
return '';
},
/**
* Check if a string s is relative path.
*
* @param {string} pathname - The path to process.
* @returns {boolean} True if s doesn't start with slash and doesn't include colon and double
* slash.
*
* @example
* pc.path.isRelativePath("file.txt"); // returns true
* pc.path.isRelativePath("path/to/file.txt"); // returns true
* pc.path.isRelativePath("./path/to/file.txt"); // returns true
* pc.path.isRelativePath("../path/to/file.jpg"); // returns true
* pc.path.isRelativePath("/path/to/file.jpg"); // returns false
* pc.path.isRelativePath("http://path/to/file.jpg"); // returns false
*/ isRelativePath (pathname) {
return pathname.charAt(0) !== '/' && pathname.match(/:\/\//) === null;
},
/**
* Return the path without file name. If path is relative path, start with period.
*
* @param {string} pathname - The full path to process.
* @returns {string} The path without a last element from list split by slash.
* @example
* pc.path.extractPath("path/to/file.txt"); // returns "./path/to"
* pc.path.extractPath("./path/to/file.txt"); // returns "./path/to"
* pc.path.extractPath("../path/to/file.txt"); // returns "../path/to"
* pc.path.extractPath("/path/to/file.txt"); // returns "/path/to"
*/ extractPath (pathname) {
let result = '';
const parts = pathname.split('/');
let i = 0;
if (parts.length > 1) {
if (path.isRelativePath(pathname)) {
if (parts[0] === '.') {
for(i = 0; i < parts.length - 1; ++i){
result += i === 0 ? parts[i] : `/${parts[i]}`;
}
} else if (parts[0] === '..') {
for(i = 0; i < parts.length - 1; ++i){
result += i === 0 ? parts[i] : `/${parts[i]}`;
}
} else {
result = '.';
for(i = 0; i < parts.length - 1; ++i){
result += `/${parts[i]}`;
}
}
} else {
for(i = 0; i < parts.length - 1; ++i){
result += i === 0 ? parts[i] : `/${parts[i]}`;
}
}
}
return result;
}
};
// detect whether passive events are supported by the browser
const detectPassiveEvents = ()=>{
let result = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: function() {
result = true;
return false;
}
});
window.addEventListener('testpassive', null, opts);
window.removeEventListener('testpassive', null, opts);
} catch (e) {}
return result;
};
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';
const environment = typeof window !== 'undefined' ? 'browser' : typeof global !== 'undefined' ? 'node' : 'worker';
// detect platform
const platformName = /android/i.test(ua) ? 'android' : /ip(?:[ao]d|hone)/i.test(ua) ? 'ios' : /windows/i.test(ua) ? 'windows' : /mac os/i.test(ua) ? 'osx' : /linux/i.test(ua) ? 'linux' : /cros/i.test(ua) ? 'cros' : null;
// detect browser
const browserName = environment !== 'browser' ? null : /Chrome\/|Chromium\/|Edg.*\//.test(ua) ? 'chrome' : /Safari\//.test(ua) ? 'safari' : /Firefox\//.test(ua) ? 'firefox' : 'other';
const xbox = /xbox/i.test(ua);
const touch = environment === 'browser' && ('ontouchstart' in window || 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0);
const gamepads = environment === 'browser' && (!!navigator.getGamepads || !!navigator.webkitGetGamepads);
const workers = typeof Worker !== 'undefined';
const passiveEvents = detectPassiveEvents();
/**
* Global namespace that stores flags regarding platform environment and features support.
*
* @namespace
* @example
* if (pc.platform.touch) {
* // touch is supported
* }
*/ const platform = {
/**
* String identifying the current platform. Can be one of: android, ios, windows, osx, linux,
* cros or null.
*
* @type {'android' | 'ios' | 'windows' | 'osx' | 'linux' | 'cros' | null}
* @ignore
*/ name: platformName,
/**
* String identifying the current runtime environment. Either 'browser', 'node' or 'worker'.
*
* @type {'browser' | 'node' | 'worker'}
*/ environment: environment,
/**
* The global object. This will be the window object when running in a browser and the global
* object when running in nodejs and self when running in a worker.
*
* @type {object}
*/ global: (typeof globalThis !== 'undefined' && globalThis) ?? (environment === 'browser' && window) ?? (environment === 'node' && global) ?? (environment === 'worker' && self),
/**
* Convenience boolean indicating whether we're running in the browser.
*
* @type {boolean}
*/ browser: environment === 'browser',
/**
* True if running in a Web Worker.
*
* @type {boolean}
* @ignore
*/ worker: environment === 'worker',
/**
* True if running on a desktop or laptop device.
*
* @type {boolean}
*/ desktop: [
'windows',
'osx',
'linux',
'cros'
].includes(platformName),
/**
* True if running on a mobile or tablet device.
*
* @type {boolean}
*/ mobile: [
'android',
'ios'
].includes(platformName),
/**
* True if running on an iOS device.
*
* @type {boolean}
*/ ios: platformName === 'ios',
/**
* True if running on an Android device.
*
* @type {boolean}
*/ android: platformName === 'android',
/**
* True if running on an Xbox device.
*
* @type {boolean}
*/ xbox: xbox,
/**
* True if the platform supports gamepads.
*
* @type {boolean}
*/ gamepads: gamepads,
/**
* True if the supports touch input.
*
* @type {boolean}
*/ touch: touch,
/**
* True if the platform supports Web Workers.
*
* @type {boolean}
*/ workers: workers,
/**
* True if the platform supports an options object as the third parameter to
* `EventTarget.addEventListener()` and the passive property is supported.
*
* @type {boolean}
* @ignore
*/ passiveEvents: passiveEvents,
/**
* Get the browser name.
*
* @type {'chrome' | 'safari' | 'firefox' | 'other' | null}
* @ignore
*/ browserName: browserName
};
const ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
const ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE;
const HIGH_SURROGATE_BEGIN = 0xD800;
const HIGH_SURROGATE_END = 0xDBFF;
const LOW_SURROGATE_BEGIN = 0xDC00;
const LOW_SURROGATE_END = 0xDFFF;
const ZERO_WIDTH_JOINER = 0x200D;
// Flag emoji
const REGIONAL_INDICATOR_BEGIN = 0x1F1E6;
const REGIONAL_INDICATOR_END = 0x1F1FF;
// Skin color modifications to emoji
const FITZPATRICK_MODIFIER_BEGIN = 0x1F3FB;
const FITZPATRICK_MODIFIER_END = 0x1F3FF;
// Accent characters
const DIACRITICAL_MARKS_BEGIN = 0x20D0;
const DIACRITICAL_MARKS_END = 0x20FF;
// Special emoji joins
const VARIATION_MODIFIER_BEGIN = 0xFE00;
const VARIATION_MODIFIER_END = 0xFE0F;
function getCodePointData(string, i = 0) {
const size = string.length;
// Account for out-of-bounds indices:
if (i < 0 || i >= size) {
return null;
}
const first = string.charCodeAt(i);
if (size > 1 && first >= HIGH_SURROGATE_BEGIN && first <= HIGH_SURROGATE_END) {
const second = string.charCodeAt(i + 1);
if (second >= LOW_SURROGATE_BEGIN && second <= LOW_SURROGATE_END) {
// https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
return {
code: (first - HIGH_SURROGATE_BEGIN) * 0x400 + second - LOW_SURROGATE_BEGIN + 0x10000,
long: true
};
}
}
return {
code: first,
long: false
};
}
function isCodeBetween(string, begin, end) {
if (!string) {
return false;
}
const codeData = getCodePointData(string);
if (codeData) {
const code = codeData.code;
return code >= begin && code <= end;
}
return false;
}
function numCharsToTakeForNextSymbol(string, index) {
if (index === string.length - 1) {
// Last character in the string, so we can only take 1
return 1;
}
if (isCodeBetween(string[index], HIGH_SURROGATE_BEGIN, HIGH_SURROGATE_END)) {
const first = string.substring(index, index + 2);
const second = string.substring(index + 2, index + 4);
// check if second character is fitzpatrick (color) modifier
// or if this is a pair of regional indicators (a flag)
if (isCodeBetween(second, FITZPATRICK_MODIFIER_BEGIN, FITZPATRICK_MODIFIER_END) || isCodeBetween(first, REGIONAL_INDICATOR_BEGIN, REGIONAL_INDICATOR_END) && isCodeBetween(second, REGIONAL_INDICATOR_BEGIN, REGIONAL_INDICATOR_END)) {
return 4;
}
// check if next character is a modifier, in which case we should return it
if (isCodeBetween(second, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
return 3;
}
// return surrogate pair
return 2;
}
// check if next character is the emoji modifier, in which case we should include it
if (isCodeBetween(string[index + 1], VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
return 2;
}
// just a regular character
return 1;
}
/**
* Extended String API.
*
* @namespace
*/ const string = {
/**
* All lowercase letters.
*
* @type {string}
*/ ASCII_LOWERCASE: ASCII_LOWERCASE,
/**
* All uppercase letters.
*
* @type {string}
*/ ASCII_UPPERCASE: ASCII_UPPERCASE,
/**
* All ASCII letters.
*
* @type {string}
*/ ASCII_LETTERS: ASCII_LETTERS,
/**
* Return a string with \{n\} replaced with the n-th argument.
*
* @param {string} s - The string to format.
* @param {...*} args - All other arguments are substituted into the string.
* @returns {string} The formatted string.
* @example
* const s = pc.string.format("Hello {0}", "world");
* console.log(s); // Prints "Hello world"
*/ format (s, ...args) {
for(let i = 0; i < args.length; i++){
s = s.replace(`{${i}}`, args[i]);
}
return s;
},
/**
* Get the code point number for a character in a string. Polyfill for
* [`codePointAt`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt}.
*
* @param {string} string - The string to get the code point from.
* @param {number} [i] - The index in the string.
* @returns {number} The code point value for the character in the string.
*/ getCodePoint (string, i) {
const codePointData = getCodePointData(string, i);
return codePointData && codePointData.code;
},
/**
* Gets an array of all code points in a string.
*
* @param {string} string - The string to get code points from.
* @returns {number[]} The code points in the string.
*/ getCodePoints (string) {
if (typeof string !== 'string') {
throw new TypeError('Not a string');
}
let i = 0;
const arr = [];
let codePoint;
while(!!(codePoint = getCodePointData(string, i))){
arr.push(codePoint.code);
i += codePoint.long ? 2 : 1;
}
return arr;
},
/**
* Gets an array of all grapheme clusters (visible symbols) in a string. This is needed because
* some symbols (such as emoji or accented characters) are actually made up of multiple
* character codes. See {@link https://mathiasbynens.be/notes/javascript-unicode here} for more
* info.
*
* @param {string} string - The string to break into symbols.
* @returns {string[]} The symbols in the string.
*/ getSymbols (string) {
if (typeof string !== 'string') {
throw new TypeError('Not a string');
}
let index = 0;
const length = string.length;
const output = [];
let take = 0;
let ch;
while(index < length){
take += numCharsToTakeForNextSymbol(string, index + take);
ch = string[index + take];
// Handle special cases
if (isCodeBetween(ch, DIACRITICAL_MARKS_BEGIN, DIACRITICAL_MARKS_END)) {
ch = string[index + take++];
}
if (isCodeBetween(ch, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
ch = string[index + take++];
}
if (ch && ch.charCodeAt(0) === ZERO_WIDTH_JOINER) {
ch = string[index + take++];
continue;
}
const char = string.substring(index, index + take);
output.push(char);
index += take;
take = 0;
}
return output;
},
/**
* Get the string for a given code point or set of code points. Polyfill for
* [`fromCodePoint`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint}.
*
* @param {...number} args - The code points to convert to a string.
* @returns {string} The converted string.
* @ignore
*/ fromCodePoint (...args) {
return args.map((codePoint)=>{
if (codePoint > 0xFFFF) {
codePoint -= 0x10000;
return String.fromCharCode((codePoint >> 10) + 0xD800, codePoint % 0x400 + 0xDC00);
}
return String.fromCharCode(codePoint);
}).join('');
}
};
/**
* Log tracing functionality, allowing for tracing of the internal functionality of the engine.
* Note that the trace logging only takes place in the debug build of the engine and is stripped
* out in other builds.
*
* @category Debug
*/ class Tracing {
static{
/**
* Set storing the names of enabled trace channels.
*
* @type {Set<string>}
* @private
*/ this._traceChannels = new Set();
}
static{
/**
* Enable call stack logging for trace calls. Defaults to false.
*
* @type {boolean}
*/ this.stack = false;
}
/**
* Enable or disable a trace channel.
*
* @param {string} channel - Name of the trace channel. Can be:
*
* - {@link TRACEID_RENDER_FRAME}
* - {@link TRACEID_RENDER_FRAME_TIME}
* - {@link TRACEID_RENDER_PASS}
* - {@link TRACEID_RENDER_PASS_DETAIL}
* - {@link TRACEID_RENDER_ACTION}
* - {@link TRACEID_RENDER_TARGET_ALLOC}
* - {@link TRACEID_TEXTURE_ALLOC}
* - {@link TRACEID_SHADER_ALLOC}
* - {@link TRACEID_SHADER_COMPILE}
* - {@link TRACEID_VRAM_TEXTURE}
* - {@link TRACEID_VRAM_VB}
* - {@link TRACEID_VRAM_IB}
* - {@link TRACEID_RENDERPIPELINE_ALLOC}
* - {@link TRACEID_COMPUTEPIPELINE_ALLOC}
* - {@link TRACEID_PIPELINELAYOUT_ALLOC}
* - {@link TRACEID_TEXTURES}
* - {@link TRACEID_ASSETS}
* - {@link TRACEID_GPU_TIMINGS}
*
* @param {boolean} enabled - New enabled state for the channel.
*/ static set(channel, enabled = true) {
if (enabled) {
Tracing._traceChannels.add(channel);
} else {
Tracing._traceChannels.delete(channel);
}
}
/**
* Test if the trace channel is enabled.
*
* @param {string} channel - Name of the trace channel.
* @returns {boolean} - True if the trace channel is enabled.
*/ static get(channel) {
return Tracing._traceChannels.has(channel);
}
}
/**
* Engine debug log system. Note that the logging only executes in the debug build of the engine,
* and is stripped out in other builds.
*/ class Debug {
static{
/**
* Set storing already logged messages, to only print each unique message one time.
*
* @type {Set<string>}
* @private
*/ this._loggedMessages = new Set();
}
/**
* Deprecated warning message.
*
* @param {string} message - The message to log.
*/ static deprecated(message) {
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.warn(`DEPRECATED: ${message}`);
}
}
/**
* Removed warning message.
*
* @param {string} message - The message to log.
*/ static removed(message) {
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.error(`REMOVED: ${message}`);
}
}
/**
* Assertion deprecated message. If the assertion is false, the deprecated message is written to the log.
*
* @param {boolean|object} assertion - The assertion to check.
* @param {string} message - The message to log.
*/ static assertDeprecated(assertion, message) {
if (!assertion) {
Debug.deprecated(message);
}
}
/**
* Assertion error message. If the assertion is false, the error message is written to the log.
*
* @param {boolean|object} assertion - The assertion to check.
* @param {...*} args - The values to be written to the log.
*/ static assert(assertion, ...args) {
if (!assertion) {
console.error('ASSERT FAILED: ', ...args);
}
}
/**
* Assertion error message that writes an error message to the log if the object has already
* been destroyed. To be used along setDestroyed.
*
* @param {object} object - The object to check.
*/ static assertDestroyed(object) {
if (object?.__alreadyDestroyed) {
const message = `[${object.constructor?.name}] with name [${object.name}] has already been destroyed, and cannot be used.`;
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.error('ASSERT FAILED: ', message, object);
}
}
}
/**
* Executes a function in debug mode only.
*
* @param {Function} func - Function to call.
*/ static call(func) {
func();
}
/**
* Info message.
*
* @param {...*} args - The values to be written to the log.
*/ static log(...args) {
console.log(...args);
}
/**
* Info message logged no more than once.
*
* @param {string} message - The message to log.
* @param {...*} args - The values to be written to the log.
*/ static logOnce(message, ...args) {
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.log(message, ...args);
}
}
/**
* Warning message.
*
* @param {...*} args - The values to be written to the log.
*/ static warn(...args) {
console.warn(...args);
}
/**
* Warning message logged no more than once.
*
* @param {string} message - The message to log.
* @param {...*} args - The values to be written to the log.
*/ static warnOnce(message, ...args) {
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.warn(message, ...args);
}
}
/**
* Error message.
*
* @param {...*} args - The values to be written to the log.
*/ static error(...args) {
console.error(...args);
}
/**
* Error message logged no more than once.
*
* @param {string} message - The message to log.
* @param {...*} args - The values to be written to the log.
*/ static errorOnce(message, ...args) {
if (!Debug._loggedMessages.has(message)) {
Debug._loggedMessages.add(message);
console.error(message, ...args);
}
}
/**
* Trace message, which is logged to the console if the tracing for the channel is enabled
*
* @param {string} channel - The trace channel
* @param {...*} args - The values to be written to the log.
*/ static trace(channel, ...args) {
if (Tracing.get(channel)) {
console.groupCollapsed(`${channel.padEnd(20, ' ')}|`, ...args);
if (Tracing.stack) {
console.trace();
}
console.groupEnd();
}
}
}
/**
* A helper debug functionality.
*/ class DebugHelper {
/**
* Set a name to the name property of the object. Executes only in the debug build.
*
* @param {object} object - The object to assign the name to.
* @param {string} name - The name to assign.
*/ static setName(object, name) {
if (object) {
object.name = name;
}
}
/**
* Set a label to the label property of the object. Executes only in the debug build.
*
* @param {object} object - The object to assign the name to.
* @param {string} label - The label to assign.
*/ static setLabel(object, label) {
if (object) {
object.label = label;
}
}
/**
* Marks object as destroyed. Executes only in the debug build. To be used along assertDestroyed.
*
* @param {object} object - The object to mark as destroyed.
*/ static setDestroyed(object) {
if (object) {
object.__alreadyDestroyed = true;
}
}
}
/**
* @import { EventHandler } from './event-handler.js'
* @import { HandleEventCallback } from './event-handler.js'
*/ /**
* Event Handle that is created by {@link EventHandler} and can be used for easier event removal
* and management.
*
* @example
* const evt = obj.on('test', (a, b) => {
* console.log(a + b);
* });
* obj.fire('test');
*
* evt.off(); // easy way to remove this event
* obj.fire('test'); // this will not trigger an event
* @example
* // store an array of event handles
* let events = [];
*
* events.push(objA.on('testA', () => {}));
* events.push(objB.on('testB', () => {}));
*
* // when needed, remove all events
* events.forEach((evt) => {
* evt.off();
* });
* events = [];
*/ class EventHandle {
/**
* @param {EventHandler} handler - source object of the event.
* @param {string} name - Name of the event.
* @param {HandleEventCallback} callback - Function that is called when event is fired.
* @param {object} scope - Object that is used as `this` when event is fired.
* @param {boolean} [once] - If this is a single event and will be removed after event is fired.
*/ constructor(handler, name, callback, scope, once = false){
/**
* True if event has been removed.
* @type {boolean}
* @private
*/ this._removed = false;
this.handler = handler;
this.name = name;
this.callback = callback;
this.scope = scope;
this._once = once;
}
/**
* Remove this event from its handler.
*/ off() {
if (this._removed) return;
this.handler.offByHandle(this);
}
on(name, callback, scope = this) {
Debug.deprecated('Using chaining with EventHandler.on is deprecated, subscribe to an event from EventHandler directly instead.');
return this.handler._addCallback(name, callback, scope, false);
}
once(name, callback, scope = this) {
Debug.deprecated('Using chaining with EventHandler.once is deprecated, subscribe to an event from EventHandler directly instead.');
return this.handler._addCallback(name, callback, scope, true);
}
/**
* Mark if event has been removed.
*
* @type {boolean}
* @ignore
*/ set removed(value) {
if (!value) return;
this._removed = true;
}
/**
* True if event has been removed.
*
* @type {boolean}
* @ignore
*/ get removed() {
return this._removed;
}
// don't stringify EventHandle to JSON by JSON.stringify
toJSON(key) {
return undefined;
}
}
/**
* @callback HandleEventCallback
* Callback used by {@link EventHandler} functions. Note the callback is limited to 8 arguments.
* @param {any} [arg1] - First argument that is passed from caller.
* @param {any} [arg2] - Second argument that is passed from caller.
* @param {any} [arg3] - Third argument that is passed from caller.
* @param {any} [arg4] - Fourth argument that is passed from caller.
* @param {any} [arg5] - Fifth argument that is passed from caller.
* @param {any} [arg6] - Sixth argument that is passed from caller.
* @param {any} [arg7] - Seventh argument that is passed from caller.
* @param {any} [arg8] - Eighth argument that is passed from caller.
* @returns {void}
*/ /**
* Abstract base class that implements functionality for event handling.
*
* ```javascript
* const obj = new EventHandlerSubclass();
*
* // subscribe to an event
* obj.on('hello', (str) => {
* console.log('event hello is fired', str);
* });
*
* // fire event
* obj.fire('hello', 'world');
* ```
*/ class EventHandler {
/**
* Reinitialize the event handler.
* @ignore
*/ initEventHandler() {
this._callbacks = new Map();
this._callbackActive = new Map();
}
/**
* Registers a new event handler.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} scope - Object to use as 'this' when the event is fired, defaults to
* current this.
* @param {boolean} once - If true, the callback will be unbound after being fired once.
* @returns {EventHandle} Created {@link EventHandle}.
* @ignore
*/ _addCallback(name, callback, scope, once) {
if (!name || typeof name !== 'string' || !callback) {
console.warn(`EventHandler: subscribing to an event (${name}) with missing arguments`, callback);
}
if (!this._callbacks.has(name)) {
this._callbacks.set(name, []);
}
// if we are adding a callback to the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name)) {
const callbackActive = this._callbackActive.get(name);
if (callbackActive && callbackActive === this._callbacks.get(name)) {
this._callbackActive.set(name, callbackActive.slice());
}
}
const evt = new EventHandle(this, name, callback, scope, once);
this._callbacks.get(name).push(evt);
return evt;
}
/**
* Attach an event handler to an event.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} [scope] - Object to use as 'this' when the event is fired, defaults to
* current this.
* @returns {EventHandle} Can be used for removing event in the future.
* @example
* obj.on('test', (a, b) => {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
* @example
* const evt = obj.on('test', (a, b) => {
* console.log(a + b);
* });
* // some time later
* evt.off();
*/ on(name, callback, scope = this) {
return this._addCallback(name, callback, scope, false);
}
/**
* Attach an event handler to an event. This handler will be removed after being fired once.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} [scope] - Object to use as 'this' when the event is fired, defaults to
* current this.
* @returns {EventHandle} - can be used for removing event in the future.
* @example
* obj.once('test', (a, b) => {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
* obj.fire('test', 1, 2); // not going to get handled
*/ once(name, callback, scope = this) {
return this._addCallback(name, callback, scope, true);
}
/**
* Detach an event handler from an event. If callback is not provided then all callbacks are
* unbound from the event, if scope is not provided then all events with the callback will be
* unbound.
*
* @param {string} [name] - Name of the event to unbind.
* @param {HandleEventCallback} [callback] - Function to be unbound.
* @param {object} [scope] - Scope that was used as the this when the event is fired.
* @returns {EventHandler} Self for chaining.
* @example
* const handler = () => {};
* obj.on('test', handler);
*
* obj.off(); // Removes all events
* obj.off('test'); // Removes all events called 'test'
* obj.off('test', handler); // Removes all handler functions, called 'test'
* obj.off('test', handler, this); // Removes all handler functions, called 'test' with scope this
*/ off(name, callback, scope) {
if (name) {
// if we are removing a callback from the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
} else {
// if we are removing a callback from any list that is executing right now
// ensure we preserve these initial lists before modifications
for (const [key, callbacks] of this._callbackActive){
if (!this._callbacks.has(key)) {
continue;
}
if (this._callbacks.get(key) !== callbacks) {
continue;
}
this._callbackActive.set(key, callbacks.slice());
}
}
if (!name) {
// remove all events
for (const callbacks of this._callbacks.values()){
for(let i = 0; i < callbacks.length; i++){
callbacks[i].removed = true;
}
}
this._callbacks.clear();
} else if (!callback) {
// remove all events of a specific name
const callbacks = this._callbacks.get(name);
if (callbacks) {
for(let i = 0; i < callbacks.length; i++){
callbacks[i].removed = true;
}
this._callbacks.delete(name);
}
} else {
const callbacks = this._callbacks.get(name);
if (!callbacks) {
return this;
}
for(let i = 0; i < callbacks.length; i++){
// remove all events with a specific name and a callback
if (callbacks[i].callback !== callback) {
continue;
}
// could be a specific scope as well
if (scope && callbacks[i].scope !== scope) {
continue;
}
callbacks[i].removed = true;
callbacks.splice(i, 1);
i--;
}
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
return this;
}
/**
* Detach an event handler from an event using EventHandle instance. More optimal remove
* as it does not have to scan callbacks array.
*
* @param {EventHandle} handle - Handle of event.
* @ignore
*/ offByHandle(handle) {
const name = handle.name;
handle.removed = true;
// if we are removing a callback from the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
const callbacks = this._callbacks.get(name);
if (!callbacks) {
return this;
}
const ind = callbacks.indexOf(handle);
if (ind !== -1) {
callbacks.splice(ind, 1);
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
return this;
}
/**
* Fire an event, all additional arguments are passed on to the event listener.
*
* @param {string} name - Name of event to fire.
* @param {any} [arg1] - First argument that is passed to the event handler.
* @param {any} [arg2] - Second argument that is passed to the event handler.
* @param {any} [arg3] - Third argument that is passed to the event handler.
* @param {any} [arg4] - Fourth argument that is passed to the event handler.
* @param {any} [arg5] - Fifth argument that is passed to the event handler.
* @param {any} [arg6] - Sixth argument that is passed to the event handler.
* @param {any} [arg7] - Seventh argument that is passed to the event handler.
* @param {any} [arg8] - Eighth argument that is passed to the event handler.
* @returns {EventHandler} Self for chaining.
* @example
* obj.fire('test', 'This is the message');
*/ fire(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
if (!name) {
return this;
}
const callbacksInitial = this._callbacks.get(name);
if (!callbacksInitial) {
return this;
}
let callbacks;
if (!this._callbackActive.has(name)) {
// when starting callbacks execution ensure we store a list of initial callbacks
this._callbackActive.set(name, callbacksInitial);
} else if (this._callbackActive.get(name) !== callbacksInitial) {
// if we are trying to execute a callback while there is an active execution right now
// and the active list has been already modified,
// then we go to an unoptimized path and clone callbacks list to ensure execution consistency
callbacks = callbacksInitial.slice();
}
// eslint-disable-next-line no-unmodified-loop-condition
for(let i = 0; (callbacks || this._callbackActive.get(name)) && i < (callbacks || this._callbackActive.get(name)).length; i++){
const evt = (callbacks || this._callbackActive.get(name))[i];
if (!evt.callback) continue;
evt.callback.call(evt.scope, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
if (evt._once) {
// check that callback still exists because user may have unsubscribed in the event handler
const existingCallback = this._callbacks.get(name);
const ind = existingCallback ? existingCallback.indexOf(evt) : -1;
if (ind !== -1) {
if (this._callbackActive.get(name) === existingCallback) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
const callbacks = this._callbacks.get(name);
if (!callbacks) continue;
callbacks[ind].removed = true;
callbacks.splice(ind, 1);
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
}
}
if (!callbacks) {
this._callbackActive.delete(name);
}
return this;
}
/**
* Test if there are any handlers bound to an event name.
*
* @param {string} name - The name of the event to test.
* @returns {boolean} True if the object has handlers bound to the specified event name.
* @example
* obj.on('test', () => {}); // bind an event to 'test'
* obj.hasEvent('test'); // returns true
* obj.hasEvent('hello'); // returns false
*/ hasEvent(name) {
return !!this._callbacks.get(name)?.length;
}
constructor(){
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/ this._callbacks = new Map();
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/ this._callbackActive = new Map();
}
}
/**
* A ordered list-type data structure that can provide item look up by key and can also return a list.
*
* @ignore
*/ class IndexedList {
/**
* Add a new item into the list with a index key.
*
* @param {string} key - Key used to look up item in index.
* @param {object} item - Item to be stored.
*/ push(key, item) {
if (this._index[key]) {
throw Error(`Key already in index ${key}`);
}
const location = this._list.push(item) - 1;
this._index[key] = location;
}
/**
* Test whether a key has been added to the index.
*
* @param {string} key - The key to test.
* @returns {boolean} Returns true if key is in the index, false if not.
*/ has(key) {
return this._index[key] !== undefined;
}
/**
* Return the item indexed by a key.
*
* @param {string} key - The key of the item to retrieve.
* @returns {object|null} The item stored at key. Returns null if key is not in the index.
*/ get(key) {
const location = this._index[key];
if (location !== undefined) {
return this._list[location];
}
return null;
}
/**
* Remove the item indexed by key from the list.
*
* @param {string} key - The key at which to remove the item.
* @returns {boolean} Returns true if the key exists and an item was removed, returns false if
* no item was removed.
*/ remove(key) {
const location = this._index[key];
if (location !== undefined) {
this._list.splice(location, 1);
delete this._index[key];
// update index
for(key in this._index){
const idx = this._index[key];
if (idx > location) {
this._index[key] = idx - 1;
}
}
return true;
}
return false;
}
/**
* Returns the list of items.
*
* @returns {object[]} The list of items.
*/ list() {
return this._list;
}
/**
* Remove all items from the list.
*/ clear() {
this._list.length = 0;
for(const prop in this._index){
delete this._index[prop];
}
}
constructor(){
/**
* @type {object[]}
* @private
*/ this._list = [];
/**
* @type {Object<string, number>}
* @private
*/ this._index = {};
}
}
// wrapper function that caches the func result on first invocation and
// then subsequently returns the cached value
const cachedResult = (func)=>{
const uninitToken = {};
let result = uninitToken;
return ()=>{
if (result === uninitToken) {
result = func();
}
return result;
};
};
class Impl {
static{
this.modules = {};
}
static{
// returns true if the running host supports wasm modules (all browsers except IE)
this.wasmSupported = cachedResult(()=>{
try {
if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
if (module instanceof WebAssembly.Module) {
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
}
} catch (e) {}
ret