diff --git a/examples/package-lock.json b/examples/package-lock.json index 08464403dba..d6c71a6d711 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -60,6 +60,7 @@ "@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-terser": "^0.4.4", "@rollup/pluginutils": "^5.1.0", + "@runtime-type-inspector/plugin-rollup": "^4.0.2", "c8": "^10.1.2", "chai": "^5.1.0", "eslint": "^9.10.0", diff --git a/examples/package.json b/examples/package.json index e42752f754d..32f7809455b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -12,6 +12,7 @@ "build:thumbnails": "node ./scripts/build-thumbnails.mjs", "clean": "node ./scripts/clean.mjs", "develop": "cross-env NODE_ENV=development concurrently --kill-others \"npm run watch\" \"npm run serve\"", + "develop:rti": "RTI=on npm run develop", "lint": "eslint .", "serve": "serve dist -l 5555 --no-request-logging --config ../serve.json", "watch": "npm run -s build:pre && cross-env NODE_ENV=development rollup -c -w" diff --git a/examples/rollup.config.js b/examples/rollup.config.mjs similarity index 93% rename from examples/rollup.config.js rename to examples/rollup.config.mjs index c0dae131d7a..0b0af8a85dd 100644 --- a/examples/rollup.config.js +++ b/examples/rollup.config.mjs @@ -13,6 +13,7 @@ import { buildExamples } from './utils/plugins/rollup-build-examples.mjs'; import { copyStatic } from './utils/plugins/rollup-copy-static.mjs'; import { isModuleWithExternalDependencies } from './utils/utils.mjs'; import { treeshakeIgnore } from '../utils/plugins/rollup-treeshake-ignore.mjs'; +import { buildTargetRTI } from '../utils/rollup-build-target-rti.mjs'; import { buildTarget } from '../utils/rollup-build-target.mjs'; // util functions @@ -20,6 +21,7 @@ import { buildTarget } from '../utils/rollup-build-target.mjs'; const NODE_ENV = process.env.NODE_ENV ?? ''; const ENGINE_PATH = !process.env.ENGINE_PATH && NODE_ENV === 'development' ? '../src/index.js' : process.env.ENGINE_PATH ?? ''; +const { RTI = '' } = process.env; const getEnginePathFiles = () => { if (!ENGINE_PATH) { @@ -54,6 +56,9 @@ const getEngineTargets = () => { checkAppEngine(); const targets = []; + if (RTI === 'on') { + targets.push(buildTargetRTI('es', '../src/index.rti.js', 'dist/iframe/ENGINE_PATH')); + } if (ENGINE_PATH) { return targets; } @@ -146,7 +151,7 @@ export default [ skipWrite: true }, treeshake: false, - plugins: [buildExamples(NODE_ENV, ENGINE_PATH), copyStatic(NODE_ENV, STATIC_FILES)] + plugins: [buildExamples(NODE_ENV, ENGINE_PATH, RTI), copyStatic(NODE_ENV, STATIC_FILES)] }, { // A debug build is ~2.3MB and a release build ~0.6MB diff --git a/examples/scripts/build-examples.mjs b/examples/scripts/build-examples.mjs index 2222dec521f..b70be941f75 100644 --- a/examples/scripts/build-examples.mjs +++ b/examples/scripts/build-examples.mjs @@ -40,7 +40,10 @@ const generateExampleFile = (categoryKebab, exampleNameKebab, setEngineType, fil // engine const engineType = process.env.ENGINE_PATH ? 'development' : process.env.NODE_ENV === 'development' ? 'debug' : setEngineType; - const engine = engineFor(engineType); + let engine = engineFor(engineType); + if (process.env.RTI === 'on') { + engine = './ENGINE_PATH/playcanvas.rti.mjs'; + } html = html.replace(/'@ENGINE'/g, JSON.stringify(engine)); if (/'@[A-Z0-9_]+'/.test(html)) { diff --git a/examples/utils/plugins/rollup-build-examples.mjs b/examples/utils/plugins/rollup-build-examples.mjs index d98e1be60d3..3569c6928fd 100644 --- a/examples/utils/plugins/rollup-build-examples.mjs +++ b/examples/utils/plugins/rollup-build-examples.mjs @@ -11,9 +11,10 @@ const REGULAR_OUT = '\x1b[22m'; * * @param {string} nodeEnv - The node environment. * @param {string} enginePath - The path to the engine. + * @param {string} rti - Whether to activate RuntimeTypeInspector. * @returns {import('rollup').Plugin} The plugin. */ -export function buildExamples(nodeEnv, enginePath) { +export function buildExamples(nodeEnv, enginePath, rti) { return { name: 'build-examples', buildStart() { @@ -24,8 +25,8 @@ export function buildExamples(nodeEnv, enginePath) { } }, buildEnd() { - build({ NODE_ENV: nodeEnv, ENGINE_PATH: enginePath }); - console.log(`${GREEN_OUT}built examples using NODE_ENV=${BOLD_OUT}${nodeEnv}${REGULAR_OUT} ENGINE_PATH=${BOLD_OUT}${enginePath}${REGULAR_OUT}`); + build({ NODE_ENV: nodeEnv, ENGINE_PATH: enginePath, RTI: rti }); + console.log(`${GREEN_OUT}built examples using NODE_ENV=${BOLD_OUT}${nodeEnv}${REGULAR_OUT} ENGINE_PATH=${BOLD_OUT}${enginePath}${REGULAR_OUT} RTI=${BOLD_OUT}${rti}${REGULAR_OUT}`); } }; } diff --git a/package-lock.json b/package-lock.json index 55ceb5c697b..ab7a9380fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-terser": "^0.4.4", "@rollup/pluginutils": "^5.1.0", + "@runtime-type-inspector/plugin-rollup": "^4.0.3", "c8": "^10.1.2", "chai": "^5.1.0", "eslint": "^9.10.0", @@ -2590,6 +2591,56 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@runtime-type-inspector/plugin-rollup": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/plugin-rollup/-/plugin-rollup-4.0.3.tgz", + "integrity": "sha512-01ZDqK3yRuSg3YBQ69gbhCIXb+wzxAePAjRn1cAV1/ciArOVr3zz/HLNH/BgR1ULfn37m00OIc18Bq3vHFKDQw==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "@runtime-type-inspector/transpiler": "^4.0.3", + "rollup": "^3.29.4" + } + }, + "node_modules/@runtime-type-inspector/plugin-rollup/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@runtime-type-inspector/runtime": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/runtime/-/runtime-4.0.3.tgz", + "integrity": "sha512-dP1DQIM9p8l2hzSu9rA1pBiO+sO1MeaXPKeRkWufwQ745zTuOyeXj6raXOFk7bPONDX30IucOyNyPLKnwqiWng==", + "dev": true, + "dependencies": { + "display-anything": "^1.2.0" + } + }, + "node_modules/@runtime-type-inspector/transpiler": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/transpiler/-/transpiler-4.0.3.tgz", + "integrity": "sha512-gc1wRiD4hURvHR/dh/CmkqTPG+5AStYd92z5oidTfH9zLimPjPfziDw7B+5Jr61UNHPHVaqmn+rmaz8Ipfmqtw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@runtime-type-inspector/runtime": "^4.0.3", + "typescript": "^5.1.6" + }, + "bin": { + "transpiler": "bin.js" + } + }, "node_modules/@shikijs/core": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.16.2.tgz", @@ -4216,6 +4267,12 @@ "node": ">=0.3.1" } }, + "node_modules/display-anything": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/display-anything/-/display-anything-1.2.0.tgz", + "integrity": "sha512-QeMtc1JMjZWH0iswd9f0LBiphQUekYClPr5wve5d+QsdZ+UWOAjsdmrLDO1XxomlCL/vAhPY7PRgu5KVN4WdOQ==", + "dev": true + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", diff --git a/package.json b/package.json index 5c79b77a1ab..40adf94f9b7 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-terser": "^0.4.4", "@rollup/pluginutils": "^5.1.0", + "@runtime-type-inspector/plugin-rollup": "^4.0.3", "c8": "^10.1.2", "chai": "^5.1.0", "eslint": "^9.10.0", @@ -115,6 +116,7 @@ "build:treenet": "npm run build target:umd treenet", "build:treesun": "npm run build target:umd treesun", "build:sourcemaps": "npm run build -- -m", + "build:rti": "rollup -c --environment target:rti", "watch": "npm run build -- -w", "watch:release": "npm run build target:release -- -w", "watch:debug": "npm run build target:debug -- -w", diff --git a/rollup.config.mjs b/rollup.config.mjs index cdb4d52000a..cfa31e2bbfa 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,6 +1,7 @@ import * as fs from 'node:fs'; import { version, revision } from './utils/rollup-version-revision.mjs'; import { buildTarget } from './utils/rollup-build-target.mjs'; +import { buildTargetRTI } from './utils/rollup-build-target-rti.mjs'; // unofficial package plugins import dts from 'rollup-plugin-dts'; @@ -103,6 +104,11 @@ BUILD_TYPES.forEach((buildType) => { }); }); +if (envTarget === 'rti') { + targets.length = 0; + targets.push(buildTargetRTI('umd'), buildTargetRTI('es')); +} + if (envTarget === null || envTarget === 'types') { targets.push(...TYPES_TARGET); } diff --git a/src/core/debug.js b/src/core/debug.js index df7fccd4418..c8ae0653f46 100644 --- a/src/core/debug.js +++ b/src/core/debug.js @@ -52,7 +52,7 @@ class Debug { /** * 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 {boolean|object|string} assertion - The assertion to check. * @param {...*} args - The values to be written to the log. */ static assert(assertion, ...args) { diff --git a/src/framework/anim/binder/anim-binder.js b/src/framework/anim/binder/anim-binder.js index 2634680071c..b86ff496b35 100644 --- a/src/framework/anim/binder/anim-binder.js +++ b/src/framework/anim/binder/anim-binder.js @@ -58,7 +58,7 @@ class AnimBinder { * or string path. * @returns {string} The locator encoded as a string. * @example - * // returns 'spotLight/light/color.r' + * // returns 'spotLight/light/color/r' * encode(['spotLight'], 'light', ['color', 'r']); */ static encode(entityPath, component, propertyPath) { diff --git a/src/framework/asset/asset-reference.js b/src/framework/asset/asset-reference.js index 6e826324a7d..9914564797c 100644 --- a/src/framework/asset/asset-reference.js +++ b/src/framework/asset/asset-reference.js @@ -59,7 +59,7 @@ class AssetReference { * Sets the asset id which this references. One of either id or url must be set to * initialize an asset reference. * - * @type {number} + * @type {number|null} */ set id(value) { if (this.url) throw Error('Can\'t set id and url'); @@ -67,7 +67,7 @@ class AssetReference { this._unbind(); this._id = value; - this.asset = this._registry.get(this._id); + this.asset = this._id === null ? null : this._registry.get(this._id); this._bind(); } @@ -93,7 +93,7 @@ class AssetReference { this._unbind(); this._url = value; - this.asset = this._registry.getByUrl(this._url); + this.asset = this._url === null ? null : this._registry.getByUrl(this._url); this._bind(); } diff --git a/src/index.rti.js b/src/index.rti.js new file mode 100644 index 00000000000..53d679cda8a --- /dev/null +++ b/src/index.rti.js @@ -0,0 +1,102 @@ +export * from './index.js'; +import { Vec2 } from './core/math/vec2.js'; +import { Vec3 } from './core/math/vec3.js'; +import { Vec4 } from './core/math/vec4.js'; +import { Quat } from './core/math/quat.js'; +import { Mat3 } from './core/math/mat3.js'; +import { Mat4 } from './core/math/mat4.js'; +import { customTypes, customValidations, validateNumber, TypePanel } from '@runtime-type-inspector/runtime'; +import 'display-anything/src/style.js'; +Object.assign(customTypes, { + AnimSetter(value) { + // Fix for type in ./framework/anim/evaluator/anim-target.js + // The AnimSetter type is not sufficient, just patching in the correct type here + if (value instanceof Function) { + return true; + } + return value?.set instanceof Function && value?.set instanceof Function; + }, + AnimBinder(value) { + // Still using: @implements {AnimBinder} + // RTI doesn't take notice of that so far and we started removing `@implements` aswell: + // Testable via graphics/contact-hardening-shadows example. + return value?.constructor?.name?.endsWith('Binder'); + }, + ComponentData(value) { + // Used in src/framework/components/collision/trigger.js + // Why do we neither use @implements nor `extends` for such type? + // Testable via animation/locomotion example. + return value?.constructor?.name?.endsWith('ComponentData'); + }, + Renderer(value) { + // E.g. instance of `ForwardRenderer` + return value?.constructor?.name?.endsWith('Renderer'); + } +}); +// For quickly checking props of Vec2/Vec3/Vec4/Quat/Mat3/Mat4 without GC +const propsXY = ['x', 'y']; +const propsXYZ = ['x', 'y', 'z']; +const propsXYZW = ['x', 'y', 'z', 'w']; +const props9 = [0, 1, 2, 3, 4, 5, 6, 7, 8]; +const props16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; +/** + * `@ignoreRTI` + * @param {any} value - The value. + * @param {*} expect - Expected type structure. + * @todo Split array/class. + * @param {string} loc - String like `BoundingBox#compute` + * @param {string} name - Name of the argument. + * @param {boolean} critical - Only false for unions. + * @param {console["warn"]} warn - Function to warn with. + * @param {number} depth - The depth to detect recursion. + * @returns {boolean} Only false if we can find some NaN issues or denormalisation issues. + */ +function validate(value, expect, loc, name, critical, warn, depth) { + /** + * @param {string|number} prop - Something like 'x', 'y', 'z', 'w', 0, 1, 2, 3, 4 etc. + * @returns {boolean} Wether prop is a valid number. + */ + const checkProp = (prop) => { + return validateNumber(value, prop); + }; + if (value instanceof Vec2) { + return propsXY.every(checkProp); + } + if (value instanceof Vec3) { + return propsXYZ.every(checkProp); + } + if (value instanceof Vec4) { + return propsXYZW.every(checkProp); + } + if (value instanceof Quat) { + const length = value.length(); + // Don't want developers of denormalized Quat's when they normalize it. + if (loc !== 'Quat#normalize' && loc !== 'Quat#mulScalar') { + // A quaternion should have unit length, but can become denormalized due to + // floating point precision errors (aka error creep). For instance through: + // - Successive quaternion operations + // - Conversion from matrices, which accumulated FP precision loss. + // Simple solution is to renormalize the quaternion. + if (Math.abs(1 - length) > 0.001) { + warn('Quat is denormalized, please renormalize it before use.'); + return false; + } + } + return propsXYZW.every(checkProp); + } + if (value instanceof Mat3) { + return props9.every(prop => validateNumber(value.data, prop)); + } + if (value instanceof Mat4) { + return props16.every(prop => validateNumber(value.data, prop)); + } + return true; +} +customValidations.push(validate); +export const typePanel = new TypePanel(); +globalThis.parent.addEventListener('message', (e) => { + if (e.data.type !== 'rti') { + return; + } + typePanel.handleEvent(e); +}); diff --git a/src/platform/graphics/index-buffer.js b/src/platform/graphics/index-buffer.js index 5ce56ec1803..5aca3d92991 100644 --- a/src/platform/graphics/index-buffer.js +++ b/src/platform/graphics/index-buffer.js @@ -160,7 +160,7 @@ class IndexBuffer { /** * Set preallocated data on the index buffer. * - * @param {ArrayBuffer} data - The index data to set. + * @param {ArrayBuffer|Uint16Array} data - The index data to set. * @returns {boolean} True if the data was set successfully, false otherwise. * @ignore */ diff --git a/src/platform/graphics/texture.js b/src/platform/graphics/texture.js index 00f6bf36171..44005ce428a 100644 --- a/src/platform/graphics/texture.js +++ b/src/platform/graphics/texture.js @@ -876,7 +876,7 @@ class Texture { * Set the pixel data of the texture from a canvas, image, video DOM element. If the texture is * a cubemap, the supplied source must be an array of 6 canvases, images or videos. * - * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]} source - A + * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|ImageBitmap} source - A * canvas, image or video element, or an array of 6 canvas, image or video elements. * @param {number} [mipLevel] - A non-negative integer specifying the image level of detail. * Defaults to 0, which represents the base image source. A level value of N, that is greater diff --git a/src/platform/graphics/vertex-buffer.js b/src/platform/graphics/vertex-buffer.js index 74c4fb66e8f..8895a709f2d 100644 --- a/src/platform/graphics/vertex-buffer.js +++ b/src/platform/graphics/vertex-buffer.js @@ -28,7 +28,7 @@ class VertexBuffer { * @param {object} [options] - Object for passing optional arguments. * @param {number} [options.usage] - The usage type of the vertex buffer (see BUFFER_*). * Defaults to BUFFER_STATIC. - * @param {ArrayBuffer} [options.data] - Initial data. + * @param {ArrayBuffer|Float32Array} [options.data] - Initial data. * @param {boolean} [options.storage] - Defines if the vertex buffer can be used as a storage * buffer by a compute shader. Defaults to false. Only supported on WebGPU. */ diff --git a/src/platform/graphics/vertex-iterator.js b/src/platform/graphics/vertex-iterator.js index adcb76a8930..8887ca7bb68 100644 --- a/src/platform/graphics/vertex-iterator.js +++ b/src/platform/graphics/vertex-iterator.js @@ -122,8 +122,6 @@ class VertexIteratorAccessor { * that are not relevant to this attribute. * @param {number} vertexElement.stride - The number of total bytes that are between the start * of one vertex, and the start of the next. - * @param {ScopeId} vertexElement.scopeId - The shader input variable corresponding to the - * attribute. * @param {number} vertexElement.size - The size of the attribute in bytes. * @param {VertexFormat} vertexFormat - A vertex format that defines the layout of vertex data * inside the buffer. diff --git a/src/platform/input/keyboard-event.js b/src/platform/input/keyboard-event.js index cbe59e43fb0..d88270aa7e6 100644 --- a/src/platform/input/keyboard-event.js +++ b/src/platform/input/keyboard-event.js @@ -33,8 +33,8 @@ class KeyboardEvent { /** * Create a new KeyboardEvent. * - * @param {Keyboard} keyboard - The keyboard object which is firing the event. - * @param {globalThis.KeyboardEvent} event - The original browser event that was fired. + * @param {Keyboard} [keyboard] - The keyboard object which is firing the event. + * @param {globalThis.KeyboardEvent} [event] - The original browser event that was fired. * @example * const onKeyDown = function (e) { * if (e.key === pc.KEY_SPACE) { diff --git a/src/scene/shader-lib/programs/standard.js b/src/scene/shader-lib/programs/standard.js index ea997085e3c..276aada4f99 100644 --- a/src/scene/shader-lib/programs/standard.js +++ b/src/scene/shader-lib/programs/standard.js @@ -108,7 +108,7 @@ class ShaderGeneratorStandard extends ShaderGenerator { * @param {object} options - The options passed into to createShaderDefinition. * @param {object} chunks - The set of shader chunks to choose from. * @param {object} mapping - The mapping between chunk and sampler - * @param {string} encoding - The texture's encoding + * @param {string|null} encoding - The texture's encoding * @returns {string} The shader code to support this map. * @private */ diff --git a/utils/rollup-build-target-rti.mjs b/utils/rollup-build-target-rti.mjs new file mode 100644 index 00000000000..b4f4b7f3f59 --- /dev/null +++ b/utils/rollup-build-target-rti.mjs @@ -0,0 +1,55 @@ +import resolve from '@rollup/plugin-node-resolve'; +import { engineLayerImportValidation } from './plugins/rollup-import-validation.mjs'; +import { getBanner } from './rollup-get-banner.mjs'; +import { runtimeTypeInspector } from '@runtime-type-inspector/plugin-rollup'; + +/** @typedef {import('rollup').RollupOptions} RollupOptions */ +/** @typedef {import('rollup').OutputOptions} OutputOptions */ +/** @typedef {import('rollup').ModuleFormat} ModuleFormat */ +/** @typedef {import('@rollup/plugin-babel').RollupBabelInputPluginOptions} RollupBabelInputPluginOptions */ +/** @typedef {import('@rollup/plugin-strip').RollupStripOptions} RollupStripOptions */ + +/** + * Configure a Runtime Type Inspector target that rollup is supposed to build. + * + * @param {'umd'|'es'} moduleFormat - The module format (subset of ModuleFormat). + * @param {string} input - The input file. + * @param {string} buildDir - The build dir. + * @returns {RollupOptions} Configuration for Runtime Type Inspector rollup target. + */ +function buildTargetRTI(moduleFormat, input = 'src/index.rti.js', buildDir = 'build') { + const banner = getBanner(' (RUNTIME-TYPE-INSPECTOR)'); + + const outputExtension = { + umd: '.js', + es: '.mjs' + }; + + const file = `${buildDir}/playcanvas.rti${outputExtension[moduleFormat]}`; + + /** @type {OutputOptions} */ + const outputOptions = { + banner, + format: moduleFormat, + indent: '\t', + name: 'pc', + file + }; + + return { + input, + output: outputOptions, + plugins: [ + engineLayerImportValidation(input, true), + resolve(), + runtimeTypeInspector({ + ignoredFiles: [ + 'node_modules', + 'framework/parsers/draco-worker.js', // runs in Worker context without RTI + 'scene/gsplat/gsplat-sorter.js' + ] + }) + ] + }; +} +export { buildTargetRTI };