diff --git a/examples/package-lock.json b/examples/package-lock.json index 3aaab8c02fc..ec18e4bf07c 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -43,7 +43,7 @@ }, "..": { "name": "playcanvas", - "version": "2.4.0-dev", + "version": "2.6.0-dev", "dev": true, "license": "MIT", "dependencies": { @@ -51,31 +51,32 @@ "@webgpu/types": "^0.1.52" }, "devDependencies": { - "@napi-rs/canvas": "^0.1.65", - "@playcanvas/eslint-config": "^2.0.8", - "@rollup/plugin-node-resolve": "^16.0.0", - "@rollup/plugin-strip": "^3.0.4", - "@rollup/plugin-swc": "^0.4.0", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/pluginutils": "^5.1.4", - "@types/node": "^22.10.2", - "c8": "^10.1.3", - "chai": "^5.1.2", - "eslint": "^9.17.0", - "fflate": "^0.8.2", - "globals": "^15.14.0", - "jsdom": "^25.0.1", - "mocha": "^11.0.1", - "publint": "^0.2.12", - "rollup": "^4.29.1", - "rollup-plugin-dts": "^6.0.2", + "@playcanvas/eslint-config": "2.0.9", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-strip": "3.0.4", + "@rollup/plugin-swc": "0.4.0", + "@rollup/plugin-terser": "0.4.4", + "@rollup/pluginutils": "5.1.4", + "@swc/core": "1.10.7", + "@types/node": "22.10.5", + "c8": "10.1.3", + "canvas": "3.0.1", + "chai": "5.1.2", + "eslint": "9.17.0", + "fflate": "0.8.2", + "globals": "15.14.0", + "jsdom": "26.0.0", + "mocha": "11.0.1", + "publint": "0.3.0", + "rollup": "4.30.1", + "rollup-plugin-dts": "6.1.1", "rollup-plugin-jscc": "2.0.0", - "rollup-plugin-visualizer": "^5.12.0", - "serve": "^14.2.4", - "sinon": "^19.0.2", - "typedoc": "^0.27.5", - "typedoc-plugin-mdn-links": "^4.0.6", - "typescript": "^5.7.2" + "rollup-plugin-visualizer": "5.14.0", + "serve": "14.2.4", + "sinon": "19.0.2", + "typedoc": "0.27.6", + "typedoc-plugin-mdn-links": "4.0.7", + "typescript": "5.7.3" }, "engines": { "node": ">=18.0.0" diff --git a/examples/package.json b/examples/package.json index 8bbad7836eb..f9e4df3eb98 100644 --- a/examples/package.json +++ b/examples/package.json @@ -5,11 +5,12 @@ "main": "index.js", "type": "module", "scripts": { - "build": "npm run -s build:metadata && cross-env NODE_ENV=production rollup -c", + "build": "npm run -s build:metadata && cross-env NODE_ENV=production RTI=on rollup -c", "build:metadata": "node ./scripts/build-metadata.mjs", "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:metadata && cross-env NODE_ENV=development rollup -c -w" diff --git a/examples/rollup.config.js b/examples/rollup.config.mjs similarity index 97% rename from examples/rollup.config.js rename to examples/rollup.config.mjs index 47afe997e74..2e23b520ddf 100644 --- a/examples/rollup.config.js +++ b/examples/rollup.config.mjs @@ -11,6 +11,7 @@ import { exampleMetaData } from './cache/metadata.mjs'; import { copy } from './utils/plugins/rollup-copy.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'; import { buildHtml } from './utils/plugins/rollup-build-html.mjs'; import { buildShare } from './utils/plugins/rollup-build-share.mjs'; @@ -21,6 +22,7 @@ import { removePc } from './utils/plugins/rollup-remove-pc.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; /** * Get the engine path files. @@ -230,6 +232,9 @@ const engineRollupOptions = () => { /** @type {RollupOptions[]} */ const options = []; + if (RTI === 'on') { + options.push(buildTargetRTI('es', '../src/index.rti.js', 'dist/iframe/ENGINE_PATH')); + } if (ENGINE_PATH) { return options; } diff --git a/examples/scripts/build-examples.mjs b/examples/scripts/build-examples.mjs new file mode 100644 index 00000000000..b70be941f75 --- /dev/null +++ b/examples/scripts/build-examples.mjs @@ -0,0 +1,126 @@ +/** + * This script is used to generate the standalone HTML file for the iframe to view the example. + */ +import fs from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +import { parseConfig, engineFor, patchScript } from './utils.mjs'; +import { exampleMetaData } from '../cache/metadata.mjs'; + +// @ts-ignore +const __filename = fileURLToPath(import.meta.url); +const MAIN_DIR = `${dirname(__filename)}/../`; +const EXAMPLE_HTML = fs.readFileSync(`${MAIN_DIR}/iframe/example.html`, 'utf-8'); +const DIR_CACHE = new Map(); + +const TEMPLATE_CONTROLS = `/** + * @param {import('../../app/components/Example.mjs').ControlOptions} options - The options. + * @returns {JSX.Element} The returned JSX Element. + */ +export function controls({ fragment }) { + return fragment(); +}\n`; + +/** + * @param {string} categoryKebab - The category kebab name. + * @param {string} exampleNameKebab - The example kebab name. + * @param {import('./utils.mjs').Engine | undefined} setEngineType - The engine type. + * @param {string[]} files - The files in the example directory. + * @returns {string} File to write as standalone example. + */ +const generateExampleFile = (categoryKebab, exampleNameKebab, setEngineType, files) => { + let html = EXAMPLE_HTML; + + // title + html = html.replace(/'@TITLE'/g, `${categoryKebab}: ${exampleNameKebab}`); + + // js files + html = html.replace(/'@FILES'/g, JSON.stringify(files)); + + // engine + const engineType = process.env.ENGINE_PATH ? 'development' : process.env.NODE_ENV === 'development' ? 'debug' : setEngineType; + 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)) { + throw new Error('HTML file still has unreplaced values'); + } + + return html; +}; + +/** + * @param {Record} env - The environment variables. + */ +export const build = (env = {}) => { + Object.assign(process.env, env); + + if (!fs.existsSync(`${MAIN_DIR}/dist/`)) { + fs.mkdirSync(`${MAIN_DIR}/dist/`); + } + if (!fs.existsSync(`${MAIN_DIR}/dist/iframe/`)) { + fs.mkdirSync(`${MAIN_DIR}/dist/iframe/`); + } + + exampleMetaData.forEach((/** @type {{ categoryKebab: string; exampleNameKebab: string; path: string; }} */ data) => { + const { categoryKebab, exampleNameKebab, path } = data; + const name = `${categoryKebab}_${exampleNameKebab}`; + + if (!DIR_CACHE.has(path)) { + DIR_CACHE.set(path, fs.readdirSync(path)); + } + + /** + * @type {string[]} + */ + const files = []; + for (const file of DIR_CACHE.get(path)) { + if (file.startsWith(`${exampleNameKebab}.`)) { + files.push(file.replace(`${exampleNameKebab}.`, '')); + } + } + if (!files.includes('example.mjs')) { + throw new Error(`Example ${name} is missing an example.mjs file`); + } + if (!files.includes('controls.mjs')) { + files.push('controls.mjs'); + } + + files.forEach((file) => { + if (file === 'example.mjs') { + const examplePath = resolve(path, `${exampleNameKebab}.${file}`); + + // example file + const script = fs.readFileSync(examplePath, 'utf-8'); + fs.writeFileSync(`${MAIN_DIR}/dist/iframe/${name}.example.mjs`, patchScript(script)); + + // html file + const config = parseConfig(script); + const out = generateExampleFile(categoryKebab, exampleNameKebab, config.ENGINE, files); + fs.writeFileSync(`${MAIN_DIR}/dist/iframe/${name}.html`, out); + return; + } + + if (file === 'controls.mjs') { + const controlsPath = resolve(path, `${exampleNameKebab}.${file}`); + const controlsExist = fs.existsSync(controlsPath); + + // controls file + const script = controlsExist ? fs.readFileSync(controlsPath, 'utf-8') : TEMPLATE_CONTROLS; + fs.writeFileSync(`${MAIN_DIR}/dist/iframe/${name}.controls.mjs`, patchScript(script)); + return; + } + + const scriptPath = resolve(path, `${exampleNameKebab}.${file}`); + let script = fs.readFileSync(scriptPath, 'utf-8'); + if (/\.(?:mjs|js)$/.test(file)) { + script = patchScript(script); + } + fs.writeFileSync(`${MAIN_DIR}/dist/iframe/${name}.${file}`, script); + }); + }); +}; diff --git a/examples/src/app/components/Bottombar.mjs b/examples/src/app/components/Bottombar.mjs new file mode 100644 index 00000000000..24d263563aa --- /dev/null +++ b/examples/src/app/components/Bottombar.mjs @@ -0,0 +1,317 @@ +import { Observer } from '@playcanvas/observer'; +import { BindingTwoWay, BooleanInput, Container, Label, LabelGroup, Panel, TextInput } from '@playcanvas/pcui/react'; +import { Component } from 'react'; +import { Link } from 'react-router-dom'; + +import { exampleMetaData } from '../../../cache/metadata.mjs'; +import { MIN_DESKTOP_WIDTH } from '../constants.mjs'; +import { iframe } from '../iframe.mjs'; +import { jsx } from '../jsx.mjs'; +import { thumbnailPath } from '../paths.mjs'; +import { getOrientation } from '../utils.mjs'; + +// eslint-disable-next-line jsdoc/require-property +/** + * @typedef {object} Props + */ + +/** + * @typedef {object} State + * @property {Record>} defaultCategories - The default categories. + * @property {Record>|null} filteredCategories - The filtered categories. + * @property {string} hash - The hash. + * @property {Observer} observer - The observer. + * @property {boolean} collapsed - Collapsed or not. + * @property {string} orientation - Current orientation. + */ + +/** + * @type {typeof Component} + */ +const TypedComponent = Component; + +/** + * @returns {Record }>} - The category files. + */ +function getDefaultExampleFiles() { + /** @type {Record }>} */ + const categories = {}; + for (let i = 0; i < exampleMetaData.length; i++) { + const { categoryKebab, exampleNameKebab } = exampleMetaData[i]; + if (!categories[categoryKebab]) { + categories[categoryKebab] = { examples: {} }; + } + + categories[categoryKebab].examples[exampleNameKebab] = exampleNameKebab; + } + return categories; +} + +class BottomBar extends TypedComponent { + /** @type {State} */ + state = { + defaultCategories: getDefaultExampleFiles(), + filteredCategories: null, + hash: location.hash, + observer: new Observer({ largeThumbnails: false }), + // @ts-ignore + collapsed: localStorage.getItem('bottomBarCollapsed') === 'true' || window.top.innerWidth < MIN_DESKTOP_WIDTH, + orientation: getOrientation() + }; + + /** + * @param {Props} props - Component properties. + */ + constructor(props) { + super(props); + this._onLayoutChange = this._onLayoutChange.bind(this); + this._onClickExample = this._onClickExample.bind(this); + this._onMessage = this._onMessage.bind(this); + } + + componentDidMount() { + // PCUI should just have a "onHeaderClick" but can't find anything + const bottomBar = document.getElementById('bottomBar'); + if (!bottomBar) { + return; + } + + /** @type {HTMLElement | null} */ + const bottomBarHeader = bottomBar.querySelector('.pcui-panel-header'); + if (!bottomBarHeader) { + return; + } + bottomBarHeader.onclick = () => this.toggleCollapse(); + this.setupControlPanelToggleButton(); + + // setup events + window.addEventListener('resize', this._onLayoutChange); + window.addEventListener('orientationchange', this._onLayoutChange); + window.addEventListener('message', this._onMessage); + } + + /** + * @param {CustomEvent} e - The event with possible RTI type infos. + */ + _onMessage(e) { + // console.log("GOT", e); + } + + componentWillUnmount() { + window.removeEventListener('resize', this._onLayoutChange); + window.removeEventListener('orientationchange', this._onLayoutChange); + window.removeEventListener('message', this._onMessage); + } + + setupControlPanelToggleButton() { + // set up the control panel toggle button + const bottomBar = document.getElementById('bottomBar'); + if (!bottomBar) { + return; + } + window.addEventListener('hashchange', () => { + this.mergeState({ hash: location.hash }); + }); + this.state.observer.on('largeThumbnails:set', () => { + let minTopNavItemDistance = Number.MAX_VALUE; + + /** @type {NodeListOf} */ + const navItems = document.querySelectorAll('.nav-item'); + for (let i = 0; i < navItems.length; i++) { + const nav = navItems[i]; + const navItemDistance = Math.abs(120 - nav.getBoundingClientRect().top); + if (navItemDistance < minTopNavItemDistance) { + minTopNavItemDistance = navItemDistance; + bottomBar.classList.toggle('small-thumbnails'); + nav.scrollIntoView(); + break; + } + } + }); + bottomBar.classList.add('visible'); + // when first opening the examples browser via a specific example, scroll it into view + // @ts-ignore + if (!window._scrolledToExample) { + const examplePath = location.hash.split('/'); + document.getElementById(`link-${examplePath[1]}-${examplePath[2]}`)?.scrollIntoView(); + // @ts-ignore + window._scrolledToExample = true; + } + } + + /** + * @param {Partial} state - The partial state to update. + */ + mergeState(state) { + // new state is always calculated from the current state, + // avoiding any potential issues with asynchronous updates + this.setState(prevState => ({ ...prevState, ...state })); + } + + toggleCollapse() { + const { collapsed } = this.state; + localStorage.setItem('bottomBarCollapsed', `${!collapsed}`); + this.mergeState({ collapsed: !collapsed }); + } + + _onLayoutChange() { + this.mergeState({ orientation: getOrientation() }); + } + + /** + * @param {string} filter - The filter string. + */ + onChangeFilter(filter) { + const { defaultCategories } = this.state; + // Turn a filter like 'mes dec' (for mesh decals) into 'mes.*dec', because the examples + // show "MESH DECALS" but internally it's just "MeshDecals". + filter = filter.replace(/\s/g, '.*'); + const reg = filter && filter.length > 0 ? new RegExp(filter, 'i') : null; + if (!reg) { + this.mergeState({ filteredCategories: defaultCategories }); + return; + } + /** @type {Record>} */ + const updatedCategories = {}; + Object.keys(defaultCategories).forEach((category) => { + if (category.search(reg) !== -1) { + updatedCategories[category] = defaultCategories[category]; + return null; + } + Object.keys(defaultCategories[category].examples).forEach((example) => { + // @ts-ignore + const title = defaultCategories[category].examples[example]; + if (title.search(reg) !== -1) { + if (!updatedCategories[category]) { + updatedCategories[category] = { + name: defaultCategories[category].name, + examples: { + [example]: title + } + }; + } else { + // @ts-ignore + updatedCategories[category].examples[example] = title; + } + } + }); + }); + this.mergeState({ filteredCategories: updatedCategories }); + } + + + /** + * @param {import("react").MouseEvent} e - The event. + * @param {string} path - The path of example. + */ + _onClickExample(e, path) { + if (path === iframe.path) { + iframe.fire('hotReload'); + } else { + iframe.fire('destroy'); + } + } + + renderContents() { + const categories = this.state.filteredCategories || this.state.defaultCategories; + if (Object.keys(categories).length === 0) { + return jsx(Label, { text: 'No results' }); + } + const { hash } = this.state; + return Object.keys(categories) + .sort((a, b) => (a > b ? 1 : -1)) + .map((category) => { + return jsx( + Panel, + { + key: category, + class: 'categoryPanel', + headerText: category.split('-').join(' ').toUpperCase(), + collapsible: true, + collapsed: false + }, + jsx( + 'ul', + { + className: 'category-nav' + }, + Object.keys(categories[category].examples) + .sort((a, b) => (a > b ? 1 : -1)) + .map((example) => { + const path = `/${category}/${example}`; + const isSelected = new RegExp(`${path}$`).test(hash); + const className = `nav-item ${isSelected ? 'selected' : null}`; + return jsx( + Link, + { + key: example, + to: path, + onClick: e => this._onClickExample(e, path) + }, + jsx( + 'div', + { className: className, id: `link-${category}-${example}` }, + jsx('img', { + className: 'small-thumbnail', + loading: 'lazy', + src: `${thumbnailPath}${category}_${example}_small.webp` + }), + jsx('img', { + className: 'large-thumbnail', + loading: 'lazy', + src: `${thumbnailPath}${category}_${example}_large.webp` + }), + jsx( + 'div', + { + className: 'nav-item-text' + }, + example.split('-').join(' ').toUpperCase() + ) + ) + ); + }) + ) + ); + }); + } + + render() { + const { observer, collapsed, orientation } = this.state; + const panelOptions = { + headerText: 'PROBLEMS', + collapsible: true, + collapsed: false, + id: 'bottomBar', + class: ['small-thumbnails', collapsed ? 'collapsed' : null], + collapseHorizontally: true + }; + if (orientation === 'portrait') { + panelOptions.class = ['small-thumbnails']; + panelOptions.collapsed = collapsed; + } + return jsx( + Panel, + // @ts-ignore + panelOptions, + jsx(TextInput, { + class: 'filter-input', + keyChange: true, + placeholder: 'Filter...', + onChange: this.onChangeFilter.bind(this) + }), + jsx( + LabelGroup, + { text: 'Large thumbnails:' }, + jsx(BooleanInput, { + type: 'toggle', + binding: new BindingTwoWay(), + link: { observer, path: 'largeThumbnails' } + }) + ), + jsx(Container, { id: 'bottomBar-contents' }, this.renderContents()) + ); + } +} + +export { BottomBar }; diff --git a/examples/src/app/components/MainLayout.mjs b/examples/src/app/components/MainLayout.mjs index 9993870398a..63443f7d8c4 100644 --- a/examples/src/app/components/MainLayout.mjs +++ b/examples/src/app/components/MainLayout.mjs @@ -2,6 +2,7 @@ import { Container } from '@playcanvas/pcui/react'; import { Component } from 'react'; import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import { BottomBar } from './Bottombar.mjs'; import { CodeEditorDesktop } from './code-editor/CodeEditorDesktop.mjs'; import { Example } from './Example.mjs'; import { Menu } from './Menu.mjs'; @@ -74,6 +75,7 @@ class MainLayout extends TypedComponent { Route, { path: '/:category/:example' }, jsx(SideBar, null), + jsx(BottomBar, null), jsx( Container, { id: 'main-view-wrapper' }, diff --git a/examples/src/static/styles.css b/examples/src/static/styles.css index 2304318ef5d..c32868bf596 100644 --- a/examples/src/static/styles.css +++ b/examples/src/static/styles.css @@ -9,7 +9,7 @@ html, body, #app { body { margin: 0; - overflow: hidden; + /*overflow: hidden;*/ background-color: #171E20; } @@ -221,6 +221,21 @@ body { margin-bottom: 8px; } +#bottomBar { + position: absolute; + left: 0; + top: 100vh; + width: 100vw; + height: 100vh; +} + +#bottomBar > .pcui-panel > .pcui-panel-content { + width: calc(100% - 32px); + position: fixed; + height: 280px; + margin-top: 32px; +} + .nav-item { margin: 12px; overflow: auto; diff --git a/examples/utils/plugins/rollup-build-examples.mjs b/examples/utils/plugins/rollup-build-examples.mjs new file mode 100644 index 00000000000..3569c6928fd --- /dev/null +++ b/examples/utils/plugins/rollup-build-examples.mjs @@ -0,0 +1,32 @@ +// custom plugins +import { build } from '../../scripts/build-examples.mjs'; +import { watch } from '../rollup-watch.mjs'; + +const GREEN_OUT = '\x1b[32m'; +const BOLD_OUT = '\x1b[1m'; +const REGULAR_OUT = '\x1b[22m'; + +/** + * This plugin builds the standalone html files. + * + * @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, rti) { + return { + name: 'build-examples', + buildStart() { + if (nodeEnv === 'development') { + watch(this, 'iframe/example.html'); + watch(this, 'scripts/build-examples.mjs'); + watch(this, 'src/examples'); + } + }, + buildEnd() { + 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/examples/utils/plugins/rollup-build-html.mjs b/examples/utils/plugins/rollup-build-html.mjs index 3ed6f13b255..7a0384c0905 100644 --- a/examples/utils/plugins/rollup-build-html.mjs +++ b/examples/utils/plugins/rollup-build-html.mjs @@ -13,6 +13,9 @@ const EXAMPLE_TEMPLATE = fs.readFileSync('templates/example.html', 'utf-8'); * @returns {string} - The build file. */ export const engineUrl = (type) => { + if (process.env.RTI === 'on') { + return './ENGINE_PATH/playcanvas.rti.mjs'; + } switch (type) { case 'development': return './ENGINE_PATH/index.js'; diff --git a/package-lock.json b/package-lock.json index 75dbc444ce4..914ad77bc86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playcanvas", - "version": "2.5.0-dev", + "version": "2.6.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playcanvas", - "version": "2.5.0-dev", + "version": "2.6.0-dev", "license": "MIT", "dependencies": { "@types/webxr": "^0.5.16", @@ -19,10 +19,10 @@ "@rollup/plugin-swc": "0.4.0", "@rollup/plugin-terser": "0.4.4", "@rollup/pluginutils": "5.1.4", + "@runtime-type-inspector/plugin-rollup": "^4.0.6", "@swc/core": "1.10.7", "@types/node": "22.10.5", "c8": "10.1.3", - "canvas": "3.0.1", "chai": "5.1.2", "eslint": "9.17.0", "fflate": "0.8.2", @@ -81,12 +81,48 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, "engines": { "node": ">=6.9.0" } @@ -994,6 +1030,56 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@runtime-type-inspector/plugin-rollup": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/plugin-rollup/-/plugin-rollup-4.0.6.tgz", + "integrity": "sha512-du9+3rL9Mlk8Tb7pu5EcoxodbQy3kQ/J63dPZhTYbsEDcc8pL/0GuBfazpGALRCAugkHj0a3TM5FYZNscP8LYQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "@runtime-type-inspector/transpiler": "^4.0.6", + "rollup": "^3.29.4" + } + }, + "node_modules/@runtime-type-inspector/plugin-rollup/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "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.6", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/runtime/-/runtime-4.0.6.tgz", + "integrity": "sha512-FYUhMxXJe/xBWCJfgg30JQinziVS+zIjX536c5C2EmvBhce5QbU6iWPm4eTttJr4z9wOlD7P4Rjp5FPbQTSonA==", + "dev": true, + "dependencies": { + "display-anything": "^1.2.0" + } + }, + "node_modules/@runtime-type-inspector/transpiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@runtime-type-inspector/transpiler/-/transpiler-4.0.6.tgz", + "integrity": "sha512-TKwtuVAVnUeei1X6UO1KUc3Q6KX6BKMWbkss2UerNLRJ8GHkhG4GiVNBPofL+jh5DXgH0X3or9SMzRiNK0ZOaw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@runtime-type-inspector/runtime": "^4.0.6", + "typescript": "^5.1.6" + }, + "bin": { + "transpiler": "bin.js" + } + }, "node_modules/@shikijs/engine-oniguruma": { "version": "1.24.3", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.3.tgz", @@ -1702,7 +1788,9 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true, + "peer": true }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -1721,6 +1809,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1808,6 +1898,8 @@ "url": "https://feross.org/support" } ], + "optional": true, + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1894,16 +1986,16 @@ } }, "node_modules/canvas": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.1.tgz", - "integrity": "sha512-PcpVF4f8RubAeN/jCQQ/UymDKzOiLmRPph8fOTzDnlsUihkO/AUlxuhaa7wGRc3vMcCbV1fzuvyu5cWZlIcn1w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz", + "integrity": "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==", "dev": true, "hasInstallScript": true, - "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "simple-get": "^3.0.3" + "prebuild-install": "^7.1.1" }, "engines": { "node": "^18.12.0 || >= 20.9.0" @@ -2007,7 +2099,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/cli-boxes": { "version": "3.0.0", @@ -2341,18 +2435,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dev": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2443,6 +2525,8 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -2456,6 +2540,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", @@ -2485,6 +2575,8 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "once": "^1.4.0" } @@ -2998,6 +3090,8 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -3138,7 +3232,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -3260,7 +3356,9 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/glob": { "version": "10.4.5", @@ -3524,7 +3622,9 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true, + "peer": true }, "node_modules/ignore": { "version": "5.3.2", @@ -3564,7 +3664,9 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/ini": { "version": "1.3.8", @@ -4399,18 +4501,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4445,7 +4535,9 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/mocha": { "version": "11.0.1", @@ -4622,7 +4714,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4666,6 +4760,8 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -4678,6 +4774,8 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "optional": true, + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -4689,7 +4787,9 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4820,6 +4920,8 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "wrappy": "1" } @@ -5061,6 +5163,8 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -5087,6 +5191,8 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -5102,6 +5208,8 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5128,6 +5236,8 @@ "url": "https://feross.org/support" } ], + "optional": true, + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -5170,6 +5280,8 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5222,6 +5334,8 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5862,18 +5976,9 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "dev": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } + ], + "optional": true, + "peer": true }, "node_modules/sinon": { "version": "19.0.2", @@ -5978,6 +6083,8 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -6203,6 +6310,8 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -6215,6 +6324,8 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -6381,6 +6492,8 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -6627,7 +6740,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -6942,7 +7057,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/ws": { "version": "8.18.0", diff --git a/package.json b/package.json index 8724ff89002..e91b9005f08 100644 --- a/package.json +++ b/package.json @@ -74,10 +74,10 @@ "@rollup/plugin-swc": "0.4.0", "@rollup/plugin-terser": "0.4.4", "@rollup/pluginutils": "5.1.4", + "@runtime-type-inspector/plugin-rollup": "^4.0.6", "@swc/core": "1.10.7", "@types/node": "22.10.5", "c8": "10.1.3", - "canvas": "3.0.1", "chai": "5.1.2", "eslint": "9.17.0", "fflate": "0.8.2", 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/anim/evaluator/anim-curve.js b/src/framework/anim/evaluator/anim-curve.js index b2a48249171..dcaca1fd68d 100644 --- a/src/framework/anim/evaluator/anim-curve.js +++ b/src/framework/anim/evaluator/anim-curve.js @@ -1,3 +1,24 @@ +/** + * @example + * [ + * { + * "entityPath": [ + * "RootNode", + * "AVATAR", + * "C_spine0001_bind_JNT" + * ], + * "component": "graph", + * "propertyPath": [ + * "localPosition" + * ] + * } + * ] + * @typedef {object} AnimCurveTarget + * @property {string[]} entityPath - The path to the entity. + * @property {string} component - The component name. + * @property {string[]} propertyPath - The path to the property. + */ + /** * Animation curve links an input data set to an output data set and defines the interpolation * method to use. @@ -8,7 +29,7 @@ class AnimCurve { /** * Create a new animation curve. * - * @param {string[]} paths - Array of path strings identifying the targets of this curve, for + * @param {(string|AnimCurveTarget)[]} paths - Array of path strings identifying the targets of this curve, for * example "rootNode.translation". * @param {number} input - Index of the curve which specifies the key data. * @param {number} output - Index of the curve which specifies the value data. @@ -28,7 +49,7 @@ class AnimCurve { /** * The list of paths which identify targets of this curve. * - * @type {string[]} + * @type {(string|AnimCurveTarget)[]} */ get paths() { return this._paths; diff --git a/src/framework/asset/asset-reference.js b/src/framework/asset/asset-reference.js index a40bff51efe..d002a73cf46 100644 --- a/src/framework/asset/asset-reference.js +++ b/src/framework/asset/asset-reference.js @@ -102,7 +102,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'); @@ -110,7 +110,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(); } @@ -136,7 +136,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 83668f37c26..2103e951485 100644 --- a/src/platform/graphics/texture.js +++ b/src/platform/graphics/texture.js @@ -968,7 +968,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/materials/material.js b/src/scene/materials/material.js index 31fee4219ef..b4275bac680 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -55,8 +55,8 @@ let id = 0; * @property {CameraShaderParams} cameraShaderParams - The camera shader parameters. * @property {number} pass - The shader pass. * @property {Light[][]} sortedLights - The sorted lights. - * @property {UniformBufferFormat|undefined} viewUniformFormat - The view uniform format. - * @property {BindGroupFormat|undefined} viewBindGroupFormat - The view bind group format. + * @property {UniformBufferFormat|null} [viewUniformFormat] - The view uniform format. + * @property {BindGroupFormat|null} [viewBindGroupFormat] - The view bind group format. * @property {VertexFormat} vertexFormat - The vertex format. * @ignore */ diff --git a/src/scene/shader-lib/programs/standard.js b/src/scene/shader-lib/programs/standard.js index 7827d35e011..548ed412510 100644 --- a/src/scene/shader-lib/programs/standard.js +++ b/src/scene/shader-lib/programs/standard.js @@ -110,7 +110,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 };