diff --git a/examples/package-lock.json b/examples/package-lock.json index 9d6546e06b0..3d894b174ae 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -43,7 +43,7 @@ }, "..": { "name": "playcanvas", - "version": "2.6.0-dev", + "version": "2.8.0-dev", "dev": true, "license": "MIT", "dependencies": { @@ -57,6 +57,7 @@ "@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.11.8", "@types/node": "22.13.10", "c8": "10.1.3", @@ -73,8 +74,9 @@ "rollup-plugin-visualizer": "5.14.0", "serve": "14.2.4", "sinon": "19.0.2", - "typedoc": "0.27.9", - "typedoc-plugin-mdn-links": "4.0.15", + "typedoc": "^0.28.1", + "typedoc-plugin-mdn-links": "^5.0.1", + "typedoc-plugin-missing-exports": "^4.0.0", "typescript": "5.8.2" }, "engines": { diff --git a/examples/package.json b/examples/package.json index d2694bb8f38..ad4a90d6e8c 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 485e4e4abff..2d773e3c2da 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. @@ -233,6 +235,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 8c6e5e422d7..db23c178a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playcanvas", - "version": "2.7.0-dev", + "version": "2.8.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playcanvas", - "version": "2.7.0-dev", + "version": "2.8.0-dev", "license": "MIT", "dependencies": { "@types/webxr": "^0.5.16", @@ -19,6 +19,7 @@ "@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.11.8", "@types/node": "22.13.10", "c8": "10.1.3", @@ -77,13 +78,49 @@ "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, "license": "MIT", - "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, "engines": { "node": ">=6.9.0" } @@ -1010,6 +1047,56 @@ "dev": true, "license": "MIT" }, + "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": "3.2.1", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz", @@ -2658,6 +2745,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 81900fa981b..f504cc28a92 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@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.11.8", "@types/node": "22.13.10", "c8": "10.1.3", 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/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index cdc229ead08..0fafd9e1975 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -97,12 +97,12 @@ class ViewCube extends EventHandler { /** * @type {{ - * nx: SVGAElement, - * ny: SVGAElement, - * nz: SVGAElement, - * px: SVGAElement, - * py: SVGAElement, - * pz: SVGAElement, + * nx: SVGGElement, + * ny: SVGGElement, + * nz: SVGGElement, + * px: SVGGElement, + * py: SVGGElement, + * pz: SVGGElement, * xaxis: SVGLineElement, * yaxis: SVGLineElement, * zaxis: SVGLineElement @@ -338,7 +338,7 @@ class ViewCube extends EventHandler { /** * @private - * @param {SVGAElement} group - The group. + * @param {SVGGElement} group - The group. * @param {number} x - The x. * @param {number} y - The y. */ @@ -375,10 +375,10 @@ class ViewCube extends EventHandler { * @param {string} color - The color. * @param {boolean} [fill] - The fill. * @param {string} [text] - The text. - * @returns {SVGAElement} - The circle. + * @returns {SVGGElement} - The circle. */ _circle(color, fill = false, text) { - const group = /** @type {SVGAElement} */ (document.createElementNS(this._svg.namespaceURI, 'g')); + const group = /** @type {SVGGElement} */ (document.createElementNS(this._svg.namespaceURI, 'g')); const circle = /** @type {SVGCircleElement} */ (document.createElementNS(this._svg.namespaceURI, 'circle')); circle.setAttribute('fill', fill ? color : this._colorNeg.toString(false)); 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/anim/evaluator/anim-target.js b/src/framework/anim/evaluator/anim-target.js index 99490e918ea..4faf33218db 100644 --- a/src/framework/anim/evaluator/anim-target.js +++ b/src/framework/anim/evaluator/anim-target.js @@ -9,7 +9,7 @@ class AnimTarget { * * @param {(value: number[]) => void} func - This function will be called when a new animation value is output * by the {@link AnimEvaluator}. - * @param {'vector'|'quaternion'} type - The type of animation data this target expects. + * @param {'vector'|'quaternion'|'number'} type - The type of animation data this target expects. * @param {number} components - The number of components on this target (this should ideally * match the number of components found on all attached animation curves). * @param {string} targetPath - The path to the target value. 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/framework/components/element/element-drag-helper.js b/src/framework/components/element/element-drag-helper.js index 6fd0bde8c84..66767d32dd3 100644 --- a/src/framework/components/element/element-drag-helper.js +++ b/src/framework/components/element/element-drag-helper.js @@ -244,7 +244,7 @@ class ElementDragHelper extends EventHandler { /** * This method is linked to `_element` events: `mousemove` and `touchmove` * - * @param {ElementTouchEvent} event - The event. + * @param {ElementMouseEvent|ElementTouchEvent} event - The event. * @private */ _onMove(event) { diff --git a/src/framework/components/render/component.js b/src/framework/components/render/component.js index 75bc3b37579..4656ca585ca 100644 --- a/src/framework/components/render/component.js +++ b/src/framework/components/render/component.js @@ -680,7 +680,7 @@ class RenderComponent extends Component { * Sets the render asset (or asset id) for the render component. This only applies to render components with * type 'asset'. * - * @type {Asset|number} + * @type {Asset|number|null} */ set asset(value) { const id = value instanceof Asset ? value.id : value; 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/device-cache.js b/src/platform/graphics/device-cache.js index 6f9c09fa66d..113e6432b65 100644 --- a/src/platform/graphics/device-cache.js +++ b/src/platform/graphics/device-cache.js @@ -18,7 +18,7 @@ class DeviceCache { * Returns the resources for the supplied device. * * @param {GraphicsDevice} device - The graphics device. - * @param {() => any} onCreate - A function that creates the resource for the device. + * @param {() => any} [onCreate] - A function that creates the resource for the device. * @returns {any} The resource for the device. */ get(device, onCreate) { diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js index 86020ac128d..82bb0cb6d1f 100644 --- a/src/platform/graphics/graphics-device.js +++ b/src/platform/graphics/graphics-device.js @@ -1033,7 +1033,7 @@ class GraphicsDevice extends EventHandler { * * @param {Shader} shader - The shader to validate. * @param {VertexFormat} vb0Format - The format of the first vertex buffer. - * @param {VertexFormat} vb1Format - The format of the second vertex buffer. + * @param {VertexFormat} [vb1Format] - The format of the second vertex buffer. * @protected */ validateAttributes(shader, vb0Format, vb1Format) { diff --git a/src/platform/graphics/index-buffer.js b/src/platform/graphics/index-buffer.js index 5ce56ec1803..3cb145e98a4 100644 --- a/src/platform/graphics/index-buffer.js +++ b/src/platform/graphics/index-buffer.js @@ -37,7 +37,7 @@ class IndexBuffer { * - {@link BUFFER_STREAM} * * Defaults to {@link BUFFER_STATIC}. - * @param {ArrayBuffer} [initialData] - Initial data. If left unspecified, the index buffer + * @param {ArrayBuffer|Uint16Array} [initialData] - Initial data. If left unspecified, the index buffer * will be initialized to zeros. * @param {object} [options] - Object for passing optional arguments. * @param {boolean} [options.storage] - Defines if the index buffer can be used as a storage @@ -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 c8c73b9abae..83ee40cd2ea 100644 --- a/src/platform/graphics/texture.js +++ b/src/platform/graphics/texture.js @@ -200,7 +200,7 @@ class Texture { * - {@link FUNC_NOTEQUAL} * * Defaults to {@link FUNC_LESS}. - * @param {Uint8Array[]|Uint16Array[]|Uint32Array[]|Float32Array[]|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|Uint8Array[][]} [options.levels] + * @param {Uint8Array[]|Uint16Array[]|Uint32Array[]|Float32Array[]|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|Uint8Array[][]|Uint8ClampedArray[][]} [options.levels] * - Array of Uint8Array or other supported browser interface; or a two-dimensional array * of Uint8Array if options.arrayLength is defined and greater than zero. * @param {boolean} [options.storage] - Defines if texture can be used as a storage texture by @@ -1003,7 +1003,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..49b9bed8bc2 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. */ @@ -146,7 +146,7 @@ class VertexBuffer { /** * Copies data into vertex buffer's memory. * - * @param {ArrayBuffer} [data] - Source data to copy. + * @param {ArrayBuffer|Float32Array} [data] - Source data to copy. * @returns {boolean} True if function finished successfully, false otherwise. */ setData(data) { diff --git a/src/platform/graphics/vertex-iterator.js b/src/platform/graphics/vertex-iterator.js index 5c737c5802b..7740dbe5149 100644 --- a/src/platform/graphics/vertex-iterator.js +++ b/src/platform/graphics/vertex-iterator.js @@ -123,8 +123,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 11a1a69bb33..7c39d19f3c5 100644 --- a/src/platform/input/keyboard-event.js +++ b/src/platform/input/keyboard-event.js @@ -36,8 +36,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 f8639fd31d2..12352bfc8a3 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/mesh-instance.js b/src/scene/mesh-instance.js index 5a82ae0e53d..a1391c35b6a 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -503,7 +503,7 @@ class MeshInstance { /** * Sets the graphics mesh being instanced. * - * @type {Mesh} + * @type {Mesh|null} */ set mesh(mesh) { @@ -722,7 +722,7 @@ class MeshInstance { /** * Sets the material used by this mesh instance. * - * @type {Material} + * @type {Material|null} */ set material(material) { diff --git a/src/scene/morph-target.js b/src/scene/morph-target.js index 2c0fc3e7caf..eeea0aadd1f 100644 --- a/src/scene/morph-target.js +++ b/src/scene/morph-target.js @@ -20,7 +20,7 @@ class MorphTarget { * Create a new MorphTarget instance. * * @param {object} options - Object for passing optional arguments. - * @param {ArrayBuffer} options.deltaPositions - An array of 3-dimensional vertex position + * @param {ArrayBuffer|Float32Array} options.deltaPositions - An array of 3-dimensional vertex position * offsets. * @param {ArrayBuffer} [options.deltaNormals] - An array of 3-dimensional vertex normal * offsets. diff --git a/src/scene/shader-lib/programs/shader-generator.js b/src/scene/shader-lib/programs/shader-generator.js index 3a9a0dc9c43..dac1d1bb382 100644 --- a/src/scene/shader-lib/programs/shader-generator.js +++ b/src/scene/shader-lib/programs/shader-generator.js @@ -2,7 +2,7 @@ import { hashCode } from '../../../core/hash.js'; class ShaderGenerator { /** - * @param {Map} defines - the set of defines to be used in the shader. + * @param {Map} defines - the set of defines to be used in the shader. * @returns {number} the hash code of the defines. */ static definesHash(defines) { 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 };