playcanvas
Version:
PlayCanvas WebGL game engine
1,408 lines (1,385 loc) • 17.7 MB
JavaScript
/**
* @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