UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

1,408 lines (1,385 loc) 17.7 MB
/** * @license * PlayCanvas Engine v2.5.0 revision 2abde2e (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. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.pc = {})); })(this, (function (exports) { 'use strict'; var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; /** * A short hand function to polyfill prototype methods which are not iterated in e.g. for-in loops. * * @param {ObjectConstructor} cls * @param {string} name * @param {Function} func * @ignore */ function defineProtoFunc(cls, name, func) { if (!cls.prototype[name]) { Object.defineProperty(cls.prototype, name, { value: func, configurable: true, enumerable: false, writable: true }); } } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill#polyfill defineProtoFunc(Array, 'fill', function(value) { // Steps 1-2. if (this == null) { throw new TypeError('this is null or not defined'); } var O = Object(this); // Steps 3-5. var len = O.length >>> 0; // Steps 6-7. var start = arguments[1]; var relativeStart = start >> 0; // Step 8. var k = relativeStart < 0 ? Math.max(len + relativeStart, 0) : Math.min(relativeStart, len); // Steps 9-10. var end = arguments[2]; var relativeEnd = end === undefined ? len : end >> 0; // Step 11. var finalValue = relativeEnd < 0 ? Math.max(len + relativeEnd, 0) : Math.min(relativeEnd, len); // Step 12. while(k < finalValue){ O[k] = value; k++; } // Step 13. return O; }); // https://tc39.github.io/ecma262/#sec-array.prototype.find defineProtoFunc(Array, 'find', function(predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while(k < len){ // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return kValue. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return kValue; } // e. Increase k by 1. k++; } // 7. Return undefined. return undefined; }); // https://tc39.github.io/ecma262/#sec-array.prototype.findindex defineProtoFunc(Array, 'findIndex', function(predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while(k < len){ // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return k. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return k; } // e. Increase k by 1. k++; } // 7. Return -1. return -1; }); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign#Polyfill if (!Math.sign) { Math.sign = function(x) { // If x is NaN, the result is NaN. // If x is -0, the result is -0. // If x is +0, the result is +0. // If x is negative and not -0, the result is -1. // If x is positive and not +0, the result is +1. return (x > 0) - (x < 0) || +x; // A more aesthetic pseudo-representation: // // ( (x > 0) ? 1 : 0 ) // if x is positive, then positive one // + // else (because you can't be both - and +) // ( (x < 0) ? -1 : 0 ) // if x is negative, then negative one // || // if x is 0, -0, or NaN, or not a number, // +x // then the result will be x, (or) if x is // // not a number, then x converts to number }; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite#polyfill if (Number.isFinite === undefined) Number.isFinite = function(value) { return typeof value === 'number' && isFinite(value); }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill if (typeof Object.assign != 'function') { // Must be writable: true, enumerable: false, configurable: true Object.defineProperty(Object, "assign", { value: function assign(target, varArgs) { 'use strict'; if (target == null) { throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for(var index = 1; index < arguments.length; index++){ var nextSource = arguments[index]; if (nextSource != null) { for(var nextKey in nextSource){ // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, writable: true, configurable: true }); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries // https://stackoverflow.com/questions/68654735/ie11-compatible-object-fromentries Object.fromEntries = Object.fromEntries || function fromEntries(entries) { if (!entries || !entries[Symbol.iterator]) { throw new Error('Object.fromEntries() requires a single iterable argument'); } var res = {}; for(var i = 0; i < entries.length; i++){ res[entries[i][0]] = entries[i][1]; } return res; }; Object.entries = Object.entries || function(obj) { var ownProps = Object.keys(obj), i = ownProps.length, resArray = new Array(i); // preallocate the Array while(i--)resArray[i] = [ ownProps[i], obj[ownProps[i]] ]; return resArray; }; Object.values = Object.values || function(object) { return Object.keys(object).map((key)=>object[key]); }; // Apply PointerLock shims (function() { // Old API if (typeof navigator === 'undefined' || typeof document === 'undefined') { // Not running in a browser return; } navigator.pointer = navigator.pointer || navigator.webkitPointer || navigator.mozPointer; // Events var pointerlockchange = function() { var e = document.createEvent('CustomEvent'); e.initCustomEvent('pointerlockchange', true, false, null); document.dispatchEvent(e); }; var pointerlockerror = function() { var e = document.createEvent('CustomEvent'); e.initCustomEvent('pointerlockerror', true, false, null); document.dispatchEvent(e); }; document.addEventListener('webkitpointerlockchange', pointerlockchange, false); document.addEventListener('webkitpointerlocklost', pointerlockchange, false); document.addEventListener('mozpointerlockchange', pointerlockchange, false); document.addEventListener('mozpointerlocklost', pointerlockchange, false); document.addEventListener('webkitpointerlockerror', pointerlockerror, false); document.addEventListener('mozpointerlockerror', pointerlockerror, false); // requestPointerLock if (Element.prototype.mozRequestPointerLock) { // FF requires a new function for some reason Element.prototype.requestPointerLock = function() { this.mozRequestPointerLock(); }; } else { Element.prototype.requestPointerLock = Element.prototype.requestPointerLock || Element.prototype.webkitRequestPointerLock || Element.prototype.mozRequestPointerLock; } if (!Element.prototype.requestPointerLock && navigator.pointer) { Element.prototype.requestPointerLock = function() { var el = this; document.pointerLockElement = el; navigator.pointer.lock(el, pointerlockchange, pointerlockerror); }; } // exitPointerLock document.exitPointerLock = document.exitPointerLock || document.webkitExitPointerLock || document.mozExitPointerLock; if (!document.exitPointerLock) { document.exitPointerLock = function() { if (navigator.pointer) { document.pointerLockElement = null; navigator.pointer.unlock(); } }; } })(); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill defineProtoFunc(String, 'endsWith', function(search, this_len) { if (this_len === undefined || this_len > this.length) { this_len = this.length; } return this.substring(this_len - search.length, this_len) === search; }); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill defineProtoFunc(String, 'includes', function(search, start) { 'use strict'; if (typeof start !== 'number') { start = 0; } if (start + search.length > this.length) { return false; } else { return this.indexOf(search, start) !== -1; } }); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith#Polyfill defineProtoFunc(String, 'startsWith', function(search, rawPos) { var pos = rawPos > 0 ? rawPos | 0 : 0; return this.substring(pos, pos + search.length) === search; }); // https://vanillajstoolkit.com/polyfills/stringtrimend/ defineProtoFunc(String, 'trimEnd', function() { return this.replace(new RegExp(/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/.source + '$', 'g'), ''); }); const typedArrays = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array ]; for (const typedArray of typedArrays){ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill#polyfill defineProtoFunc(typedArray, "fill", Array.prototype.fill); defineProtoFunc(typedArray, "join", Array.prototype.join); } /** * Logs a frame number. * * @type {string} * @category Debug */ const TRACEID_RENDER_FRAME = 'RenderFrame'; /** * Logs a frame time. * * @type {string} * @category Debug */ const TRACEID_RENDER_FRAME_TIME = 'RenderFrameTime'; /** * Logs basic information about generated render passes. * * @type {string} * @category Debug */ const TRACEID_RENDER_PASS = 'RenderPass'; /** * Logs additional detail for render passes. * * @type {string} * @category Debug */ const TRACEID_RENDER_PASS_DETAIL = 'RenderPassDetail'; /** * Logs render actions created by the layer composition. Only executes when the * layer composition changes. * * @type {string} * @category Debug */ const TRACEID_RENDER_ACTION = 'RenderAction'; /** * Logs the allocation of render targets. * * @type {string} * @category Debug */ const TRACEID_RENDER_TARGET_ALLOC = 'RenderTargetAlloc'; /** * Logs the allocation of textures. * * @type {string} * @category Debug */ const TRACEID_TEXTURE_ALLOC = 'TextureAlloc'; /** * Logs the creation of shaders. * * @type {string} * @category Debug */ const TRACEID_SHADER_ALLOC = 'ShaderAlloc'; /** * Logs the compilation time of shaders. * * @type {string} * @category Debug */ const TRACEID_SHADER_COMPILE = 'ShaderCompile'; /** * Logs the vram use by the textures. * * @type {string} * @category Debug */ const TRACEID_VRAM_TEXTURE = 'VRAM.Texture'; /** * Logs the vram use by the vertex buffers. * * @type {string} * @category Debug */ const TRACEID_VRAM_VB = 'VRAM.Vb'; /** * Logs the vram use by the index buffers. * * @type {string} * @category Debug */ const TRACEID_VRAM_IB = 'VRAM.Ib'; /** * Logs the vram use by the storage buffers. * * @type {string} * @category Debug */ const TRACEID_VRAM_SB = 'VRAM.Sb'; /** * Logs the creation of bind groups. * * @type {string} * @category Debug */ const TRACEID_BINDGROUP_ALLOC = 'BindGroupAlloc'; /** * Logs the creation of bind group formats. * * @type {string} * @category Debug */ const TRACEID_BINDGROUPFORMAT_ALLOC = 'BindGroupFormatAlloc'; /** * Logs the creation of render pipelines. WebBPU only. * * @type {string} * @category Debug */ const TRACEID_RENDERPIPELINE_ALLOC = 'RenderPipelineAlloc'; /** * Logs the creation of compute pipelines. WebGPU only. * * @type {string} * @category Debug */ const TRACEID_COMPUTEPIPELINE_ALLOC = 'ComputePipelineAlloc'; /** * Logs the creation of pipeline layouts. WebBPU only. * * @type {string} * @category Debug */ const TRACEID_PIPELINELAYOUT_ALLOC = 'PipelineLayoutAlloc'; /** * Logs the internal debug information for Elements. * * @type {string} * @category Debug */ const TRACE_ID_ELEMENT = 'Element'; /** * Logs the vram use by all textures in memory. * * @type {string} * @category Debug */ const TRACEID_TEXTURES = 'Textures'; /** * Logs the render queue commands. * * @type {string} * @category Debug */ const TRACEID_RENDER_QUEUE = 'RenderQueue'; /** * Logs the GPU timings. * * @type {string} * @category Debug */ const TRACEID_GPU_TIMINGS = 'GpuTimings'; /** * The engine version number. This is in semantic versioning format (MAJOR.MINOR.PATCH). */ const version = '2.5.0'; /** * 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 = '2abde2e'; /** * 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 () { for(var _len = arguments.length, sections = new Array(_len), _key = 0; _key < _len; _key++){ sections[_key] = arguments[_key]; } 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(); var _ref, _ref1, _ref2; /** * 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: (_ref2 = (_ref1 = (_ref = typeof globalThis !== 'undefined' && globalThis) != null ? _ref : environment === 'browser' && window) != null ? _ref1 : environment === 'node' && global) != null ? _ref2 : 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) { if (i === undefined) 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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. */ fromCodePoint () { const chars = []; let current; let codePoint; let units; for(let i = 0; i < arguments.length; ++i){ current = Number(arguments[i]); codePoint = current - 0x10000; units = current > 0xFFFF ? [ (codePoint >> 10) + 0xD800, codePoint % 0x400 + 0xDC00 ] : [ current ]; chars.push(String.fromCharCode.apply(null, units)); } return chars.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 { /** * 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_GPU_TIMINGS} * * @param {boolean} enabled - New enabled state for the channel. */ static set(channel, enabled) { if (enabled === undefined) 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 */ Tracing._traceChannels = new Set(); /** * Enable call stack logging for trace calls. Defaults to false. * * @type {boolean} */ Tracing.stack = false; /** * 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 { /** * 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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 == null ? undefined : object.__alreadyDestroyed) { var _object_constructor; const message = "[" + ((_object_constructor = object.constructor) == null ? undefined : _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() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ args[_key - 1] = arguments[_key]; } 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 */ Debug._loggedMessages = new Set(); /** * 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 { /** * Remove this event from its handler. */ off() { if (this._removed) return; this.handler.offByHandle(this); } on(name, callback, scope) { if (scope === undefined) 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) { if (scope === undefined) 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; } /** * @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; } } /** * Callback used by {@link EventHandler} functions. Note the callback is limited to 8 arguments. * * @callback HandleEventCallback * @param {*} [arg1] - First argument that is passed from caller. * @param {*} [arg2] - Second argument that is passed from caller. * @param {*} [arg3] - Third argument that is passed from caller. * @param {*} [arg4] - Fourth argument that is passed from caller. * @param {*} [arg5] - Fifth argument that is passed from caller. * @param {*} [arg6] - Sixth argument that is passed from caller. * @param {*} [arg7] - Seventh argument that is passed from caller. * @param {*} [arg8] - Eighth argument that is passed from caller. */ /** * 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._callb