playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,646 lines (1,632 loc) • 19.2 MB
JavaScript
/**
* @license
* PlayCanvas Engine v2.19.1 revision 9069deb (DEBUG)
* Copyright 2011-2026 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.
*/
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __require = /* @__PURE__ */ ((x2) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x2, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x2)(function(x2) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x2 + '" is not supported');
});
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/core/constants.js
var TRACEID_RENDER_FRAME = "RenderFrame";
var TRACEID_RENDER_FRAME_TIME = "RenderFrameTime";
var TRACEID_RENDER_PASS = "RenderPass";
var TRACEID_RENDER_PASS_DETAIL = "RenderPassDetail";
var TRACEID_RENDER_ACTION = "RenderAction";
var TRACEID_RENDER_TARGET_ALLOC = "RenderTargetAlloc";
var TRACEID_TEXTURE_ALLOC = "TextureAlloc";
var TRACEID_SHADER_ALLOC = "ShaderAlloc";
var TRACEID_SHADER_COMPILE = "ShaderCompile";
var TRACEID_VRAM_TEXTURE = "VRAM.Texture";
var TRACEID_VRAM_VB = "VRAM.Vb";
var TRACEID_VRAM_IB = "VRAM.Ib";
var TRACEID_VRAM_SB = "VRAM.Sb";
var TRACEID_BINDGROUP_ALLOC = "BindGroupAlloc";
var TRACEID_BINDGROUPFORMAT_ALLOC = "BindGroupFormatAlloc";
var TRACEID_RENDERPIPELINE_ALLOC = "RenderPipelineAlloc";
var TRACEID_COMPUTEPIPELINE_ALLOC = "ComputePipelineAlloc";
var TRACEID_PIPELINELAYOUT_ALLOC = "PipelineLayoutAlloc";
var TRACEID_ELEMENT = "Element";
var TRACEID_TEXTURES = "Textures";
var TRACEID_BUFFERS = "Buffers";
var TRACEID_ASSETS = "Assets";
var TRACEID_RENDER_QUEUE = "RenderQueue";
var TRACEID_OCTREE_RESOURCES = "OctreeResources";
var TRACEID_GPU_TIMINGS = "GpuTimings";
// src/core/core.js
var version = "2.19.1";
var revision = "9069deb";
function extend(target2, ex) {
for (const prop in ex) {
const copy = ex[prop];
if (Array.isArray(copy)) {
target2[prop] = extend([], copy);
} else if (copy && typeof copy === "object") {
target2[prop] = extend({}, copy);
} else {
target2[prop] = copy;
}
}
return target2;
}
// src/core/guid.js
var guid = {
/**
* Create an RFC4122 version 4 compliant GUID.
*
* @returns {string} A new GUID.
*/
create() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c2) => {
const r = Math.random() * 16 | 0;
const v6 = c2 === "x" ? r : r & 3 | 8;
return v6.toString(16);
});
}
};
// src/core/path.js
var 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;
}
};
// src/core/platform.js
var 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;
};
var ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
var environment = typeof window !== "undefined" ? "browser" : typeof global !== "undefined" ? "node" : "worker";
var 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;
var browserName = environment !== "browser" ? null : /Chrome\/|Chromium\/|Edg.*\//.test(ua) ? "chrome" : (
// chrome, chromium, edge
/Safari\//.test(ua) ? "safari" : (
// safari, ios chrome/firefox
/Firefox\//.test(ua) ? "firefox" : "other"
)
);
var xbox = /xbox/i.test(ua);
var visionos = /Macintosh/i.test(ua) && typeof navigator !== "undefined" && navigator.maxTouchPoints > 0 && !/iPhone|iPad|iPod/i.test(ua);
var touch = environment === "browser" && ("ontouchstart" in window || "maxTouchPoints" in navigator && navigator.maxTouchPoints > 0);
var gamepads = environment === "browser" && (!!navigator.getGamepads || !!navigator.webkitGetGamepads);
var workers = typeof Worker !== "undefined";
var passiveEvents = detectPassiveEvents();
var 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,
/**
* 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 Apple Vision Pro (visionOS).
*
* @type {boolean}
* @ignore
*/
visionos,
/**
* True if running on an Xbox device.
*
* @type {boolean}
*/
xbox,
/**
* True if the platform supports gamepads.
*
* @type {boolean}
*/
gamepads,
/**
* True if the platform supports touch input.
*
* @type {boolean}
*/
touch,
/**
* True if the platform supports Web Workers.
*
* @type {boolean}
*/
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,
/**
* Get the browser name.
*
* @type {'chrome' | 'safari' | 'firefox' | 'other' | null}
* @ignore
*/
browserName
};
// src/core/string.js
var ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
var ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE;
var HIGH_SURROGATE_BEGIN = 55296;
var HIGH_SURROGATE_END = 56319;
var LOW_SURROGATE_BEGIN = 56320;
var LOW_SURROGATE_END = 57343;
var ZERO_WIDTH_JOINER = 8205;
var REGIONAL_INDICATOR_BEGIN = 127462;
var REGIONAL_INDICATOR_END = 127487;
var FITZPATRICK_MODIFIER_BEGIN = 127995;
var FITZPATRICK_MODIFIER_END = 127999;
var DIACRITICAL_MARKS_BEGIN = 8400;
var DIACRITICAL_MARKS_END = 8447;
var VARIATION_MODIFIER_BEGIN = 65024;
var VARIATION_MODIFIER_END = 65039;
function getCodePointData(string2, i = 0) {
const size = string2.length;
if (i < 0 || i >= size) {
return null;
}
const first = string2.charCodeAt(i);
if (size > 1 && first >= HIGH_SURROGATE_BEGIN && first <= HIGH_SURROGATE_END) {
const second = string2.charCodeAt(i + 1);
if (second >= LOW_SURROGATE_BEGIN && second <= LOW_SURROGATE_END) {
return {
code: (first - HIGH_SURROGATE_BEGIN) * 1024 + second - LOW_SURROGATE_BEGIN + 65536,
long: true
};
}
}
return {
code: first,
long: false
};
}
function isCodeBetween(string2, begin, end) {
if (!string2) {
return false;
}
const codeData = getCodePointData(string2);
if (codeData) {
const code = codeData.code;
return code >= begin && code <= end;
}
return false;
}
function numCharsToTakeForNextSymbol(string2, index) {
if (index === string2.length - 1) {
return 1;
}
if (isCodeBetween(string2[index], HIGH_SURROGATE_BEGIN, HIGH_SURROGATE_END)) {
const first = string2.substring(index, index + 2);
const second = string2.substring(index + 2, index + 4);
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;
}
if (isCodeBetween(second, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
return 3;
}
return 2;
}
if (isCodeBetween(string2[index + 1], VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
return 2;
}
return 1;
}
var string = {
/**
* All lowercase letters.
*
* @type {string}
*/
ASCII_LOWERCASE,
/**
* All uppercase letters.
*
* @type {string}
*/
ASCII_UPPERCASE,
/**
* All ASCII letters.
*
* @type {string}
*/
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(s2, ...args) {
for (let i = 0; i < args.length; i++) {
s2 = s2.replace(`{${i}}`, args[i]);
}
return s2;
},
/**
* 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(string2, i) {
const codePointData = getCodePointData(string2, 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(string2) {
if (typeof string2 !== "string") {
throw new TypeError("Not a string");
}
let i = 0;
const arr = [];
let codePoint;
while (!!(codePoint = getCodePointData(string2, 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(string2) {
if (typeof string2 !== "string") {
throw new TypeError("Not a string");
}
let index = 0;
const length = string2.length;
const output = [];
let take = 0;
let ch;
while (index < length) {
take += numCharsToTakeForNextSymbol(string2, index + take);
ch = string2[index + take];
if (isCodeBetween(ch, DIACRITICAL_MARKS_BEGIN, DIACRITICAL_MARKS_END)) {
ch = string2[index + take++];
}
if (isCodeBetween(ch, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) {
ch = string2[index + take++];
}
if (ch && ch.charCodeAt(0) === ZERO_WIDTH_JOINER) {
ch = string2[index + take++];
continue;
}
const char = string2.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 > 65535) {
codePoint -= 65536;
return String.fromCharCode(
(codePoint >> 10) + 55296,
codePoint % 1024 + 56320
);
}
return String.fromCharCode(codePoint);
}).join("");
}
};
// src/core/tracing.js
var _Tracing = class _Tracing {
/**
* 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_BUFFERS}
* - {@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);
}
};
/**
* Set storing the names of enabled trace channels.
*
* @type {Set<string>}
* @private
*/
__publicField(_Tracing, "_traceChannels", /* @__PURE__ */ new Set());
/**
* Enable call stack logging for trace calls. Defaults to false.
*
* @type {boolean}
*/
__publicField(_Tracing, "stack", false);
var Tracing = _Tracing;
// src/core/debug.js
var _Debug = class _Debug {
/**
* 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();
}
}
};
/**
* Set storing already logged messages, to only print each unique message one time.
*
* @type {Set<string>}
* @private
*/
__publicField(_Debug, "_loggedMessages", /* @__PURE__ */ new Set());
var Debug = _Debug;
var DebugHelper = class {
/**
* 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;
}
}
};
// src/core/event-handle.js
var EventHandle = class {
/**
* @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) {
/**
* @type {EventHandler}
* @private
*/
__publicField(this, "handler");
/**
* @type {string}
* @ignore
*/
__publicField(this, "name");
/**
* @type {HandleEventCallback}
* @ignore
*/
__publicField(this, "callback");
/**
* @type {object}
* @ignore
*/
__publicField(this, "scope");
/**
* @type {boolean}
* @ignore
*/
__publicField(this, "_once");
/**
* True if event has been removed.
*
* @private
*/
__publicField(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 void 0;
}
};
// src/core/event-handler.js
var EventHandler = class {
constructor() {
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/
__publicField(this, "_callbacks", /* @__PURE__ */ new Map());
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/
__publicField(this, "_callbackActive", /* @__PURE__ */ new Map());
}
/**
* Reinitialize the event handler.
* @ignore
*/
initEventHandler() {
this._callbacks = /* @__PURE__ */ new Map();
this._callbackActive = /* @__PURE__ */ 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 (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 (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
} else {
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) {
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) {
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++) {
if (callbacks[i].callback !== callback) {
continue;
}
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 (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)) {
this._callbackActive.set(name, callbacksInitial);
} else if (this._callbackActive.get(name) !== callbacksInitial) {
callbacks = callbacksInitial.slice();
}
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) {
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 callbacks2 = this._callbacks.get(name);
if (!callbacks2) continue;
callbacks2[ind].removed = true;
callbacks2.splice(ind, 1);
if (callbacks2.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;
}
};
// src/core/indexed-list.js
var IndexedList = class {
constructor() {
/**
* @type {object[]}
* @private
*/
__publicField(this, "_list", []);
/**
* @type {Object<string, number>}
* @private
*/
__publicField(this, "_index", {});
}
/**
* Add a new item into the list with an 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] !== void 0;
}
/**
* 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 !== void 0) {
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 !== void 0) {
this._list.splice(location, 1);
delete this._index[key];
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];
}
}
};
// src/core/wasm-module.js
var cachedResult = (func) => {
const uninitToken = {};
let result = uninitToken;
return () => {
if (result === uninitToken) {
result = func();
}
return result;
};
};
var _Impl = class _Impl {
// load a script
static loadScript(url, callback) {
const s2 = document.createElement("script");
s2.setAttribute("src", url);
s2.onload = () => {
callback(null);
};
s2.onerror = () => {
callback(`Failed to load script='${url}'`);
};
document.body.appendChild(s2);
}
// load a wasm module
static loadWasm(moduleName, config, callback) {
const loadUrl = _Impl.wasmSupported() && config.glueUrl && config.wasmUrl ? config.glueUrl : config.fallbackUrl;
if (loadUrl) {
_Impl.loadScript(loadUrl, (err2) => {
if (err2) {
callback(err2, null);
} else {
const module = window[moduleName];
window[moduleName] = void 0;
module({
locateFile: () => config.wasmUrl,
onAbort: () => {
callback("wasm module aborted.");
}
}).then((instance) => {
callback(null, instance);
});
}
});
} else {
callback("No supported wasm modules found.", null);
}
}
// get state object for the named module
static getModule(name) {
if (!_Impl.modules.hasOwnProperty(name)) {
_Impl.modules[name] = {
config: null,
initializing: false,
instance: null,
callbacks: []
};
}
return _Impl.modules[name];
}
static initialize(moduleName, module) {
if (module.initializing) {
return;
}
const config = module.config;
if (config.glueUrl || config.wasmUrl || config.fallbackUrl) {
module.initializing = true;
_Impl.loadWasm(moduleName, config, (err2, instance) => {
if (err2) {
if (config.errorHandler) {
config.errorHandler(err2);
} else {
console.error(`failed to initialize module=${moduleName} error=${err2}`);
}
} else {
module.instance = instance;
module.callbacks.forEach((callback) => {
callback(instance);
});
}
});
}
}
};
__publicField(_Impl, "modules", {});
// returns true if the running host supports wasm modules (all browsers except IE)
__publicField(_Impl, "wasmSupported", cachedResult(() => {
try {
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
const module = new WebAssembly.Module(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0));
if (module instanceof WebAssembly.Module) {
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
}
} catch (e) {
}
return false;
}));
var Impl = _Impl;
var WasmModule = class {
/**
* Set a wasm module's configuration.
*
* @param {string} moduleName - Name of the module.
* @param {object} [config] - The configuration object.
* @param {string} [config.glueUrl] - URL of glue script.
* @param {string} [config.wasmUrl] - URL of the wasm script.
* @param {string} [config.fallbackUrl] - URL of the fallback script to use when wasm modules
* aren't supported.
* @param {number} [config.numWorkers] - For modules running on worker threads, the number of
* threads to use. Default value is based on module implementation.
* @param {ModuleErrorCallback} [config.errorHandler] - Function to be called if the module fails
* to download.
*/
static setConfig(moduleName, config) {
const module = Impl.getModule(moduleName);
module.config = config;
if (module.callbacks.length > 0) {
Impl.initialize(moduleName, module);
}
}
/**
* Get a wasm module's configuration.
*
* @param {string} moduleName - Name of the module.
* @returns {object | undefined} The previously set configuration.
*/
static getConfig(moduleName) {
return Impl.modules?.[moduleName]?.config;
}
/**
* Get a wasm module instance. The instance will be created if necessary and returned
* in the second parameter to callback.
*
* @param {string} moduleName - Name of the module.
* @param {ModuleInstanceCallback} callback - The function called when the instance is
* available.
*/
static getInstance(moduleName, callback) {
const module = Impl.getModule(moduleName);
if (module.instance) {
callback(module.instance);
} else {
module.callbacks.push(callback);
if (module.config) {
Impl.initialize(moduleName, module);
}
}
}
};
// src/core/read-stream.js
var ReadStream = class {
/**
* @param {ArrayBuffer} arraybuffer - The buffer to read from.
*/
constructor(arraybuffer) {
/** @type {ArrayBuffer} */
__publicField(this, "arraybuffer");
/** @type {DataView} */
__publicField(this, "dataView");
/** @type {number} */
__publicField(this, "offset", 0);
this.arraybuffer = arraybuffer;
this.dataView = new DataView(arraybuffer);
}
/**
* The number of bytes remaining to be read.
*
* @type {number}
*/
get remainingBytes() {
return this.dataView.byteLength - this.offset;
}
/**
* Resets the offset to a given value. If no value is given, the offset is reset to 0.
*
* @param {number} offset - The new offset.
*/
reset(offset3 = 0) {
this.offset = offset3;
}
/**
* Skips a number of bytes.
*
* @param {number} bytes - The number of bytes to skip.
*/
skip(bytes) {
this.offset += bytes;
}
/**
* Aligns the offset to a multiple of a number of bytes.
*
* @param {number} bytes - The number of bytes to align to.
*/
align(bytes) {
this.offset = this.offset + bytes - 1 & ~(bytes - 1);
}
/**
* Increments the offset by the specified number of bytes and returns the previous offset.
*
* @param {number} amount - The number of bytes to increment by.
* @returns {number} The previous offset.
* @private
*/
_inc(amount) {
this.offset += amount;
return this.offset - amount;
}
/**
* Reads a single character.
*
* @returns {string} The character.
*/
readChar() {
return String.fromCharCode(this.dataView.getUint8(this.offset++));
}
/**
* Reads a string of a given length.
*
* @param {number} numChars - The number of characters to read.
* @returns {string} The string.
*/
readChars(numChars) {
let result = "";
for (let i = 0; i < numChars; ++i) {
result += this.readChar();
}
return result;
}
/**
* Read an unsigned 8-bit integer.
*
* @returns {number} The integer.
*/
readU8() {
return this.dataView.getUint8(this.offset++);
}
/**
* Read an unsigned 16-bit integer.
*
* @returns {number} The integer.
*/
readU16() {
return this.dataView.getUint16(this._inc(2), true);
}
/**
* Read an unsigned 32-bit integer.
*
* @returns {number} The integer.
*/
readU32() {
return this.dataView.getUint32(this._inc(4), true);
}
/**
* Read an unsigned 64-bit integer.
*
* @returns {number} The integer.
*/
readU64() {
return this.readU32() + 2 ** 32 * this.readU32();
}
/**
* Read a big endian unsigned 32-bit integer.
*
* @returns {number} The integer.
*/
readU32be() {
return this.dataView.getUint32(this._inc(4), false);
}
/**
* Read unsigned 8-bit integers into an array.
*
* @param {number[]} result - The array to read into.
*/
readArray(result) {
for (let i = 0; i < result.length; ++i) {
result[i] = this.readU8();
}
}
/**
* Read a line of text from the stream.
*
* @returns {string} The line of text.
*/
readLine() {
const view = this.dataView;
let result = "";
while (true) {
if (this.offset >= view.byteLength) {
break;
}
const c2 = String.fromCharCode(this.readU8());
if (c2 === "\n") {
break;
}
result += c2;
}
return result;
}
};
// src/core/sorted-loop-array.js
var SortedLoopArray = class {
/**
* Create a new SortedLoopArray instance.
*
* @param {object} args - Arguments.
* @param {string} args.sortBy - The name of the field that each element in the array is going
* to be sorted by.
* @example
* const array = new pc.SortedLoopArray({ sortBy: 'priority' });
* array.insert(item); // adds item to the right slot based on item.priority
* array.append(item); // adds item to the end of the array
* array.remove(item); // removes item from array
* for (array.loopIndex = 0; array.loopIndex < array.length; array.loopIndex++) {
* // do things with array elements
* // safe to remove and add elements into the array while looping
* }
*/
constructor(args) {
/**
* The internal array that holds the actual array elements.
*
* @type {object[]}
*/
__publicField(this, "items", []);
/**
* The number of elements in the array.
*/
__publicField(this, "length", 0);
/**
* The current index used to loop through the array. This gets modified if we add or remove
* elements from the array while looping. See the example to see how to loop through this
* array.
*/
__publicField(this, "loopIndex", -1);
/** @private */
__publicField(this, "_sortBy");
/** @private */
__publicField(this, "_sortHandler");
this._sortBy = args.sortBy;
this._sortHandler = this._doSort.bind(this);
}
/**
* Searches for the right spot to insert the specified item.
*
* @param {object} item - The item.
* @returns {number} The index where to insert the item.
* @private
*/
_binarySearch(item) {
let left = 0;
let right2 = this.items.length - 1;
const search = item[this._sortBy];
let middle;
let current;
while (left <= right2) {
middle = Math.floor((left + right2) / 2);
current = this.items[middle][this._sortBy];
if (current <= search) {
left = middle + 1;
} else if (current >