UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

1,437 lines (1,426 loc) 19.7 MB
/** * @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