diff --git a/README.md b/README.md index 08006517e6..faf0a0d32b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ Then visit https://hubs.local:8080 (note: HTTPS is required, you'll need to acce > Note: When running the Hubs client locally, you will still connect to the development versions of the [reticulum](https://github.com/Hubs-Foundation/reticulum) server. This server does not allow being accessed outside of localhost. If you want to host your own Hubs servers, please check out [Hubs Community Edition](https://github.com/Hubs-Foundation/hubs-cloud/tree/master/community-edition). +## Add-ons + +Hubs client add-ons are pluggable libraries that are installed as part of the client and loaded at runtime. Add-ons allow functionality to the Hubs core and allow easy Hubs client extensibility while maintaining the Hubs client core lean and minimal. + +You can read more about add-ons installation and development [here](doc/add-ons.md). + ## Contributing Read our [contributor guide](./CONTRIBUTING.md) to learn how you can submit bug reports, feature requests, and pull requests. @@ -49,11 +55,10 @@ Contributors are expected to abide by the project's [Code of Conduct](./CODE_OF_ ## Additional Resources -* [Reticulum](https://github.com/Hubs-Foundation/reticulum) - Phoenix-based backend for managing state and presence. -* [Networked A-Frame](https://github.com/Hubs-Foundation/networked-aframe). -* [Hubs-Ops](https://github.com/Hubs-Foundation/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS. +- [Reticulum](https://github.com/Hubs-Foundation/reticulum) - Phoenix-based backend for managing state and presence. +- [Networked A-Frame](https://github.com/Hubs-Foundation/networked-aframe). +- [Hubs-Ops](https://github.com/Hubs-Foundation/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS. ## License Hubs is licensed with the [Mozilla Public License 2.0](./LICENSE) - diff --git a/addons.json b/addons.json new file mode 100644 index 0000000000..702f72fd45 --- /dev/null +++ b/addons.json @@ -0,0 +1,8 @@ +{ + "addons": [ + "hubs-duck-addon", + "hubs-portals-addon", + "hubs-behavior-graphs-addon", + "hubs-postprocessing-addon" + ] +} \ No newline at end of file diff --git a/admin/package-lock.json b/admin/package-lock.json index 29904c666b..53c02fbde5 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -11,15 +11,15 @@ "dependencies": { "@iarna/toml": "^2.2.3", "@mozilla/lilypad-ui": "^1.8.2", - "aframe": "github:hubs-foundation/aframe#hubs/master", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "classnames": "^2.2.5", "hubs": "file:..", "react": "^16.1.1", "react-admin": "^2.6.3", "react-dom": "^16.1.1", "react-intl": "^2.4.0", - "three": "github:hubs-foundation/three.js#hubs-patches-141" + "three": "github:Hubs-Foundation/three.js#hubs-patches-141" }, "devDependencies": { "@babel/core": "^7.18.9", @@ -78,14 +78,14 @@ "@fortawesome/fontawesome-svg-core": "^1.2.2", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.0", - "@mozilla/lilypad-ui": "^1.8.3", + "@mozilla/lilypad-ui": "1.8.6", "@mozillareality/easing-functions": "^0.1.1", "@popperjs/core": "^2.4.4", - "aframe": "github:hubs-foundation/aframe#hubs/master", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", - "ammo.js": "github:hubs-foundation/ammo.js#hubs/master", - "animejs": "github:hubs-foundation/anime#hubs/master", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master", + "animejs": "github:Hubs-Foundation/anime#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "buffered-interpolation": "github:Infinitelee/buffered-interpolation", "classnames": "^2.2.5", "color": "^3.1.2", @@ -96,22 +96,24 @@ "detect-browser": "^3.0.1", "downshift": "^6.0.5", "draft-js": "^0.11.7", - "emoji-mart": "^5.5.2", + "emoji-picker-react": "^4.4.9", "event-target-shim": "^3.0.1", "form-data": "^3.0.0", "form-urlencoded": "^2.0.4", "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon", + "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", - "lib-hubs": "github:hubs-foundation/lib-hubs#master", + "lib-hubs": "github:Hubs-Foundation/lib-hubs#master", "linkify-it": "^2.0.3", "markdown-it": "^12.3.2", "moving-average": "^1.0.0", - "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", - "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master", + "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", + "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master", "node-ensure": "0.0.0", "normalize.css": "^8.0.1", "pdfjs-dist": "^2.14.305", @@ -135,13 +137,15 @@ "screenfull": "^4.0.1", "sdp-transform": "^2.14.1", "semver": "^7.3.2", - "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", - "three-ammo": "github:hubs-foundation/three-ammo", + "stream-browserify": "^3.0.0", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "three-ammo": "github:Hubs-Foundation/three-ammo", "three-gltf-extensions": "^0.0.14", "three-mesh-bvh": "^0.3.7", "three-pathfinding": "^1.1.0", "three-to-ammo": "github:infinitelee/three-to-ammo", "troika-three-text": "^0.45.0", + "url": "^0.11.1", "use-clipboard-copy": "^0.2.0", "uuid": "^3.2.1", "webrtc-adapter": "^7.7.0", @@ -162,13 +166,12 @@ "@formatjs/cli": "^5.0.6", "@formatjs/cli-lib": "^5.1.0", "@iarna/toml": "^2.2.5", - "@storybook/addon-actions": "^6.5.9", - "@storybook/addon-essentials": "^6.5.9", - "@storybook/addon-links": "^6.5.9", - "@storybook/builder-webpack5": "^6.5.9", - "@storybook/manager-webpack5": "^6.5.9", - "@storybook/react": "^6.5.9", - "@storybook/storybook-deployer": "^2.8.12", + "@storybook/addon-actions": "^7.0.20", + "@storybook/addon-essentials": "^7.0.20", + "@storybook/addon-links": "^7.0.20", + "@storybook/react": "^7.0.20", + "@storybook/react-webpack5": "^7.0.20", + "@storybook/storybook-deployer": "^2.8.16", "@svgr/webpack": "^6.3.1", "@types/three": "^0.141.0", "@types/webxr": "^0.5.0", @@ -185,7 +188,7 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-storybook": "^0.6.1", + "eslint-plugin-storybook": "^0.6.12", "esm": "^3.2.25", "fast-plural-rules": "1.0.2", "file-loader": "^6.2.0", @@ -215,7 +218,8 @@ "sass-loader": "^13.0.2", "selfsigned": "^2.0.1", "shelljs": "^0.8.5", - "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master", + "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master", + "storybook": "^7.0.20", "style-loader": "^3.3.1", "stylelint": "^14.9.1", "stylelint-config-recommended-scss": "^7.0.0", @@ -2978,7 +2982,7 @@ }, "node_modules/aframe": { "version": "1.0.3", - "resolved": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", "license": "MIT", "dependencies": { "custom-event-polyfill": "^1.0.6", @@ -3466,7 +3470,7 @@ }, "node_modules/bitecs": { "version": "0.3.38", - "resolved": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", "license": "MPL-2.0" }, "node_modules/body-parser": { @@ -9749,7 +9753,7 @@ }, "node_modules/three": { "version": "0.141.0", - "resolved": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", "license": "MIT" }, "node_modules/thunky": { @@ -12740,8 +12744,8 @@ "requires": {} }, "aframe": { - "version": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", - "from": "aframe@github:hubs-foundation/aframe#hubs/master", + "version": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", + "from": "aframe@github:Hubs-Foundation/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", "debug": "ngokevin/debug#noTimestamp", @@ -13107,8 +13111,8 @@ "dev": true }, "bitecs": { - "version": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", - "from": "bitecs@github:hubs-foundation/bitECS#hubs-patches" + "version": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", + "from": "bitecs@github:Hubs-Foundation/bitECS#hubs-patches" }, "body-parser": { "version": "1.20.1", @@ -15117,28 +15121,27 @@ "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.0", "@iarna/toml": "^2.2.5", - "@mozilla/lilypad-ui": "^1.8.3", + "@mozilla/lilypad-ui": "1.8.6", "@mozillareality/easing-functions": "^0.1.1", "@popperjs/core": "^2.4.4", - "@storybook/addon-actions": "^6.5.9", - "@storybook/addon-essentials": "^6.5.9", - "@storybook/addon-links": "^6.5.9", - "@storybook/builder-webpack5": "^6.5.9", - "@storybook/manager-webpack5": "^6.5.9", - "@storybook/react": "^6.5.9", - "@storybook/storybook-deployer": "^2.8.12", + "@storybook/addon-actions": "^7.0.20", + "@storybook/addon-essentials": "^7.0.20", + "@storybook/addon-links": "^7.0.20", + "@storybook/react": "^7.0.20", + "@storybook/react-webpack5": "^7.0.20", + "@storybook/storybook-deployer": "^2.8.16", "@svgr/webpack": "^6.3.1", "@types/three": "^0.141.0", "@types/webxr": "^0.5.0", "acorn": "^8.8.0", - "aframe": "github:hubs-foundation/aframe#hubs/master", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", - "ammo.js": "github:hubs-foundation/ammo.js#hubs/master", - "animejs": "github:hubs-foundation/anime#hubs/master", + "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master", + "animejs": "github:Hubs-Foundation/anime#hubs/master", "ava": "^4.3.1", "babel-loader": "^8.2.5", "babel-plugin-react-intl": "^8.2.21", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "buffered-interpolation": "github:Infinitelee/buffered-interpolation", "classnames": "^2.2.5", "color": "^3.1.2", @@ -15153,13 +15156,13 @@ "dotenv": "^16.0.1", "downshift": "^6.0.5", "draft-js": "^0.11.7", - "emoji-mart": "^5.5.2", + "emoji-picker-react": "^4.4.9", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-storybook": "^0.6.1", + "eslint-plugin-storybook": "^0.6.12", "esm": "^3.2.25", "event-target-shim": "^3.0.1", "fast-plural-rules": "1.0.2", @@ -15176,20 +15179,22 @@ "html-webpack-plugin": "^5.5.0", "html2canvas": "^1.0.0-rc.7", "htmlhint": "^1.1.4", + "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon", + "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon", "internal-ip": "^7.0.0", "js-cookie": "^2.2.0", "jsdom": "^20.0.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", - "lib-hubs": "github:hubs-foundation/lib-hubs#master", + "lib-hubs": "github:Hubs-Foundation/lib-hubs#master", "linkify-it": "^2.0.3", "localstorage-memory": "^1.0.3", "markdown-it": "^12.3.2", "mediasoup-client": "^3.6.54", "mini-css-extract-plugin": "^2.6.1", "moving-average": "^1.0.0", - "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", - "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master", + "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", + "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master", "node-ensure": "0.0.0", "node-fetch": "^2.6.7", "normalize.css": "^8.0.1", @@ -15228,14 +15233,16 @@ "selfsigned": "^2.0.1", "semver": "^7.3.2", "shelljs": "^0.8.5", - "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master", + "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master", + "storybook": "^7.0.20", + "stream-browserify": "^3.0.0", "style-loader": "^3.3.1", "stylelint": "^14.9.1", "stylelint-config-recommended-scss": "^7.0.0", "stylelint-scss": "^4.3.0", "tar": "^6.1.11", - "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", - "three-ammo": "github:hubs-foundation/three-ammo", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "three-ammo": "github:Hubs-Foundation/three-ammo", "three-gltf-extensions": "^0.0.14", "three-mesh-bvh": "^0.3.7", "three-pathfinding": "^1.1.0", @@ -15243,6 +15250,7 @@ "troika-three-text": "^0.45.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", + "url": "^0.11.1", "url-loader": "^4.1.1", "use-clipboard-copy": "^0.2.0", "uuid": "^3.2.1", @@ -17985,8 +17993,8 @@ } }, "three": { - "version": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", - "from": "three@github:hubs-foundation/three.js#hubs-patches-141" + "version": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", + "from": "three@github:Hubs-Foundation/three.js#hubs-patches-141" }, "thunky": { "version": "1.1.0", diff --git a/admin/package.json b/admin/package.json index 5055a08d7f..d3b250a6b1 100644 --- a/admin/package.json +++ b/admin/package.json @@ -22,15 +22,15 @@ "dependencies": { "@iarna/toml": "^2.2.3", "@mozilla/lilypad-ui": "^1.8.2", - "aframe": "github:hubs-foundation/aframe#hubs/master", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "classnames": "^2.2.5", "hubs": "file:..", "react": "^16.1.1", "react-admin": "^2.6.3", "react-dom": "^16.1.1", "react-intl": "^2.4.0", - "three": "github:hubs-foundation/three.js#hubs-patches-141" + "three": "github:Hubs-Foundation/three.js#hubs-patches-141" }, "devDependencies": { "@babel/core": "^7.18.9", diff --git a/admin/src/admin.js b/admin/src/admin.js index 2be64b9e77..289b2b0dd8 100644 --- a/admin/src/admin.js +++ b/admin/src/admin.js @@ -36,11 +36,11 @@ import { AutoEndSessionDialog } from "./react-components/auto-end-session-dialog import registerTelemetry from "hubs/src/telemetry"; import { createMuiTheme, withStyles } from "@material-ui/core/styles"; import { UnauthorizedPage } from "./react-components/unauthorized"; -import { store } from "hubs/src/utils/store-instance"; +import { getStore } from "hubs/src/utils/store-instance"; const qs = new URLSearchParams(location.hash.split("?")[1]); -window.APP = { store }; +window.APP = { store: getStore() }; registerTelemetry("/admin", "Hubs Admin"); @@ -185,6 +185,7 @@ const mountUI = async (retPhxChannel, customRoutes, layout) => { let permsTokenRefreshInterval; + const store = APP.store; if (configs.POSTGREST_SERVER) { dataProvider = postgrestClient(configs.POSTGREST_SERVER); authProvider = postgrestAuthenticatior.createAuthProvider(retPhxChannel); @@ -234,6 +235,7 @@ const HiddenAppBar = withStyles({ document.addEventListener("DOMContentLoaded", async () => { const socket = await connectToReticulum(); + const store = APP.store; if (store.state && store.state.credentials && store.state.credentials.token) { setItaAuthToken(store.state.credentials.token); try { diff --git a/admin/src/react-components/service-editor.js b/admin/src/react-components/service-editor.js index 4ff7a3b19f..24b1e5927c 100644 --- a/admin/src/react-components/service-editor.js +++ b/admin/src/react-components/service-editor.js @@ -24,7 +24,7 @@ import LinearProgress from "@material-ui/core/LinearProgress"; import clsx from "classnames"; import { Title } from "react-admin"; import theme from "../utils/sample-theme"; -import { store } from "hubs/src/utils/store-instance"; +import { getStore } from "hubs/src/utils/store-instance"; import withCommonStyles from "../utils/with-common-styles"; import { getEditableConfig, @@ -649,6 +649,7 @@ const AppConfigEditor = withStyles(styles)( class AppConfigEditor extends ConfigurationEditor { constructor(props) { super(props); + const store = getStore(); if (store.state && store.state.credentials && store.state.credentials.token) { AppConfigUtils.setAuthToken(store.state.credentials.token); } diff --git a/doc/add-ons.md b/doc/add-ons.md new file mode 100644 index 0000000000..10bcaffc34 --- /dev/null +++ b/doc/add-ons.md @@ -0,0 +1,104 @@ +# Hubs client Add-ons + +Hubs client add-ons are pluggable libraries that are installed as part of the client and loaded at runtime. Add-ons allow functionality to the Hubs core and allow easy Hubs client extensibility while maintaining the Hubs client core lean and minimal. + +## Installation + +Installing an addon in your Hubs client is a two step process: + +1. You'll need to install the add-on package in your client. You can do that using the `npm install` as with any other npm package. + For example if we would want to install the portals add-on we would need to do: + +``` +npm i https://github.com/MozillaReality/hubs-portals-addon.git +``` + +2. After the add-on is installed you'll need to add it to the addons array inside the `addons.json` file at the root of the Hubs client source. + +``` +{ + "addons": [ + ... + "hubs-portals-addon", + ... + ] +} +``` + +Now you can build your client and run it as usual. + +## Configuration + +To configure your add-ons you need to update the add-on configuration JSON in the Admin console. Open your admin console and go to `App Settings -> Features`. + +**Important Note**: Add-on require the bitECS based loader so you'll need to enable it for add-ons to work. + +Update the add-ons JSON config with your add-ons configuration: + +

+ +

+ +There are currently two properties supported in the add-ons configuration: + +- **enabled**: Determines if the add-on is enabled by default in the instance rooms. +- **config**: A free form JSON containing the add-on configuration. This JSON will be passed as is to the add-on during the add-on initialization. See each add-on docs to know what JSON properties the add-on supports if any. + +Once your configuration is saved, you can create or open a room in your instance and you should see add-ons running for that room. + +### Room add-ons configuration + +Add-ons can also be enabled/disabled per room independently from the instance configuration. To override the instance configuration you can go to your room settings and change the add-ons enabled/disabled configuration there. The room add-ons configuration will take precedence over the instance configuration if it has been changed at least once. + +

+ +

+ +## Add-on development + +You can develop Hubs add-ons using Javascript or Typescript. + +If you are developing using Typescript you can use the add-on template as a starting point. You can get the Add-on template from [here](https://github.com/MozillaReality/hubs-template-addon). + +The add-on template has the basic dependencies already configured to get started with development as fast as possible. + +The easiest way of iterating over an add-on development is by linking the add-on package source from the client. + +1. Create a global link for the add-on. Go to the add-ons source folder and do: + + `npm link` + + Other related commands: + + - To see all the global package links: + + `npm ls --link --global` + + - To remove a global linked package: + + `npm unlink -g` + +2. Go to the Hubs client root and add a link to the add-on package: + + `npm link [package-id]` + + Other related commands: + + - You can see the currently linked packages with: + + `npm ls --link` + + - To remove a linked package: + + `npm unlink --no-save [package-id]` + +Now you can build the client and the add-on should be bundled as part of the client code. + +There is a typings library available for Typescript development: [Hubs Client TS Types](https://github.com/MozillaReality/hubs-ts-types) + +## Currently available add-ons + +- [Template add-on](https://github.com/MozillaReality/hubs-template-addon). This add-on serves as a foundation for add-on development. +- [Duck add-on](https://github.com/MozillaReality/hubs-duck-addon): Replaces the existing `/duck` chat command and refactors it into a Hubs add-on. +- [Portals add-on](https://github.com/MozillaReality/hubs-portals-addon): Simple portals implementation as an add-on that lets you spawn portals using the `/portal`. It also show how to add key bindings. +- [Behavior Graphs add-on](https://github.com/MozillaReality/hubs-behavior-graphs-addon/): Initial Behavior Graphs implementation as an add-on diff --git a/doc/img/addons-admin-config.png b/doc/img/addons-admin-config.png new file mode 100644 index 0000000000..5642a73f4a Binary files /dev/null and b/doc/img/addons-admin-config.png differ diff --git a/doc/img/addons-room-config.png b/doc/img/addons-room-config.png new file mode 100644 index 0000000000..949f36b3c2 Binary files /dev/null and b/doc/img/addons-room-config.png differ diff --git a/package-lock.json b/package-lock.json index 9fdd0440c2..7889e7bb88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,11 @@ "@mozilla/lilypad-ui": "1.8.6", "@mozillareality/easing-functions": "^0.1.1", "@popperjs/core": "^2.4.4", - "aframe": "github:hubs-foundation/aframe#hubs/master", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", - "ammo.js": "github:hubs-foundation/ammo.js#hubs/master", - "animejs": "github:hubs-foundation/anime#hubs/master", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master", + "animejs": "github:Hubs-Foundation/anime#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "buffered-interpolation": "github:Infinitelee/buffered-interpolation", "classnames": "^2.2.5", "color": "^3.1.2", @@ -46,15 +46,19 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "hubs-behavior-graphs-addon": "github:Hubs-Foundation/hubs-behavior-graphs-addon#main", + "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon#main", + "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon#main", + "hubs-postprocessing-addon": "github:Hubs-Foundation/hubs-postprocessing-addon#main", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", - "lib-hubs": "github:hubs-foundation/lib-hubs#master", + "lib-hubs": "github:Hubs-Foundation/lib-hubs#master", "linkify-it": "^2.0.3", "markdown-it": "^12.3.2", "moving-average": "^1.0.0", - "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", - "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master", + "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", + "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master", "node-ensure": "0.0.0", "normalize.css": "^8.0.1", "pdfjs-dist": "^2.14.305", @@ -79,8 +83,8 @@ "sdp-transform": "^2.14.1", "semver": "^7.3.2", "stream-browserify": "^3.0.0", - "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", - "three-ammo": "github:hubs-foundation/three-ammo", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "three-ammo": "github:Hubs-Foundation/three-ammo", "three-gltf-extensions": "^0.0.14", "three-mesh-bvh": "^0.3.7", "three-pathfinding": "^1.1.0", @@ -159,7 +163,7 @@ "sass-loader": "^13.0.2", "selfsigned": "^2.0.1", "shelljs": "^0.8.5", - "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master", + "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master", "storybook": "^7.0.20", "style-loader": "^3.3.1", "stylelint": "^14.9.1", @@ -179,6 +183,23 @@ "fsevents": "^2.2.1" } }, + "../../../hubs-postprocessing-addon": { + "version": "1.0.0", + "extraneous": true, + "license": "MPL-2.0", + "devDependencies": { + "@types/hubs": "github:Hubs-Foundation/hubs-ts-types#d9f56d541f3cf6b8fd360ae96652bf47941f8894", + "@types/node": "^18.15.0", + "@types/three": "^0.141.0", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", + "postprocessing": "~6.28.7", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "ts-loader": "^9.5.1", + "typescript": "^5.3.3", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -3744,6 +3765,14 @@ "node": ">= 8" } }, + "node_modules/@oveddan-behave-graph/core": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@oveddan-behave-graph/core/-/core-0.11.1.tgz", + "integrity": "sha512-HH6hVYHIjAs4KgrOhg4UoMeXwqiu+DRSU9UI/lQsN4HyGky0cLrzsHMtQxZKLTBl9dGdZIXjlrIBFAepYcMivQ==", + "dependencies": { + "three-stdlib": "^2.21.5" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -9251,6 +9280,11 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==" + }, "node_modules/@types/ejs": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.2.tgz", @@ -9476,6 +9510,11 @@ "integrity": "sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==", "dev": true }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -9598,8 +9637,7 @@ "node_modules/@types/webxr": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", - "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==", - "dev": true + "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==" }, "node_modules/@types/ws": { "version": "8.5.4", @@ -10039,7 +10077,7 @@ }, "node_modules/aframe": { "version": "1.0.3", - "resolved": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79", "license": "MIT", "dependencies": { "custom-event-polyfill": "^1.0.6", @@ -10167,11 +10205,11 @@ }, "node_modules/ammo.js": { "version": "0.0.2", - "resolved": "git+ssh://git@github.com/hubs-foundation/ammo.js.git#a38109e87e300c820da14c9615be379caf03d77a" + "resolved": "git+ssh://git@github.com/Hubs-Foundation/ammo.js.git#a38109e87e300c820da14c9615be379caf03d77a" }, "node_modules/animejs": { "version": "3.0.1", - "resolved": "git+ssh://git@github.com/hubs-foundation/anime.git#883d028420c7eb8c2934290e5d10827be4640318", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/anime.git#883d028420c7eb8c2934290e5d10827be4640318", "license": "MIT" }, "node_modules/ansi-align": { @@ -11069,7 +11107,7 @@ }, "node_modules/bitecs": { "version": "0.3.38", - "resolved": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e", "license": "MPL-2.0" }, "node_modules/bl": { @@ -12615,40 +12653,6 @@ "postcss": "^8.1.0" } }, - "node_modules/css-loader/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/css-loader/node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/css-loader/node_modules/postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", @@ -13399,6 +13403,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + }, "node_modules/draft-js": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", @@ -14787,6 +14796,11 @@ "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", "dev": true }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" + }, "node_modules/figures": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/figures/-/figures-4.0.1.tgz", @@ -16460,6 +16474,53 @@ "node": ">= 6" } }, + "node_modules/hubs-behavior-graphs-addon": { + "version": "0.0.1", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-behavior-graphs-addon.git#main", + "integrity": "sha512-ZNQJzTSuJihiGAONdi8KAFoVi8qZxkG4RLAwS3IUJi2sP6N0qTBCtItBr37TygN3AgLP4KY32sFkP3jc9MMAkA==", + "license": "MPL-2.0", + "dependencies": { + "@oveddan-behave-graph/core": "^0.11.1" + }, + "peerDependencies": { + "aframe": "github:Hubs-Foundation/aframe#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", + "react": "^18.2.0", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "troika-three-text": "^0.45.0" + } + }, + "node_modules/hubs-duck-addon": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-duck-addon.git#main", + "integrity": "sha512-1mAnkCPudr+jG9w4qG7zKPl7mA5PvhQ7/IZ1bUMt0SCDz6n7ZR7hOvZ0ewQtqXz4NLSU+RGgw2BlVFo3JLQTWg==", + "license": "MPL-2.0", + "peerDependencies": { + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777" + } + }, + "node_modules/hubs-portals-addon": { + "version": "1.0.1", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-portals-addon.git#main", + "integrity": "sha512-Pwse1rxVOw+uw1+uS2lNXak4c7b1yA6ucDFhD/d4EiX5lnWPSOkSq+Ljs6Cv02bhFO5eWvOrNKlo6REZLXU1LQ==", + "license": "MPL-2.0", + "peerDependencies": { + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777" + } + }, + "node_modules/hubs-postprocessing-addon": { + "version": "1.0.2", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-postprocessing-addon.git#main", + "integrity": "sha512-4qBFYY6eW0nLKH8NLxefSORbRcGdHrei6d3BQYknd4o6Nnelu74CfGaKil8k2Y8fLBq4TBqK9ZD85lpsTEc/iA==", + "license": "MPL-2.0", + "peerDependencies": { + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", + "postprocessing": "~6.28.7", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -18143,12 +18204,6 @@ "shell-quote": "^1.7.3" } }, - "node_modules/launch-editor/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -18173,7 +18228,7 @@ }, "node_modules/lib-hubs": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/hubs-foundation/lib-hubs.git#9430db2a06cc63c6f91319c47393fea1dca42d43", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/lib-hubs.git#9430db2a06cc63c6f91319c47393fea1dca42d43", "workspaces": [ "packages/*" ] @@ -19152,9 +19207,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -19192,7 +19247,7 @@ }, "node_modules/networked-aframe": { "version": "0.6.1", - "resolved": "git+ssh://git@github.com/hubs-foundation/networked-aframe.git#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/networked-aframe.git#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", "integrity": "sha512-T8wzMgtLYuFqWRx2cTxadNzDECir2x2gdJlV4TQTAwpZALo0PKyPie5VX3rjEIL9l4/5N3yK+dCtRW70NuUfhg==", "license": "MIT", "dependencies": { @@ -19214,7 +19269,7 @@ }, "node_modules/nipplejs": { "version": "0.6.8", - "resolved": "git+ssh://git@github.com/hubs-foundation/nipplejs.git#7b5f953f75df28d42689e96c6a8342ab0a3cb595", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/nipplejs.git#7b5f953f75df28d42689e96c6a8342ab0a3cb595", "license": "MIT" }, "node_modules/no-case": { @@ -20117,6 +20172,12 @@ "websocket": "^1.0.24" } }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "devOptional": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -20284,6 +20345,34 @@ "node": ">=10" } }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -20316,16 +20405,21 @@ "dev": true }, "node_modules/postprocessing": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.31.0.tgz", - "integrity": "sha512-h1g2KDVrTS6QB4AHP55opp8FYzq66jJHh4JIFCptaj283RUX1y/tPkv8FBB2oK4WYrdPgqvElnKrXZwgiLWeHQ==", + "version": "6.28.7", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.28.7.tgz", + "integrity": "sha512-norsBlNIhDoNG2R0nki9C0bNEARIunE/s5W/4qH/Ozlg3m0dOOmBoY8JZhnmPild/+Jvq/UFEeHQRBvTc11WUg==", "engines": { "node": ">= 0.13.2" }, "peerDependencies": { - "three": ">= 0.138.0 < 0.153.0" + "three": ">= 0.125.0 < 0.145.0" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, "node_modules/prefix-style": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz", @@ -22446,9 +22540,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -22544,7 +22638,7 @@ }, "node_modules/spritesheet-js": { "version": "1.2.6", - "resolved": "git+ssh://git@github.com/hubs-foundation/spritesheet.js.git#edce7f45d9f1f5997850fba0d0034a44fceda8c5", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/spritesheet.js.git#edce7f45d9f1f5997850fba0d0034a44fceda8c5", "dev": true, "license": "MIT", "dependencies": { @@ -23026,42 +23120,6 @@ "stylelint": "^14.4.0" } }, - "node_modules/stylelint-config-recommended-scss/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true, - "peer": true - }, - "node_modules/stylelint-config-recommended-scss/node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/stylelint-config-recommended-scss/node_modules/postcss-scss": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", @@ -23249,40 +23307,6 @@ "node": ">=10" } }, - "node_modules/stylelint/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/stylelint/node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/stylelint/node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -23480,12 +23504,6 @@ "node": ">= 10" } }, - "node_modules/svgo/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -23947,13 +23965,13 @@ }, "node_modules/three": { "version": "0.141.0", - "resolved": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777", "integrity": "sha512-oqsJ2XGLH1ki2w/pjyt5nmmpQVw71s1Quo9CK4U3eYVQfe/a12tYKy8rcgUC8BU58eO8lP4ng1B50s4O9p5q6A==", "license": "MIT" }, "node_modules/three-ammo": { "version": "1.0.12", - "resolved": "git+ssh://git@github.com/hubs-foundation/three-ammo.git#26d445f13a63690271cdf385157ac91e269a744e", + "resolved": "git+ssh://git@github.com/Hubs-Foundation/three-ammo.git#26d445f13a63690271cdf385157ac91e269a744e", "license": "MPL-2.0", "peerDependencies": { "ammo.js": "*", @@ -23981,6 +23999,22 @@ "three": "0.x.x" } }, + "node_modules/three-stdlib": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.30.0.tgz", + "integrity": "sha512-ALL7rn57jq/MovDRk5hGjeWCvOeZlZhFCWIdpbBAQNudCO3nMwxEba5ZulsMgiI1ymQMzUzTMcxhLTCVlUaKDw==", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, "node_modules/three-to-ammo": { "version": "1.0.1", "resolved": "git+ssh://git@github.com/infinitelee/three-to-ammo.git#2364f7ce5e4b6b622f29a8a4f8b467b83aa36293", @@ -24664,12 +24698,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/update-browserslist-db/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "devOptional": true - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 829ad83314..1bb0c3260a 100644 --- a/package.json +++ b/package.json @@ -80,11 +80,11 @@ "@mozilla/lilypad-ui": "1.8.6", "@mozillareality/easing-functions": "^0.1.1", "@popperjs/core": "^2.4.4", - "aframe": "github:hubs-foundation/aframe#hubs/master", + "aframe": "github:Hubs-Foundation/aframe#hubs/master", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", - "ammo.js": "github:hubs-foundation/ammo.js#hubs/master", - "animejs": "github:hubs-foundation/anime#hubs/master", - "bitecs": "github:hubs-foundation/bitECS#hubs-patches", + "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master", + "animejs": "github:Hubs-Foundation/anime#hubs/master", + "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches", "buffered-interpolation": "github:Infinitelee/buffered-interpolation", "classnames": "^2.2.5", "color": "^3.1.2", @@ -102,15 +102,19 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "hubs-behavior-graphs-addon": "github:Hubs-Foundation/hubs-behavior-graphs-addon#main", + "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon#main", + "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon#main", + "hubs-postprocessing-addon": "github:Hubs-Foundation/hubs-postprocessing-addon#main", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", - "lib-hubs": "github:hubs-foundation/lib-hubs#master", + "lib-hubs": "github:Hubs-Foundation/lib-hubs#master", "linkify-it": "^2.0.3", "markdown-it": "^12.3.2", "moving-average": "^1.0.0", - "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", - "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master", + "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479", + "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master", "node-ensure": "0.0.0", "normalize.css": "^8.0.1", "pdfjs-dist": "^2.14.305", @@ -135,8 +139,8 @@ "sdp-transform": "^2.14.1", "semver": "^7.3.2", "stream-browserify": "^3.0.0", - "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", - "three-ammo": "github:hubs-foundation/three-ammo", + "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "three-ammo": "github:Hubs-Foundation/three-ammo", "three-gltf-extensions": "^0.0.14", "three-mesh-bvh": "^0.3.7", "three-pathfinding": "^1.1.0", @@ -215,7 +219,7 @@ "sass-loader": "^13.0.2", "selfsigned": "^2.0.1", "shelljs": "^0.8.5", - "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master", + "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master", "storybook": "^7.0.20", "style-loader": "^3.3.1", "stylelint": "^14.9.1", diff --git a/src/addons.ts b/src/addons.ts new file mode 100644 index 0000000000..85d1eaa3ab --- /dev/null +++ b/src/addons.ts @@ -0,0 +1,381 @@ +import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader"; +import { App, HubsWorld } from "./app"; +import { prefabs } from "./prefabs/prefabs"; + +import { + InflatorConfigT, + SystemConfigT, + SystemOrderE, + PrefabConfigT, + NetworkSchemaConfigT, + ChatCommandConfigT, + PreferenceConfigT, + PreferencePrefsScreenItemT, + PreferencePrefsScreenCategory, + PreferenceScreenLabelT, + PreferenceScreenDefT, + PreferenceDefConfigT, + PostProcessOrderE +} from "./types"; +import configs from "./utils/configs"; +import { commonInflators, gltfInflators, jsxInflators } from "./utils/jsx-entity"; +import { networkableComponents, schemas } from "./utils/network-schemas"; +import { gltfPluginsExtra } from "./components/gltf-model-plus"; +import { GLTFLinkResolverFn, gltfLinkResolvers } from "./inflators/model"; +import { Object3D } from "three"; +import { extraSections } from "./react-components/debug-panel/ECSSidebar"; +import { shouldUseNewLoader } from "./hubs"; +import { SCHEMA } from "./storage/store"; +import { Pass } from "postprocessing"; + +function getNextIdx(slot: Array, system: SystemConfigT) { + return slot.findIndex(item => { + item.order > system.order; + }); +} + +function registerSystem(system: SystemConfigT) { + let slot = APP.addon_systems.prePhysics; + if (system.order < SystemOrderE.PrePhysics) { + slot = APP.addon_systems.setup; + } else if (system.order < SystemOrderE.PostPhysics) { + slot = APP.addon_systems.prePhysics; + } else if (system.order < SystemOrderE.BeforeMatricesUpdate) { + slot = APP.addon_systems.postPhysics; + } else if (system.order < SystemOrderE.BeforeRender) { + slot = APP.addon_systems.postPhysics; + } else if (system.order < SystemOrderE.AfterRender) { + slot = APP.addon_systems.beforeRender; + } else { + slot = APP.addon_systems.afterRender; + } + const nextIdx = getNextIdx(slot, system); + slot.splice(nextIdx, 0, system); +} + +function registerInflator(inflator: InflatorConfigT) { + if (inflator.common) { + commonInflators[inflator.common.id] = inflator.common.inflator; + } else { + if (inflator.jsx) { + jsxInflators[inflator.jsx.id] = inflator.jsx.inflator; + } + if (inflator.gltf) { + gltfInflators[inflator.gltf.id] = inflator.gltf.inflator; + } + } +} + +function registerPrefab(prefab: PrefabConfigT) { + if (prefabs.has(prefab.id)) { + throw Error(`Error registering prefab ${name}: prefab already registered`); + } + prefabs.set(prefab.id, prefab.config); +} + +function registerNetworkSchema(schemaConfig: NetworkSchemaConfigT) { + if (schemas.has(schemaConfig.component)) { + throw Error( + `Error registering network schema ${schemaConfig.schema.componentName}: network schema already registered` + ); + } + schemas.set(schemaConfig.component, schemaConfig.schema); + networkableComponents.push(schemaConfig.component); +} + +function registerChatCommand(command: ChatCommandConfigT) { + APP.messageDispatch.registerChatCommand(command.id, command.command); +} + +export type AddonIdT = string; +export type AddonNameT = string; +export type AddonDescriptionT = string; +export type AddonOnLoadedFn = () => void; +export type AddonOnReadyFn = (app: App, config?: JSON) => void; + +export interface InternalAddonConfigT { + name: AddonNameT; + description?: AddonDescriptionT; + onLoaded?: AddonOnLoadedFn; + onReady?: AddonOnReadyFn; + system?: SystemConfigT | SystemConfigT[]; + inflator?: InflatorConfigT | InflatorConfigT[]; + prefab?: PrefabConfigT | PrefabConfigT[]; + networkSchema?: NetworkSchemaConfigT | NetworkSchemaConfigT[]; + chatCommand?: ChatCommandConfigT | ChatCommandConfigT[]; + preference?: PreferenceConfigT | PreferenceConfigT[]; + enabled?: boolean; + config?: JSON | undefined; +} +type AddonConfigT = Omit; +export type AdminAddonConfig = { + enabled: boolean; + config: JSON; +}; + +const pendingAddons = new Map(); +export const addons = new Map(); +export type AddonRegisterCallbackT = (app: App) => void; +export function registerAddon(id: AddonIdT, config: AddonConfigT) { + console.log(`Add-on ${id} registered`); + pendingAddons.set(id, config); + registerPreferences(id, config); + if (config.onLoaded) { + config.onLoaded(); + } +} + +export type GLTFParserCallbackFn = (parser: GLTFParser) => GLTFLoaderPlugin; +export function registerGLTFLoaderPlugin(callback: GLTFParserCallbackFn): void { + gltfPluginsExtra.push(callback); +} +export function registerGLTFLinkResolver(resolver: GLTFLinkResolverFn): void { + gltfLinkResolvers.push(resolver); +} +export function registerECSSidebarSection(section: (world: HubsWorld, selectedObj: Object3D) => React.JSX.Element) { + extraSections.push(section); +} + +const screenPreferencesDefs = new Map(); +export function getAddonsPreferencesDefs(): PreferenceScreenDefT { + return screenPreferencesDefs; +} + +const screenPreferencesLabels = new Map(); +export function getAddonsPreferencesLabels(): PreferenceScreenLabelT { + return screenPreferencesLabels; +} + +let xFormedScreenPreferencesCategories: Map; +const screenPreferencesCategories = new Map(); +export function getAddonsPreferencesCategories(app: App): PreferencePrefsScreenCategory { + // We need to transform on the spot as when the preferences are added we don't yet have the hub user_data + // to know what addons are enabled in the room + if (!xFormedScreenPreferencesCategories) { + xFormedScreenPreferencesCategories = new Map(); + screenPreferencesCategories.forEach((categories, addonId) => { + if (isAddonActive(app, addonId)) { + const config = addons.get(addonId); + xFormedScreenPreferencesCategories.set(config?.name || addonId, categories); + } + }); + return xFormedScreenPreferencesCategories; + } else { + return xFormedScreenPreferencesCategories; + } +} + +function registerPreferences(addonId: string, addonConfig: AddonConfigT) { + const prefSchema = SCHEMA.definitions.preferences.properties; + function register(preference: PreferenceConfigT) { + for (const key in preference) { + if (!(key in prefSchema)) { + const prefDef = preference[key].prefDefinition; + if (key in prefSchema) { + throw new Error(`Preference ${key} already exists`); + } + (prefSchema as any)[key] = prefDef; + screenPreferencesDefs.set(key, prefDef); + + const prefConfig = preference[key]; + let categoryPrefs: PreferencePrefsScreenItemT[]; + if (screenPreferencesCategories.has(addonId)) { + categoryPrefs = screenPreferencesCategories.get(addonId)!; + } else { + categoryPrefs = new Array(); + screenPreferencesCategories.set(addonId, categoryPrefs); + } + categoryPrefs.push({ key, ...prefConfig.prefConfig }); + screenPreferencesLabels.set(key, prefConfig.prefConfig.description); + } else { + throw new Error("Preference already exists"); + } + } + } + + if (addonConfig.preference) { + if (Array.isArray(addonConfig.preference)) { + addonConfig.preference.forEach(preference => { + register(preference); + }); + } else { + register(addonConfig.preference); + } + } +} + +const fxOrder2Passes = { + [PostProcessOrderE.AfterScene]: 0, + [PostProcessOrderE.AfterBloom]: 0, + [PostProcessOrderE.AfterUI]: 0, + [PostProcessOrderE.AfterAA]: 0 +}; +const fx2Order = new Map(); +function afterSceneIdx() { + return 2 + fxOrder2Passes[PostProcessOrderE.AfterScene]; +} +function afterBloomIdx(app: App) { + let idx = afterSceneIdx(); + idx += fxOrder2Passes[PostProcessOrderE.AfterBloom]; + if (app.fx.bloomAndTonemapPass) { + idx++; + } + return idx; +} +function afterUIIdx(app: App) { + let idx = afterBloomIdx(app); + idx += fxOrder2Passes[PostProcessOrderE.AfterUI]; + return idx; +} +function afterAAIdx(app: App) { + let idx = afterUIIdx(app); + idx += fxOrder2Passes[PostProcessOrderE.AfterAA]; + return idx; +} +function getPassIdx(app: App, order: PostProcessOrderE) { + switch (order) { + case PostProcessOrderE.AfterScene: + return afterSceneIdx(); + case PostProcessOrderE.AfterBloom: + return afterBloomIdx(app); + case PostProcessOrderE.AfterUI: + return afterUIIdx(app); + case PostProcessOrderE.AfterAA: + return afterAAIdx(app); + } +} +export function registerPass(app: App, pass: Pass | Pass[], order: PostProcessOrderE) { + function register(pass: Pass) { + const nextIdx = getPassIdx(app, order) + 1; + fx2Order.set(pass, order); + fxOrder2Passes[order]++; + app.fx.composer?.addPass(pass, nextIdx); + } + + if (Array.isArray(pass)) { + pass.every(pass => register(pass)); + } else { + register(pass); + } +} + +export function unregisterPass(app: App, pass: Pass | Pass[]) { + function unregister(pass: Pass) { + if (fx2Order.has(pass)) { + const order = fx2Order.get(pass)!; + fxOrder2Passes[order]--; + app.fx.composer?.removePass(pass); + fx2Order.delete(pass); + } + } + + if (Array.isArray(pass)) { + pass.every(pass => unregister(pass)); + } else { + unregister(pass); + } +} + +export function getAddonConfig(id: string): AdminAddonConfig { + const adminAddonsConfig = configs.feature("addons_config"); + let adminAddonConfig = { + enabled: false, + config: {} as JSON + }; + if (adminAddonsConfig && id in adminAddonsConfig) { + adminAddonConfig = adminAddonsConfig[id]; + } + return adminAddonConfig; +} + +export function isAddonEnabled(app: App, id: string): boolean { + let enabled = false; + if (app.hub?.user_data && "addons" in app.hub?.user_data && id in app.hub.user_data["addons"]) { + enabled = app.hub.user_data.addons[id]; + } else { + const adminAddonsConfig = getAddonConfig(id); + if (adminAddonsConfig) { + enabled = adminAddonsConfig.enabled; + } + } + return enabled; +} + +function isAddonActive(app: App, id: string): boolean { + if (shouldUseNewLoader()) { + return isAddonEnabled(app, id); + } + return false; +} + +export function onAddonsInit(app: App) { + app.scene?.addEventListener("hub_updated", () => { + for (const [id, addon] of pendingAddons) { + if (addons.has(id)) { + throw Error(`Addon ${id} already registered`); + } else { + addons.set(id, addon); + } + + if (!isAddonActive(app, id)) { + continue; + } + + if (addon.prefab) { + if (Array.isArray(addon.prefab)) { + addon.prefab.forEach(prefab => { + registerPrefab(prefab); + }); + } else { + registerPrefab(addon.prefab); + } + } + + if (addon.networkSchema) { + if (Array.isArray(addon.networkSchema)) { + addon.networkSchema.forEach(networkSchema => { + registerNetworkSchema(networkSchema); + }); + } else { + registerNetworkSchema(addon.networkSchema); + } + } + + if (addon.inflator) { + if (Array.isArray(addon.inflator)) { + addon.inflator.forEach(inflator => { + registerInflator(inflator); + }); + } else { + registerInflator(addon.inflator); + } + } + + if (addon.system) { + if (Array.isArray(addon.system)) { + addon.system.forEach(system => { + registerSystem(system); + }); + } else { + registerSystem(addon.system); + } + } + + if (addon.chatCommand) { + if (Array.isArray(addon.chatCommand)) { + addon.chatCommand.forEach(chatCommand => { + registerChatCommand(chatCommand); + }); + } else { + registerChatCommand(addon.chatCommand); + } + } + + if (addon.onReady) { + const adminAddonConfig = getAddonConfig(id); + addon.onReady(app, adminAddonConfig.config); + } + } + pendingAddons.clear(); + }); +} diff --git a/src/app.ts b/src/app.ts index 16a03c475c..f238dcb685 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,6 @@ import { addEntity, createWorld, IWorld } from "bitecs"; import "./aframe-to-bit-components"; import { AEntity, Networked, Object3DTag, Owned } from "./bit-components"; import MediaSearchStore from "./storage/media-search-store"; -import Store from "./storage/store"; import qsTruthy from "./utils/qs_truthy"; import type { AComponent, AScene } from "aframe"; @@ -19,6 +18,7 @@ import { PositionalAudio, Scene, sRGBEncoding, + Texture, WebGLRenderer } from "three"; import { AudioSettings, SourceType } from "./components/audio-params"; @@ -27,9 +27,11 @@ import { DialogAdapter } from "./naf-dialog-adapter"; import { mainTick } from "./systems/hubs-systems"; import { waitForPreloads } from "./utils/preload"; import SceneEntryManager from "./scene-entry-manager"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; import { addObject3DComponent } from "./utils/jsx-entity"; import { ElOrEid } from "./utils/bit-utils"; +import { onAddonsInit } from "./addons"; +import { CoreSystemKeyT, HubsSystemKeyT, SystemConfigT, SystemKeyT, SystemT } from "./types"; declare global { interface Window { @@ -52,6 +54,7 @@ export interface HubsWorld extends IWorld { nid2eid: Map; eid2obj: Map; eid2mat: Map; + eid2tex: Map; time: { delta: number; elapsed: number; tick: number }; } @@ -63,7 +66,7 @@ export function getScene() { return promiseToScene; } -interface HubDescription { +export interface HubDescription { hub_id: string; user_data?: any; } @@ -75,7 +78,6 @@ export class App { mediaDevicesManager?: MediaDevicesManager; entryManager?: SceneEntryManager; messageDispatch?: any; - store: Store; componentRegistry: { [key: string]: AComponent[] }; mediaSearchStore = new MediaSearchStore(); @@ -104,6 +106,15 @@ export class App { dialog = new DialogAdapter(); + addon_systems = { + setup: new Array<{ order: number; system: SystemT }>(), + prePhysics: new Array<{ order: number; system: SystemT }>(), + postPhysics: new Array<{ order: number; system: SystemT }>(), + beforeMatricesUpdate: new Array<{ order: number; system: SystemT }>(), + beforeRender: new Array<{ order: number; system: SystemT }>(), + afterRender: new Array<{ order: number; system: SystemT }>() + }; + RENDER_ORDER = { HUD_BACKGROUND: 1, HUD_ICONS: 2, @@ -117,10 +128,10 @@ export class App { } = {}; constructor() { - this.store = store; // TODO: Create accessor / update methods for these maps / set this.world.eid2obj = new Map(); this.world.eid2mat = new Map(); + this.world.eid2tex = new Map(); this.world.nid2eid = new Map(); this.world.deletedNids = new Set(); @@ -159,6 +170,23 @@ export class App { return this.sid2str.get(sid); } + notifyOnInit() { + onAddonsInit(this); + } + + get store() { + return getStore(); + } + + getSystem(id: SystemKeyT) { + const systems = this.scene?.systems!; + if (id in systems) { + return systems[id as CoreSystemKeyT]; + } else { + return systems["hubs-systems"][id as HubsSystemKeyT]; + } + } + // This gets called by a-scene to setup the renderer, camera, and audio listener // TODO ideally the contorl flow here would be inverted, and we would setup this stuff, // initialize aframe, and then run our own RAF loop diff --git a/src/bit-components.js b/src/bit-components.js index 1bafeafa25..09d6bb53d8 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -21,7 +21,9 @@ export const Owned = defineComponent(); export const EntityStateDirty = defineComponent(); export const NetworkedMediaFrame = defineComponent({ capturedNid: Types.ui32, - scale: [Types.f32, 3] + scale: [Types.f32, 3], + flags: Types.ui8, + mediaType: Types.ui8 }); NetworkedMediaFrame.capturedNid[$isStringType] = true; @@ -36,6 +38,43 @@ export const MediaFrame = defineComponent({ previewingNid: Types.eid, flags: Types.ui8 }); +export const MediaRoot = defineComponent(); +export const NetworkedText = defineComponent({ + text: Types.ui8, + anchorX: Types.ui8, + anchorY: Types.ui8, + color: Types.ui32, + curveRadius: Types.f32, + direction: Types.ui8, + fillOpacity: Types.f32, + fontUrl: Types.ui8, + fontSize: Types.f32, + letterSpacing: Types.f32, + lineHeight: Types.ui8, + textAlign: Types.ui8, + outlineWidth: Types.ui8, + outlineColor: Types.ui32, + outlineBlur: Types.ui8, + outlineOffsetX: Types.ui8, + outlineOffsetY: Types.ui8, + outlineOpacity: Types.f32, + strokeWidth: Types.ui8, + strokeColor: Types.ui32, + strokeOpacity: Types.ui32, + textIndent: Types.ui32, + whiteSpace: Types.ui8, + overflowWrap: Types.ui8, + opacity: Types.f32, + side: Types.ui8, + maxWidth: Types.f32 +}); +NetworkedText.text[$isStringType] = true; +NetworkedText.lineHeight[$isStringType] = true; +NetworkedText.outlineWidth[$isStringType] = true; +NetworkedText.outlineBlur[$isStringType] = true; +NetworkedText.outlineOffsetX[$isStringType] = true; +NetworkedText.outlineOffsetY[$isStringType] = true; +NetworkedText.strokeWidth[$isStringType] = true; export const TextTag = defineComponent(); export const ReflectionProbe = defineComponent(); export const Slice9 = defineComponent({ @@ -59,7 +98,9 @@ export const SpotLightTag = defineComponent(); export const CursorRaycastable = defineComponent(); export const RemoteHoverTarget = defineComponent(); export const NotRemoteHoverTarget = defineComponent(); -export const Holdable = defineComponent(); +export const Holdable = defineComponent({ + flags: Types.ui8 +}); export const RemoveNetworkedEntityButton = defineComponent(); export const Interacted = defineComponent(); export const HandRight = defineComponent(); @@ -119,7 +160,11 @@ export const Rigidbody = defineComponent({ activationState: Types.ui8, collisionFilterGroup: Types.ui32, collisionFilterMask: Types.ui32, - flags: Types.ui8 + flags: Types.ui8, + initialCollisionFilterMask: Types.ui32 +}); +export const NetworkedRigidBody = defineComponent({ + prevType: Types.ui8 }); export const PhysicsShape = defineComponent({ bodyId: Types.ui16, @@ -270,9 +315,12 @@ export const LoopAnimation = defineComponent(); */ export const LoopAnimationData = new Map(); export const NetworkedVideo = defineComponent({ + src: Types.ui8, time: Types.f32, - flags: Types.ui8 + flags: Types.ui8, + projection: Types.ui8 }); +NetworkedVideo.src[$isStringType] = true; export const VideoMenuItem = defineComponent(); export const VideoMenu = defineComponent({ videoRef: Types.eid, @@ -387,6 +435,7 @@ export const Billboard = defineComponent({ onlyY: Types.ui8 }); export const MaterialTag = defineComponent(); +export const TextureTag = defineComponent(); export const UVScroll = defineComponent({ speed: [Types.f32, 2], increment: [Types.f32, 2], @@ -441,7 +490,6 @@ export const LinearScale = defineComponent({ targetY: Types.f32, targetZ: Types.f32 }); -export const Quack = defineComponent(); export const TrimeshTag = defineComponent(); export const HeightFieldTag = defineComponent(); export const LocalAvatar = defineComponent(); diff --git a/src/bit-systems/audio-emitter-system.ts b/src/bit-systems/audio-emitter-system.ts index a525fc3e02..43f8cd7dd3 100644 --- a/src/bit-systems/audio-emitter-system.ts +++ b/src/bit-systems/audio-emitter-system.ts @@ -6,6 +6,7 @@ import { AudioType, SourceType } from "../components/audio-params"; import { AudioSystem } from "../systems/audio-system"; import { applySettings, getCurrentAudioSettings, updateAudioSettings } from "../update-audio-settings"; import { addObject3DComponent, swapObject3DComponent } from "../utils/jsx-entity"; +import { EntityID } from "../utils/networking-types"; export type AudioObject3D = StereoAudio | PositionalAudio; type AudioConstructor = new (listener: ThreeAudioListener) => T; @@ -55,6 +56,13 @@ function swapAudioType( swapObject3DComponent(world, eid, newAudio); } +export function swapAudioSrc(world: HubsWorld, videoEid: EntityID, audioEid: EntityID) { + const audio = world.eid2obj.get(audioEid)! as AudioObject3D; + const video = MediaVideoData.get(videoEid)!; + audio.setMediaElementSource(video); + video.volume = 1; +} + export function makeAudioEntity(world: HubsWorld, source: number, sourceType: SourceType, audioSystem: AudioSystem) { const eid = addEntity(world); APP.sourceType.set(eid, sourceType); diff --git a/src/bit-systems/camera-tool.js b/src/bit-systems/camera-tool.js index 0ce7cf8411..f403300cf4 100644 --- a/src/bit-systems/camera-tool.js +++ b/src/bit-systems/camera-tool.js @@ -211,7 +211,7 @@ function rotateWithRightClick(world, camera) { userinput.get(paths.device.mouse.buttonRight) ) { const rightCursor = anyEntityWith(world, RemoteRight); - physicsSystem.updateRigidBodyOptions(camera, { type: "kinematic" }); + physicsSystem.updateRigidBody(camera, { type: "kinematic" }); transformSystem.startTransform(world.eid2obj.get(camera), world.eid2obj.get(rightCursor), { mode: "cursor" }); diff --git a/src/bit-systems/interactable-system.ts b/src/bit-systems/interactable-system.ts new file mode 100644 index 0000000000..d7aa7e3442 --- /dev/null +++ b/src/bit-systems/interactable-system.ts @@ -0,0 +1,41 @@ +import { addComponent, defineQuery, enterQuery } from "bitecs"; +import { HubsWorld } from "../app"; +import { Holdable, MediaContentBounds, Networked, Rigidbody } from "../bit-components"; +import { getBox } from "../utils/auto-box-collider"; +import { Mesh, Vector3 } from "three"; +import { takeSoftOwnership } from "../utils/take-soft-ownership"; +import { EntityID } from "../utils/networking-types"; +import { COLLISION_LAYERS } from "../constants"; + +const tmpVector = new Vector3(); + +const interactableQuery = defineQuery([Holdable, Rigidbody, Networked]); +const interactableEnterQuery = enterQuery(interactableQuery); +export function interactableSystem(world: HubsWorld) { + interactableEnterQuery(world).forEach((eid: EntityID) => { + // Somebody must own a scene grabbable otherwise the networked transform will fight with physics. + if (Networked.creator[eid] === APP.getSid("scene") && Networked.owner[eid] === APP.getSid("reticulum")) { + takeSoftOwnership(world, eid); + } + + const obj = world.eid2obj.get(eid); + let hasMesh = false; + obj?.traverse(child => { + if ((child as Mesh).isMesh) { + hasMesh = true; + } + }); + + // If it has media frame collision mask, it needs to have content bounds + if (hasMesh && Rigidbody.collisionFilterMask[eid] & COLLISION_LAYERS.MEDIA_FRAMES) { + const box = getBox(obj, obj); + if (!box.isEmpty()) { + box.getSize(tmpVector); + addComponent(world, MediaContentBounds, eid); + MediaContentBounds.bounds[eid].set(tmpVector.toArray()); + } else { + console.error(`Couldn't create content bounds for entity ${eid}. It seems to be empty or have negative scale.`); + } + } + }); +} diff --git a/src/bit-systems/loop-animation.ts b/src/bit-systems/loop-animation.ts index 4bc7844ccd..bd791ddad8 100644 --- a/src/bit-systems/loop-animation.ts +++ b/src/bit-systems/loop-animation.ts @@ -1,5 +1,5 @@ import { addComponent, defineQuery, enterQuery, exitQuery, hasComponent, removeComponent } from "bitecs"; -import { AnimationAction, AnimationClip, AnimationMixer, LoopRepeat } from "three"; +import { AnimationClip, LoopRepeat } from "three"; import { MixerAnimatable, MixerAnimatableData, @@ -47,12 +47,16 @@ const getActiveClips = ( export function loopAnimationSystem(world: HubsWorld): void { loopAnimationInitializeEnterQuery(world).forEach((eid: number): void => { + const params = LoopAnimationInitializeData.get(eid)!; + if (!params.length) { + return; + } + const object = world.eid2obj.get(eid)!; const mixer = MixerAnimatableData.get(eid)!; addComponent(world, LoopAnimation, eid); - const params = LoopAnimationInitializeData.get(eid)!; const activeAnimations = []; for (let i = 0; i < params.length; i++) { diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index 98832a80dd..a31dcd9051 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -30,7 +30,8 @@ import { Rigidbody, MediaLoaderOffset, MediaVideo, - NetworkedTransform + NetworkedTransform, + MediaRoot } from "../bit-components"; import { inflatePhysicsShape, Shape } from "../inflators/physics-shape"; import { ErrorObject } from "../prefabs/error-object"; @@ -204,7 +205,7 @@ class UnsupportedMediaTypeError extends Error { } } -type MediaInfo = { +export type MediaInfo = { accessibleUrl: string; canonicalUrl: string; canonicalAudioUrl: string | null; @@ -283,6 +284,7 @@ function* loadMedia(world: HubsWorld, eid: EntityID) { try { const urlData = (yield resolveMediaInfo(src)) as MediaInfo; media = yield* loadByMediaType(world, eid, urlData); + addComponent(world, MediaRoot, media); addComponent(world, MediaLoaded, media); addComponent(world, MediaInfo, media); MediaInfo.accessibleUrl[media] = APP.getSid(urlData.accessibleUrl); @@ -359,7 +361,7 @@ export function mediaLoadingSystem(world: HubsWorld) { jobs.add(eid, clearRollbacks => loadAndAnimateMedia(world, eid, clearRollbacks)); }); - mediaLoadingExitQuery(world).forEach(function (eid) { + mediaLoadingExitQuery(world).forEach(function (eid: EntityID) { jobs.stop(eid); if (MediaImageLoaderData.has(eid)) { @@ -406,7 +408,7 @@ export function mediaLoadingSystem(world: HubsWorld) { } }); - mediaLoadingQuery(world).forEach(eid => { + mediaLoadingQuery(world).forEach((eid: EntityID) => { const mediaLoaderObj = world.eid2obj.get(eid)!; transformPosition.fromArray(NetworkedTransform.position[eid]); if (mediaLoaderObj.position.near(transformPosition, 0.001)) { @@ -417,7 +419,7 @@ export function mediaLoadingSystem(world: HubsWorld) { mediaLoadedEnterQuery(world).forEach(() => APP.scene?.emit("listed_media_changed")); mediaLoadedExitQuery(world).forEach(() => APP.scene?.emit("listed_media_changed")); - mediaRefreshEnterQuery(world).forEach(eid => { + mediaRefreshEnterQuery(world).forEach((eid: EntityID) => { if (!jobs.has(eid)) { jobs.add(eid, clearRollbacks => refreshMedia(world, eid, clearRollbacks)); } diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts index 51c6702a73..e7545c9936 100644 --- a/src/bit-systems/object-menu.ts +++ b/src/bit-systems/object-menu.ts @@ -105,7 +105,7 @@ function startRotation(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) } const transformSystem = APP.scene!.systems["transform-selected-object"]; const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; - physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" }); + physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" }); const rightCursorEid = anyEntityWith(world, RemoteRight)!; transformSystem.startTransform(world.eid2obj.get(targetEid)!, world.eid2obj.get(rightCursorEid)!, { mode: TRANSFORM_MODE.CURSOR @@ -137,7 +137,7 @@ function startScaling(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) // TODO: Remove the dependency with AFRAME const transformSystem = (AFRAME as any).scenes[0].systems["transform-selected-object"]; const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; - physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" }); + physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" }); const rightCursorEid = anyEntityWith(world, RemoteRight)!; scalingHandler = new ScalingHandler(world.eid2obj.get(targetEid), transformSystem); scalingHandler!.objectToScale = world.eid2obj.get(targetEid); diff --git a/src/bit-systems/quack.ts b/src/bit-systems/quack.ts deleted file mode 100644 index e66a4bb5c4..0000000000 --- a/src/bit-systems/quack.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { HubsWorld } from "../app"; -import { Held, Quack } from "../bit-components"; -import { defineQuery, enterQuery } from "bitecs"; -import { SOUND_QUACK, SOUND_SPECIAL_QUACK } from "../systems/sound-effects-system"; - -const heldQuackQuery = defineQuery([Quack, Held]); -const heldQuackEnterQuery = enterQuery(heldQuackQuery); - -export function quackSystem(world: HubsWorld) { - heldQuackEnterQuery(world).forEach(() => { - const rand = Math.random(); - if (rand < 0.01) { - APP.scene?.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_SPECIAL_QUACK); - } else { - APP.scene?.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK); - } - }); -} diff --git a/src/bit-systems/scene-loading.ts b/src/bit-systems/scene-loading.ts index c782c001b6..5f5a176d91 100644 --- a/src/bit-systems/scene-loading.ts +++ b/src/bit-systems/scene-loading.ts @@ -7,7 +7,6 @@ import { HeightFieldTag, NavMesh, Networked, - PhysicsShape, SceneLoader, ScenePreviewCamera, SceneRoot, diff --git a/src/bit-systems/text.ts b/src/bit-systems/text.ts index 5f6a93a7e3..717c2fc49e 100644 --- a/src/bit-systems/text.ts +++ b/src/bit-systems/text.ts @@ -1,9 +1,23 @@ import { defineQuery } from "bitecs"; import { Text as TroikaText } from "troika-three-text"; import { HubsWorld } from "../app"; -import { TextTag } from "../bit-components"; +import { NetworkedText, TextTag } from "../bit-components"; +import { + NumberOrNormalT, + NumberOrPctT, + THREE_SIDES, + flagToAnchorX, + flagToAnchorY, + flagToDirection, + flagToOverflowWrap, + flagToSide, + flagToTextAlign, + flagToWhiteSpace, + stringToNumberOrString +} from "../inflators/text"; const textQuery = defineQuery([TextTag]); +const networkedTextQuery = defineQuery([TextTag, NetworkedText]); export function textSystem(world: HubsWorld) { textQuery(world).forEach(eid => { @@ -30,4 +44,100 @@ export function textSystem(world: HubsWorld) { // because TroikaText properly handles text.sync(); }); + networkedTextQuery(world).forEach(eid => { + const text = world.eid2obj.get(eid)! as TroikaText; + const newText = APP.getString(NetworkedText.text[eid]); + if (text.text !== newText) { + text.text = newText!; + } + if (text.fontSize !== NetworkedText.fontSize[eid]) { + text.fontSize = NetworkedText.fontSize[eid]; + } + const textAlign = flagToTextAlign(NetworkedText.textAlign[eid]); + if (text.textAlign !== textAlign) { + text.textAlign = textAlign; + } + const anchorX = flagToAnchorX(NetworkedText.anchorX[eid]); + if (text.anchorX !== anchorX) { + text.anchorX = anchorX; + } + const anchorY = flagToAnchorY(NetworkedText.anchorY[eid]); + if (text.anchorY !== anchorY) { + text.anchorY = anchorY; + } + if (text.color !== NetworkedText.color[eid]) { + text.color = NetworkedText.color[eid]; + } + if (text.letterSpacing !== NetworkedText.letterSpacing[eid]) { + text.letterSpacing = NetworkedText.letterSpacing[eid]; + } + const lineHeight = stringToNumberOrString(APP.getString(NetworkedText.lineHeight[eid])!) as NumberOrNormalT; + if (text.lineHeight !== lineHeight) { + text.lineHeight = lineHeight; + } + const outlineWidth = stringToNumberOrString(APP.getString(NetworkedText.outlineWidth[eid])!) as NumberOrPctT; + if (text.outlineWidth !== outlineWidth) { + text.outlineWidth = outlineWidth; + } + if (text.outlineColor !== NetworkedText.outlineColor[eid]) { + text.outlineColor = NetworkedText.outlineColor[eid]; + } + const outlineBlur = stringToNumberOrString(APP.getString(NetworkedText.outlineBlur[eid])!) as NumberOrPctT; + if (text.outlineBlur !== outlineBlur) { + text.outlineBlur = outlineBlur; + } + const outlineOffsetX = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetX[eid])!) as NumberOrPctT; + if (text.outlineOffsetX !== outlineOffsetX) { + text.outlineOffsetX = outlineOffsetX; + } + const outlineOffsetY = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetY[eid])!) as NumberOrPctT; + if (text.outlineOffsetY !== outlineOffsetY) { + text.outlineOffsetY = outlineOffsetY; + } + if (text.outlineOpacity !== NetworkedText.outlineOpacity[eid]) { + text.outlineOpacity = NetworkedText.outlineOpacity[eid]; + } + if (text.fillOpacity !== NetworkedText.fillOpacity[eid]) { + text.fillOpacity = NetworkedText.fillOpacity[eid]; + } + const strokeWidth = stringToNumberOrString(APP.getString(NetworkedText.strokeWidth[eid])!) as NumberOrPctT; + if (text.strokeWidth !== strokeWidth) { + text.strokeWidth = strokeWidth; + } + if (text.strokeColor !== NetworkedText.strokeColor[eid]) { + text.strokeColor = NetworkedText.strokeColor[eid]; + } + if (text.strokeOpacity !== NetworkedText.strokeOpacity[eid]) { + text.strokeOpacity = NetworkedText.strokeOpacity[eid]; + } + if (text.textIndent !== NetworkedText.textIndent[eid]) { + text.textIndent = NetworkedText.textIndent[eid]; + } + const whiteSpace = flagToWhiteSpace(NetworkedText.whiteSpace[eid]); + if (text.whiteSpace !== whiteSpace) { + text.whiteSpace = whiteSpace; + } + const overflowWrap = flagToOverflowWrap(NetworkedText.overflowWrap[eid]); + if (text.overflowWrap !== overflowWrap) { + text.overflowWrap = overflowWrap; + } + if (text.material!.opacity !== NetworkedText.opacity[eid]) { + text.material!.opacity = NetworkedText.opacity[eid]; + } + const side = THREE_SIDES[flagToSide(NetworkedText.side[eid])]; + if (text.material!.side !== side) { + text.material!.side = side; + } + if (text.maxWidth !== NetworkedText.maxWidth[eid]) { + text.maxWidth = NetworkedText.maxWidth[eid]; + } + if (text.curveRadius !== NetworkedText.curveRadius[eid]) { + text.curveRadius = NetworkedText.curveRadius[eid]; + } + const direction = flagToDirection(NetworkedText.direction[eid]); + if (text.direction !== direction) { + text.direction = direction; + } + text.sync(); + }); } diff --git a/src/bit-systems/video-system.ts b/src/bit-systems/video-system.ts index 6fe23202c4..95dc5b2a9e 100644 --- a/src/bit-systems/video-system.ts +++ b/src/bit-systems/video-system.ts @@ -1,10 +1,22 @@ -import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs"; +import { + addComponent, + defineComponent, + defineQuery, + enterQuery, + entityExists, + exitQuery, + hasComponent, + removeComponent +} from "bitecs"; import { Mesh } from "three"; import { HubsWorld } from "../app"; import { + AudioEmitter, AudioParams, AudioSettingsChanged, + MediaInfo, MediaLoaded, + MediaRoot, MediaVideo, MediaVideoData, MediaVideoUpdated, @@ -14,25 +26,102 @@ import { } from "../bit-components"; import { SourceType } from "../components/audio-params"; import { AudioSystem } from "../systems/audio-system"; -import { findAncestorWithComponent } from "../utils/bit-utils"; -import { Emitter2Audio, Emitter2Params, makeAudioEntity } from "./audio-emitter-system"; +import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils"; +import { Emitter2Audio, Emitter2Params, makeAudioEntity, swapAudioSrc } from "./audio-emitter-system"; import { takeSoftOwnership } from "../utils/take-soft-ownership"; import { crNextFrame } from "../utils/coroutine"; +import { ClearFunction, JobRunner, swapObject3DComponent } from "../hubs"; +import { VIDEO_FLAGS } from "../inflators/video"; +import { HubsVideoTexture } from "../textures/HubsVideoTexture"; +import { create360ImageMesh, createImageMesh } from "../utils/create-image-mesh"; +import { loadAudioTexture } from "../utils/load-audio-texture"; +import { loadVideoTexture } from "../utils/load-video-texture"; +import { resolveMediaInfo, MediaType } from "../utils/media-utils"; +import { EntityID } from "../utils/networking-types"; +import { ProjectionModeName, getProjectionNameFromProjection } from "../utils/projection-mode"; +import { disposeNode } from "../utils/three-utils"; +import { MediaInfo as MediaInfoT } from "./media-loading"; + +export const MediaVideoUpdateSrcEvent = defineComponent(); + +function* loadSrc( + world: HubsWorld, + eid: EntityID, + src: string, + oldVideo: HTMLVideoElement, + clearRollbacks: ClearFunction +) { + const projection = getProjectionNameFromProjection(NetworkedVideo.projection[eid]); + const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.LOOP ? true : false; + const { accessibleUrl, contentType, mediaType } = (yield resolveMediaInfo(src)) as MediaInfoT; + let data: any; + if (mediaType === MediaType.VIDEO) { + data = (yield loadVideoTexture(accessibleUrl, contentType, loop, autoPlay)) as unknown; + } else if (mediaType === MediaType.AUDIO) { + data = (yield loadAudioTexture(accessibleUrl, loop, autoPlay)) as unknown; + } else { + return; + } + + const { texture, ratio, video }: { texture: HubsVideoTexture; ratio: number; video: HTMLVideoElement } = data; + + clearRollbacks(); // After this point, normal entity cleanup will take care of things + + let videoObj; + if (projection === ProjectionModeName.SPHERE_EQUIRECTANGULAR) { + videoObj = create360ImageMesh(texture, ratio); + } else { + videoObj = createImageMesh(texture, ratio); + } + MediaVideo.ratio[eid] = ratio; + MediaVideoData.set(eid, video); + oldVideo.pause(); + const mediaRoot = findAncestorWithComponent(world, MediaRoot, eid)!; + const mediaRootObj = world.eid2obj.get(mediaRoot)!; + mediaRootObj.add(videoObj); + + const audioEmitter = findChildWithComponent(world, AudioEmitter, eid)!; + swapAudioSrc(world, eid, audioEmitter); + const audioObj = APP.world.eid2obj.get(audioEmitter)!; + videoObj.add(audioObj); + + const oldVideoObj = APP.world.eid2obj.get(eid)! as Mesh; + mediaRootObj.remove(oldVideoObj); + disposeNode(oldVideoObj); + + swapObject3DComponent(world, eid, videoObj); + + if ((NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED) === 0 || autoPlay) { + video.play(); + } + + removeComponent(world, MediaVideoUpdateSrcEvent, eid); +} enum Flags { PAUSED = 1 << 0 } +export function updateVideoSrc(world: HubsWorld, eid: EntityID, src: string, video: HTMLVideoElement) { + addComponent(world, MediaVideoUpdateSrcEvent, eid); + + jobs.stop(eid); + jobs.add(eid, clearRollbacks => loadSrc(world, eid, src, video, clearRollbacks)); +} + +const jobs = new JobRunner(); export const OUT_OF_SYNC_SEC = 5; const networkedVideoQuery = defineQuery([Networked, NetworkedVideo]); const networkedVideoEnterQuery = enterQuery(networkedVideoQuery); const mediaVideoQuery = defineQuery([MediaVideo]); const mediaVideoEnterQuery = enterQuery(mediaVideoQuery); +const networkedVideoExitQuery = exitQuery(networkedVideoQuery); const mediaVideoExitQuery = exitQuery(mediaVideoQuery); const mediaLoadStatusQuery = defineQuery([MediaVideo, MediaLoaded]); const mediaLoadedQuery = enterQuery(mediaLoadStatusQuery); export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { - mediaVideoEnterQuery(world).forEach(function (videoEid) { + mediaVideoEnterQuery(world).forEach(function (videoEid: EntityID) { const videoObj = world.eid2obj.get(videoEid) as Mesh; const video = MediaVideoData.get(videoEid)!; if (video.autoplay) { @@ -48,7 +137,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { // Note in media-video we call updateMatrixWorld here to force PositionalAudio's updateMatrixWorld to run even // if it has an invisible parent. We don't want to have invisible parents now. }); - mediaLoadedQuery(world).forEach(videoEid => { + mediaLoadedQuery(world).forEach((videoEid: EntityID) => { const audioParamsEid = findAncestorWithComponent(world, AudioParams, videoEid); if (audioParamsEid) { const audioSettings = APP.audioOverrides.get(audioParamsEid)!; @@ -58,7 +147,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { addComponent(world, AudioSettingsChanged, audioEid); } }); - mediaVideoExitQuery(world).forEach(videoEid => { + mediaVideoExitQuery(world).forEach((videoEid: EntityID) => { const audioParamsEid = Emitter2Params.get(videoEid); audioParamsEid && APP.audioOverrides.delete(audioParamsEid); Emitter2Params.delete(videoEid); @@ -66,13 +155,16 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { MediaVideoData.delete(videoEid); }); - networkedVideoEnterQuery(world).forEach(function (eid) { + networkedVideoEnterQuery(world).forEach(function (eid: EntityID) { if (Networked.owner[eid] === APP.getSid("reticulum")) { takeSoftOwnership(world, eid); } }); + networkedVideoExitQuery(world).forEach((eid: EntityID) => { + jobs.stop(eid); + }); - networkedVideoQuery(world).forEach(function (eid) { + networkedVideoQuery(world).forEach(function (eid: EntityID) { const video = MediaVideoData.get(eid)!; if (hasComponent(world, Owned, eid)) { const now = performance.now(); @@ -80,18 +172,49 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { NetworkedVideo.time[eid] = video.currentTime; MediaVideo.lastUpdate[eid] = now; } - let flags = 0; - flags |= video.paused ? Flags.PAUSED : 0; + let flags = MediaVideo.flags[eid]; + if (video.paused) { + flags |= VIDEO_FLAGS.PAUSED; + } else { + flags &= ~VIDEO_FLAGS.PAUSED; + } + if (video.loop) { + flags |= VIDEO_FLAGS.LOOP; + } else { + flags &= ~VIDEO_FLAGS.LOOP; + } + if (video.autoplay) { + flags |= VIDEO_FLAGS.AUTO_PLAY; + } else { + flags &= ~VIDEO_FLAGS.AUTO_PLAY; + } NetworkedVideo.flags[eid] = flags; + NetworkedVideo.src[eid] = MediaInfo.accessibleUrl[eid]; } else { - const networkedPauseState = !!(NetworkedVideo.flags[eid] & Flags.PAUSED); + let shouldUpdateVideo = false; + const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + if (MediaVideo.flags[eid] !== NetworkedVideo.flags[eid]) { + MediaVideo.flags[eid] = NetworkedVideo.flags[eid]; + } + if (MediaVideo.projection[eid] !== NetworkedVideo.projection[eid]) { + MediaVideo.projection[eid] = NetworkedVideo.projection[eid]; + shouldUpdateVideo ||= true; + } + const src = APP.getString(NetworkedVideo.src[eid])!; + const currentSrc = APP.getString(MediaInfo.accessibleUrl[eid]); + shouldUpdateVideo ||= src !== currentSrc || autoPlay !== video.autoplay || loop !== video.loop; + if (shouldUpdateVideo && !hasComponent(world, MediaVideoUpdateSrcEvent, eid)) { + updateVideoSrc(world, eid, src, video); + } + const networkedPauseState = !!(NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED); if (networkedPauseState !== video.paused) { - video.paused - ? video.play().catch(() => { + networkedPauseState + ? video.pause() + : video.play().catch(() => { // Need to deal with the fact play() may fail if user has not interacted with browser yet. console.error("Error playing video."); - }) - : video.pause(); + }); addComponent(world, MediaVideoUpdated, eid); } if (networkedPauseState || Math.abs(NetworkedVideo.time[eid] - video.currentTime) > OUT_OF_SYNC_SEC) { @@ -100,7 +223,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { } } }); - mediaVideoQuery(world).forEach(eid => { + mediaVideoQuery(world).forEach((eid: EntityID) => { // We need to delay this a frame to give a chance to other services to process this event crNextFrame().then(() => { if (entityExists(world, eid)) { @@ -108,4 +231,6 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { } }); }); + + jobs.tick(); } diff --git a/src/cloud.js b/src/cloud.js index e6e4576f86..a99adde6f5 100644 --- a/src/cloud.js +++ b/src/cloud.js @@ -9,7 +9,7 @@ import { PageContainer } from "./react-components/layout/PageContainer"; import { AuthContextProvider } from "./react-components/auth/AuthContext"; import { Container } from "./react-components/layout/Container"; import { Button } from "./react-components/input/Button"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; import registerTelemetry from "./telemetry"; import { FormattedMessage } from "react-intl"; @@ -117,6 +117,7 @@ function HubsCloudPage() { ); } +const store = getStore(); window.APP = { store }; function CloudRoot() { diff --git a/src/components/body-helper.js b/src/components/body-helper.js index e7b17d63d7..96496da1e0 100644 --- a/src/components/body-helper.js +++ b/src/components/body-helper.js @@ -1,6 +1,7 @@ import { addComponent, removeComponent } from "bitecs"; import { CONSTANTS } from "three-ammo"; import { Rigidbody } from "../bit-components"; +import { updateBodyParams } from "../inflators/rigid-body"; const ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE, TYPE = CONSTANTS.TYPE; @@ -41,6 +42,7 @@ AFRAME.registerComponent("body-helper", { this.uuid = this.system.addBody(this.el.object3D, this.data); const eid = this.el.object3D.eid; addComponent(APP.world, Rigidbody, eid); + updateBodyParams(eid, this.data); Rigidbody.bodyId[eid] = this.uuid; //uuid is a lie, it's actually an int }, diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 13f5e87a19..c668615aa4 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -508,6 +508,18 @@ class GLTFHubsPlugin { } } } + const materials = parser.json.materials; + if (materials) { + for (let i = 0; i < materials.length; i++) { + const mat = materials[i]; + + if (!mat.extras) { + mat.extras = {}; + } + + mat.extras.gltfIndex = i; + } + } } afterRoot(gltf) { @@ -858,6 +870,7 @@ class GLTFHubsLoopAnimationComponent { } } +export const gltfPluginsExtra = []; export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) { let gltfUrl = src; let fileMap; @@ -933,6 +946,7 @@ export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) { } }) ); + gltfPluginsExtra.forEach(ext => gltfLoader.register(parser => ext(parser))); // TODO some models are loaded before the renderer exists. This is likely things like the camera tool and loading cube. // They don't currently use KTX textures but if they did this would be an issue. Fixing this is hard but is part of diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 9c89bbddcf..94961d74f6 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -284,8 +284,10 @@ AFRAME.registerComponent("media-loader", { // TODO this does duplicate work in some cases, but finish() is the only consistent place to do it const contentBounds = getBox(this.el.object3D, this.el.getObject3D("mesh")).getSize(new THREE.Vector3()); - addComponent(APP.world, MediaContentBounds, el.eid); - MediaContentBounds.bounds[el.eid].set(contentBounds.toArray()); + if (el.eid) { + addComponent(APP.world, MediaContentBounds, el.eid); + MediaContentBounds.bounds[el.eid].set(contentBounds.toArray()); + } el.emit("media-loaded"); }; diff --git a/src/constants.ts b/src/constants.ts index 64c1c988e7..189993a663 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,12 +6,13 @@ export enum COLLISION_LAYERS { AVATAR = 1 << 2, HANDS = 1 << 3, MEDIA_FRAMES = 1 << 4, + TRIGGERS = 1 << 5, // @TODO we should split these "sets" off into something other than COLLISION_LAYERS or at least name // them differently to indicate they are a combination of multiple bits - DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES, + DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES | TRIGGERS, UNOWNED_INTERACTABLE = INTERACTABLES | HANDS | MEDIA_FRAMES, DEFAULT_SPAWNER = INTERACTABLES | HANDS -}; +} export enum AAModes { NONE = "NONE", diff --git a/src/discord.js b/src/discord.js index 5a0efc53c0..1148b9e30f 100644 --- a/src/discord.js +++ b/src/discord.js @@ -9,7 +9,7 @@ import discordBotLogo from "./assets/images/discord-bot-logo.png"; import registerTelemetry from "./telemetry"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/discord", "Discord Landing Page"); @@ -19,6 +19,7 @@ class DiscordPage extends Component { componentDidMount() {} render() { + const store = getStore(); return ( diff --git a/src/hub.html b/src/hub.html index 623027ad19..a6d733bd6f 100644 --- a/src/hub.html +++ b/src/hub.html @@ -211,7 +211,7 @@ @@ -821,7 +821,8 @@ diff --git a/src/hub.js b/src/hub.js index fb609a6b7e..bd2eb1a554 100644 --- a/src/hub.js +++ b/src/hub.js @@ -183,6 +183,7 @@ import "./systems/audio-debug-system"; import "./systems/audio-gain-system"; import "./gltf-component-mappings"; +import { addons } from "./addons"; import { App, getScene } from "./app"; import MediaDevicesManager from "./utils/media-devices-manager"; import PinningHelper from "./utils/pinning-helper"; @@ -219,8 +220,6 @@ preload( }) ); -const store = window.APP.store; -store.update({ preferences: { shouldPromptForRefresh: false } }); // Clear flag that prompts for refresh from preference screen const mediaSearchStore = window.APP.mediaSearchStore; const OAUTH_FLOW_PERMS_TOKEN_KEY = "ret-oauth-flow-perms-token"; const NOISY_OCCUPANT_COUNT = 30; // Above this # of occupants, we stop posting join/leaves/renames @@ -273,6 +272,7 @@ import { exposeBitECSDebugHelpers } from "./bitecs-debug-helpers"; import { loadLegacyRoomObjects } from "./utils/load-legacy-room-objects"; import { loadSavedEntityStates } from "./utils/entity-state-utils"; import { shouldUseNewLoader } from "./utils/bit-utils"; +import { getStore } from "./utils/store-instance"; const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; @@ -353,6 +353,7 @@ function mountUI(props = {}) { qsTruthy("allow_idle") || (process.env.NODE_ENV === "development" && !qs.get("idle_timeout")); const forcedVREntryType = qsVREntryType; + const store = getStore(); root.render( @@ -543,8 +544,18 @@ export async function updateEnvironmentForHub(hub, entryManager) { } } -export async function updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt = false) { - remountUI({ hub, entryDisallowed: !hubChannel.canEnterRoom(hub), showBitECSBasedClientRefreshPrompt }); +export async function updateUIForHub( + hub, + hubChannel, + showBitECSBasedClientRefreshPrompt = false, + showAddonRefreshPrompt = false +) { + remountUI({ + hub, + entryDisallowed: !hubChannel.canEnterRoom(hub), + showBitECSBasedClientRefreshPrompt, + showAddonRefreshPrompt + }); } function onConnectionError(entryManager, connectError) { @@ -596,7 +607,7 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) onSendMessage: messageDispatch.dispatch, onLoaded: () => { audioSystem.setMediaGainOverride(1); - store.executeOnLoadActions(scene); + getStore().executeOnLoadActions(scene); }, onMediaSearchResultEntrySelected: (entry, selectAction) => scene.emit("action_selected_media_result_entry", { entry, selectAction }), @@ -728,6 +739,9 @@ async function runBotMode(scene, entryManager) { } document.addEventListener("DOMContentLoaded", async () => { + const store = getStore(); + store.update({ preferences: { shouldPromptForRefresh: false } }); // Clear flag that prompts for refresh from preference screen + if (!root) { const container = document.getElementById("ui-root"); root = createRoot(container); @@ -1388,16 +1402,26 @@ document.addEventListener("DOMContentLoaded", async () => { const displayName = (userInfo && userInfo.metas[0].profile.displayName) || "API"; let showBitECSBasedClientRefreshPrompt = false; - - if (!!hub.user_data?.hubs_use_bitecs_based_client !== !!window.APP.hub.user_data?.hubs_use_bitecs_based_client) { + if (!!hub.user_data?.hubs_use_bitecs_based_client !== !!APP.hub.user_data?.hubs_use_bitecs_based_client) { showBitECSBasedClientRefreshPrompt = true; setTimeout(() => { document.location.reload(); }, 5000); } + let showAddonRefreshPrompt = false; + [...addons.keys()].map(id => { + const oldAddonState = !!APP.hub.user_data && "addons" in APP.hub.user_data && APP.hub.user_data.addons[id]; + const newAddonState = !!hub.user_data && "addons" in hub.user_data && hub.user_data.addons[id]; + if (newAddonState !== oldAddonState) { + showAddonRefreshPrompt = true; + setTimeout(() => { + document.location.reload(); + }, 5000); + } + }); window.APP.hub = hub; - updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt); + updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt, showAddonRefreshPrompt); if ( stale_fields.includes("scene") || @@ -1484,4 +1508,6 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); + + APP.notifyOnInit(); }); diff --git a/src/hubs.js b/src/hubs.js new file mode 100644 index 0000000000..48dc23a122 --- /dev/null +++ b/src/hubs.js @@ -0,0 +1,43 @@ +export * from "./bit-components"; +export * as bitComponents from "./bit-components"; +export * from "./addons"; +export * from "./types"; +export * from "./camera-layers"; +export * from "./constants"; +export * from "./change-hub"; +export * from "./utils/bit-utils"; +export * from "./utils/jsx-entity"; +export * from "./utils/media-url-utils"; +export * from "./utils/bit-pinning-helper"; +export * from "./utils/create-networked-entity"; +export * from "./utils/material-utils"; +export * from "./utils/network-schemas"; +export * from "./utils/define-network-schema"; +export * from "./utils/animate"; +export * from "./utils/easing"; +export * from "./utils/coroutine"; +export * from "./utils/coroutine-utils"; +export * from "./utils/take-ownership"; +export * from "./utils/take-soft-ownership"; +export * from "./utils/component-utils"; +export * from "./utils/projection-mode"; +export * from "./utils/assign-network-ids"; +export * from "./utils/store-instance"; +export * from "./components/gltf-model-plus"; +export * from "./inflators/model"; +export * from "./inflators/physics-shape"; +export * from "./inflators/media-frame"; +export * from "./inflators/rigid-body"; +export * from "./inflators/text"; +export * from "./inflators/video"; +export * from "./bit-systems/delete-entity-system"; +export * from "./bit-systems/video-system"; +export * from "./systems/floaty-object-system"; +export * from "./systems/userinput/paths"; +export * from "./systems/userinput/sets"; +export * from "./systems/userinput/userinput"; +export * from "./systems/userinput/bindings/xforms"; +export * from "./systems/userinput/bindings/keyboard-mouse-user"; +export * from "./systems/userinput/devices/keyboard"; +export * from "./systems/bit-physics"; +export * from "./react-components/debug-panel/ECSSidebar"; diff --git a/src/index.js b/src/index.js index 896c491f7a..414578772c 100644 --- a/src/index.js +++ b/src/index.js @@ -7,10 +7,11 @@ import { HomePage } from "./react-components/home/HomePage"; import { AuthContextProvider } from "./react-components/auth/AuthContext"; import "./react-components/styles/global.scss"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/home", "Hubs Home Page"); +const store = getStore(); window.APP = { store }; function HomeRoot() { diff --git a/src/inflators/grabbable.ts b/src/inflators/grabbable.ts index 2c8f89405e..4aa29e6f4f 100644 --- a/src/inflators/grabbable.ts +++ b/src/inflators/grabbable.ts @@ -3,11 +3,11 @@ import { HubsWorld } from "../app"; import { CursorRaycastable, HandCollisionTarget, - Holdable, OffersHandConstraint, OffersRemoteConstraint, RemoteHoverTarget } from "../bit-components"; +import { inflateHoldable } from "./holdable"; export type GrabbableParams = { cursor: boolean; hand: boolean }; const defaults: GrabbableParams = { cursor: true, hand: true }; @@ -22,5 +22,5 @@ export function inflateGrabbable(world: HubsWorld, eid: number, props: Grabbable addComponent(world, RemoteHoverTarget, eid); addComponent(world, OffersRemoteConstraint, eid); } - addComponent(world, Holdable, eid); + inflateHoldable(world, eid); } diff --git a/src/inflators/holdable.ts b/src/inflators/holdable.ts new file mode 100644 index 0000000000..d76c099823 --- /dev/null +++ b/src/inflators/holdable.ts @@ -0,0 +1,20 @@ +import { addComponent } from "bitecs"; +import { HubsWorld } from "../app"; +import { Holdable } from "../bit-components"; + +export const HOLDABLE_FLAGS = { + ENABLED: 1 << 0 +}; + +export type HoldableParams = { enabled: boolean }; +const defaults: HoldableParams = { enabled: true }; +export function inflateHoldable(world: HubsWorld, eid: number, props?: HoldableParams) { + props = Object.assign({}, defaults, props); + + addComponent(world, Holdable, eid); + if (props.enabled !== false) { + Holdable.flags[eid] |= HOLDABLE_FLAGS.ENABLED; + } else { + Holdable.flags[eid] &= ~HOLDABLE_FLAGS.ENABLED; + } +} diff --git a/src/inflators/loop-animation.ts b/src/inflators/loop-animation.ts index d21665bec7..f032e5fd4a 100644 --- a/src/inflators/loop-animation.ts +++ b/src/inflators/loop-animation.ts @@ -20,7 +20,7 @@ type ElementParams = { export type LoopAnimationParams = ElementParams[]; -const ELEMENT_DEFAULTS: Required = { +export const LOOP_ANIMATION_DEFAULTS: Required = { activeClipIndex: 0, clip: "", activeClipIndices: [], @@ -29,20 +29,14 @@ const ELEMENT_DEFAULTS: Required = { timeScale: 1.0 }; -const DEFAULTS: Required = [ELEMENT_DEFAULTS]; - export function inflateLoopAnimationInitialize( world: HubsWorld, eid: number, params: LoopAnimationParams = [] ): number { - if (params.length === 0) { - params = DEFAULTS; - } - const componentParams = []; for (let i = 0; i < params.length; i++) { - const requiredParams = Object.assign({}, ELEMENT_DEFAULTS, params[i]) as Required; + const requiredParams = Object.assign({}, LOOP_ANIMATION_DEFAULTS, params[i]) as Required; const activeClipIndices = requiredParams.activeClipIndices.length > 0 ? requiredParams.activeClipIndices : [requiredParams.activeClipIndex]; componentParams.push({ diff --git a/src/inflators/media-frame.js b/src/inflators/media-frame.js index 84db6250ef..d1dccaaace 100644 --- a/src/inflators/media-frame.js +++ b/src/inflators/media-frame.js @@ -15,14 +15,28 @@ export const AxisAlignType = { }; export const MEDIA_FRAME_FLAGS = { - SCALE_TO_BOUNDS: 1 << 0 + SCALE_TO_BOUNDS: 1 << 0, + ACTIVE: 1 << 1, + SNAP_TO_CENTER: 1 << 2, + LOCKED: 1 << 3 +}; + +export const MediaTypes = { + all: MediaType.ALL, + "all-2d": MediaType.ALL_2D, + model: MediaType.MODEL, + image: MediaType.IMAGE, + video: MediaType.VIDEO, + pdf: MediaType.PDF }; const DEFAULTS = { bounds: { x: 1, y: 1, z: 1 }, mediaType: "all", scaleToBounds: true, - align: { x: "center", y: "center", z: "center" } + align: { x: "center", y: "center", z: "center" }, + active: true, + locked: false }; export function inflateMediaFrame(world, eid, componentProps) { componentProps = Object.assign({}, DEFAULTS, componentProps); @@ -68,17 +82,16 @@ export function inflateMediaFrame(world, eid, componentProps) { addComponent(world, MediaFrame, eid, true); addComponent(world, NetworkedMediaFrame, eid, true); + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE; + if (componentProps.snapToCenter) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.SNAP_TO_CENTER; + } + if (!hasComponent(world, Networked, eid)) addComponent(world, Networked, eid); // Media types accepted - MediaFrame.mediaType[eid] = { - all: MediaType.ALL, - "all-2d": MediaType.ALL_2D, - model: MediaType.MODEL, - image: MediaType.IMAGE, - video: MediaType.VIDEO, - pdf: MediaType.PDF - }[componentProps.mediaType]; + MediaFrame.mediaType[eid] = MediaTypes[componentProps.mediaType]; + NetworkedMediaFrame.mediaType[eid] = MediaFrame.mediaType[eid]; // Bounds MediaFrame.bounds[eid].set([componentProps.bounds.x, componentProps.bounds.y, componentProps.bounds.z]); // Axis alignment @@ -101,6 +114,15 @@ export function inflateMediaFrame(world, eid, componentProps) { if (componentProps.scaleToBounds) flags |= MEDIA_FRAME_FLAGS.SCALE_TO_BOUNDS; MediaFrame.flags[eid] = flags; + if (componentProps.active) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE; + MediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE; + } + if (componentProps.locked) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.LOCKED; + MediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.LOCKED; + } + inflateRigidBody(world, eid, { type: Type.KINEMATIC, collisionGroup: COLLISION_LAYERS.MEDIA_FRAMES, diff --git a/src/inflators/model.tsx b/src/inflators/model.tsx index 42875bd551..7d03ff2ca9 100644 --- a/src/inflators/model.tsx +++ b/src/inflators/model.tsx @@ -7,24 +7,23 @@ import { mapMaterials } from "../utils/material-utils"; import { EntityID } from "../utils/networking-types"; import { inflateLoopAnimationInitialize, LoopAnimationParams } from "./loop-animation"; -function camelCase(s: string) { +export function camelCase(s: string) { return s.replace(/-(\w)/g, (_, m) => m.toUpperCase()); } export type ModelParams = { model: Object3D }; +export type GLTFLinkResolverFn = ( + world: HubsWorld, + model: Object3D, + rootEid: EntityID, + idx2eid: Map +) => void; + // These components are all handled in some special way, not through inflators -const ignoredComponents = [ - "visible", - "frustum", - "frustrum", - "shadow", - "networked", - "animation-mixer", - "loop-animation" -]; - -function inflateComponents( +const ignoredComponents = ["visible", "frustum", "frustrum", "shadow", "animation-mixer", "loop-animation"]; + +export function inflateComponents( world: HubsWorld, eid: number, components: { [componentName: string]: any }, @@ -59,6 +58,7 @@ function inflateComponents( }); } +export const gltfLinkResolvers = new Array(); export function inflateModel(world: HubsWorld, rootEid: number, { model }: ModelParams) { const swap: [old: Object3D, replacement: Object3D][] = []; const idx2eid = new Map(); @@ -166,5 +166,7 @@ export function inflateModel(world: HubsWorld, rootEid: number, { model }: Model inflateLoopAnimationInitialize(world, rootEid, loopAnimationParams); } + gltfLinkResolvers.forEach(resolved => resolved(world, model, rootEid, idx2eid)); + addComponent(world, GLTFModel, rootEid); } diff --git a/src/inflators/rigid-body.ts b/src/inflators/rigid-body.ts index 54f2cf55e5..fee7717ffe 100644 --- a/src/inflators/rigid-body.ts +++ b/src/inflators/rigid-body.ts @@ -1,7 +1,8 @@ import { addComponent } from "bitecs"; -import { Rigidbody } from "../bit-components"; +import { NetworkedRigidBody, Rigidbody } from "../bit-components"; import { HubsWorld } from "../app"; import { CONSTANTS } from "three-ammo"; +import { COLLISION_LAYERS } from "../constants"; export enum Type { STATIC = 0, @@ -9,6 +10,13 @@ export enum Type { KINEMATIC } +export enum CollisionGroup { + OBJECTS = "objects", + ENVIRONMENT = "environment", + TRIGGERS = "triggers", + AVATARS = "avatars" +} + export enum ActivationState { ACTIVE_TAG = 0, ISLAND_SLEEPING = 1, @@ -17,6 +25,23 @@ export enum ActivationState { DISABLE_SIMULATION = 4 } +export type BodyParams = { + type: string; + mass: number; + gravity: { x: number; y: number; z: number }; + linearDamping: number; + angularDamping: number; + linearSleepingThreshold: number; + angularSleepingThreshold: number; + angularFactor: { x: number; y: number; z: number }; + activationState: string; + emitCollisionEvents: boolean; + disableCollision: boolean; + collisionFilterGroup: number; + collisionFilterMask: number; + scaleAutoUpdate: boolean; +}; + export type RigidBodyParams = { type: Type; mass: number; @@ -61,10 +86,50 @@ export const getTypeString = (eid: number) => { return Object.values(CONSTANTS.TYPE)[Rigidbody.type[eid]]; }; -export const getActivationStateString = (eid: number) => { +export const getStringFromActivationState = (eid: number) => { return Object.values(CONSTANTS.ACTIVATION_STATE)[Rigidbody.activationState[eid]]; }; +export const getActivationStateFromString = (activationState: string) => { + switch (activationState) { + case CONSTANTS.ACTIVATION_STATE.ACTIVE_TAG: + return ActivationState.ACTIVE_TAG; + case CONSTANTS.ACTIVATION_STATE.DISABLE_DEACTIVATION: + return ActivationState.DISABLE_DEACTIVATION; + case CONSTANTS.ACTIVATION_STATE.DISABLE_SIMULATION: + return ActivationState.DISABLE_SIMULATION; + case CONSTANTS.ACTIVATION_STATE.ISLAND_SLEEPING: + return ActivationState.ISLAND_SLEEPING; + case CONSTANTS.ACTIVATION_STATE.WANTS_DEACTIVATION: + return ActivationState.WANTS_DEACTIVATION; + } + return ActivationState.ACTIVE_TAG; +}; + +export const getTypeFromBodyType = (type: string) => { + switch (type) { + case "static": + return Type.STATIC; + case "dynamic": + return Type.DYNAMIC; + case "kinematic": + return Type.KINEMATIC; + } + return Type.KINEMATIC; +}; + +export const getBodyTypeFromType = (type: Type) => { + switch (type) { + case Type.STATIC: + return "static"; + case Type.DYNAMIC: + return "dynamic"; + case Type.KINEMATIC: + return "kinematic"; + } + return "kinematic"; +}; + export const getBodyFromRigidBody = (eid: number) => { return { mass: Rigidbody.mass[eid], @@ -78,44 +143,156 @@ export const getBodyFromRigidBody = (eid: number) => { y: Rigidbody.angularFactor[eid][1], z: Rigidbody.angularFactor[eid][2] }, - activationState: getActivationStateString(eid), - emitCollisionEvents: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS, - scaleAutoUpdate: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE, + activationState: getStringFromActivationState(eid), + emitCollisionEvents: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS) !== 0, + scaleAutoUpdate: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE) !== 0, type: getTypeString(eid), - disableCollision: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION, + disableCollision: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION) !== 0, collisionFilterGroup: Rigidbody.collisionFilterGroup[eid], collisionFilterMask: Rigidbody.collisionFilterMask[eid] }; }; -const updateRigidBody = (eid: number, params: RigidBodyParams) => { - Rigidbody.type[eid] = params.type; - Rigidbody.mass[eid] = params.mass; - Rigidbody.gravity[eid].set(params.gravity); - Rigidbody.linearDamping[eid] = params.linearDamping; - Rigidbody.angularDamping[eid] = params.angularDamping; - Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold; - Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold; - Rigidbody.angularFactor[eid].set(params.angularFactor); - Rigidbody.activationState[eid] = params.activationState; - params.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); - params.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); - Rigidbody.collisionFilterGroup[eid] = params.collisionGroup; - Rigidbody.collisionFilterMask[eid] = params.collisionMask; - params.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); +export const updateBodyParams = (eid: number, params: Partial) => { + const currentParams = getBodyFromRigidBody(eid); + const bodyParams = Object.assign({}, currentParams, params) as BodyParams; + + Rigidbody.type[eid] = getTypeFromBodyType(bodyParams.type); + Rigidbody.mass[eid] = bodyParams.mass; + Rigidbody.gravity[eid].set([bodyParams.gravity.x, bodyParams.gravity.y, bodyParams.gravity.z]); + Rigidbody.linearDamping[eid] = bodyParams.linearDamping; + Rigidbody.angularDamping[eid] = bodyParams.angularDamping; + Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold; + Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold; + Rigidbody.angularFactor[eid].set([ + bodyParams.angularFactor.x, + bodyParams.angularFactor.y, + bodyParams.angularFactor.z + ]); + Rigidbody.activationState[eid] = getActivationStateFromString(params.activationState!); + bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); + bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); + Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionFilterGroup; + Rigidbody.collisionFilterMask[eid] = bodyParams.collisionFilterMask; + bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); }; -export const updateRigiBodyParams = (eid: number, params: Partial) => { - const currentParams = getBodyFromRigidBody(eid); - const bodyParams = Object.assign({}, currentParams, params); - updateRigidBody(eid, bodyParams); +export const updateRigidBodyParams = (eid: number, params: Partial) => { + if (params.type !== undefined) { + Rigidbody.type[eid] = params.type; + } + if (params.mass !== undefined) { + Rigidbody.mass[eid] = params.mass; + } + if (params.gravity !== undefined) { + Rigidbody.gravity[eid].set(params.gravity); + } + if (params.linearDamping !== undefined) { + Rigidbody.linearDamping[eid] = params.linearDamping; + } + if (params.angularDamping !== undefined) { + Rigidbody.angularDamping[eid] = params.angularDamping; + } + if (params.linearSleepingThreshold !== undefined) { + Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold; + } + if (params.angularSleepingThreshold !== undefined) { + Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold; + } + if (params.angularFactor !== undefined) { + Rigidbody.angularFactor[eid].set(params.angularFactor); + } + if (params.activationState !== undefined) { + Rigidbody.activationState[eid] = params.activationState; + } + if (params.emitCollisionEvents !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS; + } + if (params.disableCollision !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION; + } + if (params.scaleAutoUpdate !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE; + } + if (params.collisionGroup !== undefined) { + Rigidbody.collisionFilterGroup[eid] = params.collisionGroup; + } + if (params.collisionMask !== undefined) { + Rigidbody.collisionFilterMask[eid] = params.collisionMask; + } }; export function inflateRigidBody(world: HubsWorld, eid: number, params: Partial) { - const bodyParams = Object.assign({}, DEFAULTS, params); + const bodyParams = Object.assign({}, DEFAULTS, params) as RigidBodyParams; addComponent(world, Rigidbody, eid); - updateRigidBody(eid, bodyParams); + addComponent(world, NetworkedRigidBody, eid); + + Rigidbody.type[eid] = bodyParams.type; + Rigidbody.mass[eid] = bodyParams.mass; + Rigidbody.gravity[eid].set(bodyParams.gravity); + Rigidbody.linearDamping[eid] = bodyParams.linearDamping; + Rigidbody.angularDamping[eid] = bodyParams.angularDamping; + Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold; + Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold; + Rigidbody.angularFactor[eid].set(bodyParams.angularFactor); + Rigidbody.activationState[eid] = bodyParams.activationState; + bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); + bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); + Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionGroup; + Rigidbody.collisionFilterMask[eid] = bodyParams.collisionMask; + Rigidbody.initialCollisionFilterMask[eid] = bodyParams.collisionMask; + bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); + NetworkedRigidBody.prevType[eid] = bodyParams.type; + + return eid; +} + +export enum GLTFRigidBodyType { + STATIC = "static", + DYNAMIC = "dynamic", + KINEMATIC = "kinematic" +} + +export enum GLTFRigidBodyCollisionGroup { + OBJECTS = "objects", + ENVIRONMENT = "environment", + TRIGGERS = "triggers", + AVATARS = "avatars", + MEDIA_FRAMES = "media-frames" +} + +const GLTF_DEFAULTS = { + ...DEFAULTS, + type: GLTFRigidBodyType.DYNAMIC, + collisionGroup: GLTFRigidBodyCollisionGroup.OBJECTS, + collisionMask: [GLTFRigidBodyCollisionGroup.AVATARS] +}; + +const gltfGroupToLayer = { + [GLTFRigidBodyCollisionGroup.OBJECTS]: COLLISION_LAYERS.INTERACTABLES, + [GLTFRigidBodyCollisionGroup.ENVIRONMENT]: COLLISION_LAYERS.ENVIRONMENT, + [GLTFRigidBodyCollisionGroup.TRIGGERS]: COLLISION_LAYERS.TRIGGERS, + [GLTFRigidBodyCollisionGroup.AVATARS]: COLLISION_LAYERS.AVATAR, + [GLTFRigidBodyCollisionGroup.MEDIA_FRAMES]: COLLISION_LAYERS.MEDIA_FRAMES +} as const; + +export interface GLTFRigidBodyParams + extends Partial> { + type?: GLTFRigidBodyType; + collisionGroup?: GLTFRigidBodyCollisionGroup; + collisionMask?: GLTFRigidBodyCollisionGroup[]; +} + +export function inflateGLTFRigidBody(world: HubsWorld, eid: number, params: GLTFRigidBodyParams) { + const bodyParams = Object.assign({}, GLTF_DEFAULTS, params); + + inflateRigidBody(world, eid, { + ...bodyParams, + type: Object.values(GLTFRigidBodyType).indexOf(bodyParams.type), + collisionGroup: gltfGroupToLayer[bodyParams.collisionGroup], + collisionMask: bodyParams.collisionMask.reduce((acc, m) => acc | gltfGroupToLayer[m], 0) + }); return eid; } diff --git a/src/inflators/spawner.ts b/src/inflators/spawner.ts index 78c6be0310..1587fadb39 100644 --- a/src/inflators/spawner.ts +++ b/src/inflators/spawner.ts @@ -42,7 +42,6 @@ export function inflateSpawner(world: HubsWorld, eid: number, props: SpawnerPara inflateRigidBody(world, eid, { mass: 0, - type: Type.STATIC, collisionGroup: COLLISION_LAYERS.INTERACTABLES, collisionMask: COLLISION_LAYERS.DEFAULT_SPAWNER }); diff --git a/src/inflators/text.ts b/src/inflators/text.ts index 418d7a0e43..cc7f38480c 100644 --- a/src/inflators/text.ts +++ b/src/inflators/text.ts @@ -1,10 +1,255 @@ import { addComponent } from "bitecs"; -import { BackSide, DoubleSide, FrontSide } from "three"; +import { BackSide, Color, DoubleSide, FrontSide, Side } from "three"; import { Text as TroikaText } from "troika-three-text"; import { HubsWorld } from "../app"; -import { TextTag } from "../bit-components"; +import { Networked, NetworkedText, TextTag } from "../bit-components"; import { addObject3DComponent } from "../utils/jsx-entity"; +export const ANCHOR_X = { + LEFT: 1 << 0, + CENTER: 1 << 1, + RIGHT: 1 << 2 +}; + +export function anchorXToFlag(anchorX: string) { + switch (anchorX) { + case "center": + return ANCHOR_X.CENTER; + case "left": + return ANCHOR_X.LEFT; + case "right": + return ANCHOR_X.RIGHT; + } + return ANCHOR_X.CENTER; +} + +export function flagToAnchorX(flag: number) { + switch (flag) { + case ANCHOR_X.CENTER: + return "center"; + case ANCHOR_X.LEFT: + return "left"; + case ANCHOR_X.RIGHT: + return "right"; + } + return "center"; +} + +export const ANCHOR_Y = { + TOP: 1 << 0, + TOP_BASELINE: 1 << 1, + TOP_CAP: 1 << 2, + TOP_EX: 1 << 3, + MIDDLE: 1 << 4, + BOTTOM_BASELINE: 1 << 5, + BOTTOM: 1 << 6 +}; + +export function anchorYToFlag(anchorY: string) { + switch (anchorY) { + case "top": + return ANCHOR_Y.TOP; + case "top-baseline": + return ANCHOR_Y.TOP_BASELINE; + case "top-cap": + return ANCHOR_Y.TOP_CAP; + case "top-ex": + return ANCHOR_Y.TOP_EX; + case "middle": + return ANCHOR_Y.MIDDLE; + case "bottom-baseline": + return ANCHOR_Y.BOTTOM_BASELINE; + case "bottom": + return ANCHOR_Y.BOTTOM; + } + return ANCHOR_Y.MIDDLE; +} + +export function flagToAnchorY(flag: number) { + switch (flag) { + case ANCHOR_Y.TOP: + return "top"; + case ANCHOR_Y.TOP_BASELINE: + return "top-baseline"; + case ANCHOR_Y.TOP_CAP: + return "top-cap"; + case ANCHOR_Y.TOP_EX: + return "top-ex"; + case ANCHOR_Y.MIDDLE: + return "middle"; + case ANCHOR_Y.BOTTOM_BASELINE: + return "bottom-baseline"; + case ANCHOR_Y.BOTTOM: + return "bottom"; + } + return "middle"; +} + +export const DIRECTION = { + AUTO: 1 << 0, + LTR: 1 << 1, + RTL: 1 << 2 +}; + +export function directionToFlag(direction: string) { + switch (direction) { + case "auto": + return DIRECTION.AUTO; + case "ltr": + return DIRECTION.LTR; + case "rtl": + return DIRECTION.RTL; + } + return DIRECTION.AUTO; +} + +export function flagToDirection(flag: number) { + switch (flag) { + case DIRECTION.AUTO: + return "auto"; + case DIRECTION.LTR: + return "ltr"; + case DIRECTION.RTL: + return "rtl"; + } + return "auto"; +} + +export const OVERFLOW_WRAP = { + NORMAL: 1 << 0, + BREAK_WORD: 1 << 1 +}; + +export function overflowWrapToFlag(overflowWrap: string) { + switch (overflowWrap) { + case "normal": + return OVERFLOW_WRAP.NORMAL; + case "break-word": + return OVERFLOW_WRAP.BREAK_WORD; + } + return OVERFLOW_WRAP.NORMAL; +} + +export function flagToOverflowWrap(flag: number) { + switch (flag) { + case OVERFLOW_WRAP.NORMAL: + return "normal"; + case OVERFLOW_WRAP.BREAK_WORD: + return "break-word"; + } + return "normal"; +} + +export const SIDE = { + FRONT: 1 << 0, + BACK: 1 << 1, + DOUBLE: 1 << 2 +}; + +export function sideToFlag(side: string) { + switch (side) { + case "front": + return SIDE.FRONT; + case "back": + return SIDE.BACK; + case "double": + return SIDE.DOUBLE; + } + return SIDE.FRONT; +} + +export function flagToSide(flag: number) { + switch (flag) { + case SIDE.FRONT: + return "front"; + case SIDE.BACK: + return "back"; + case SIDE.DOUBLE: + return "double"; + } + return "front"; +} + +export const TEXT_ALIGN = { + LEFT: 1 << 0, + RIGHT: 1 << 1, + CENTER: 1 << 2, + JUSTIFY: 1 << 2 +}; + +export function textAlignToFlag(textAlign: string) { + switch (textAlign) { + case "left": + return TEXT_ALIGN.LEFT; + case "right": + return TEXT_ALIGN.RIGHT; + case "center": + return TEXT_ALIGN.CENTER; + case "justify": + return TEXT_ALIGN.JUSTIFY; + } + return TEXT_ALIGN.CENTER; +} + +export function flagToTextAlign(flag: number) { + switch (flag) { + case TEXT_ALIGN.LEFT: + return "left"; + case TEXT_ALIGN.RIGHT: + return "right"; + case TEXT_ALIGN.CENTER: + return "center"; + case TEXT_ALIGN.JUSTIFY: + return "justify"; + } + return "center"; +} + +export const WHITESPACE = { + NORMAL: 1 << 0, + NO_WRAP: 1 << 1 +}; + +export function whiteSpaceToFlag(whiteSpace: string) { + switch (whiteSpace) { + case "normal": + return WHITESPACE.NORMAL; + case "nowrap": + return WHITESPACE.NO_WRAP; + } + return WHITESPACE.NORMAL; +} + +export function flagToWhiteSpace(flag: number) { + switch (flag) { + case WHITESPACE.NORMAL: + return "normal"; + case WHITESPACE.NO_WRAP: + return "nowrap"; + } + return "normal"; +} + +export function numberOrStringToString(value: number | string) { + if (isNaN(Number(value))) { + return APP.getSid(value as string); + } else { + return APP.getSid(`${value as number}`); + } +} + +export function stringToNumberOrString(value: string): number | string { + if (!value) return 0; + if (value.indexOf("%") !== -1) { + return value; + } else { + return Number(value); + } +} + +export type NumberOrNormalT = number | "normal"; +export type NumberOrPctT = number | `${number}%`; + export type TextParams = { value: string; anchorX?: "left" | "center" | "right"; @@ -13,39 +258,53 @@ export type TextParams = { color?: string; curveRadius?: number; depthOffset?: number; - direction?: "auto" | "ltr" | "trl"; + direction?: "auto" | "ltr" | "rtl"; fillOpacity?: number; fontUrl?: string | null; fontSize?: number; glyphGeometryDetail?: number; gpuAccelerateSDF?: boolean; letterSpacing?: number; - lineHeight?: number | "normal"; + lineHeight?: NumberOrNormalT; maxWidth?: number; opacity?: number; - outlineBlur?: number | `${number}%`; + outlineBlur?: NumberOrPctT; outlineColor?: string; - outlineOffsetX?: number | `${number}%`; - outlineOffsetY?: number | `${number}%`; + outlineOffsetX?: NumberOrPctT; + outlineOffsetY?: NumberOrPctT; outlineOpacity?: number; - outlineWidth?: number | `${number}%`; + outlineWidth?: NumberOrPctT; overflowWrap?: "normal" | "break-word"; sdfGlyphSize?: number | null; side?: "front" | "back" | "double"; strokeColor?: string; strokeOpacity?: number; - strokeWidth?: number | `${number}%`; + strokeWidth?: NumberOrPctT; textAlign?: "left" | "right" | "center" | "justify"; textIndent?: number; whiteSpace?: "normal" | "nowrap"; }; -const THREE_SIDES = { +export const THREE_SIDES = { front: FrontSide, back: BackSide, double: DoubleSide }; +export function sideToThree(side: "front" | "back" | "double"): Side { + return THREE_SIDES[side]; +} + +export const THREE_TO_SIDE = { + [FrontSide]: "front", + [BackSide]: "back", + [DoubleSide]: "double" +}; + +export function threeToSide(side: Side) { + return THREE_TO_SIDE[side]; +} + const DEFAULTS: Required = { anchorX: "center", anchorY: "middle", @@ -92,7 +351,7 @@ const DEFAULTS: Required = { // glTF. If we notice problems with other parameters, we may add // casting of other parameters. // TODO: Add a generic mechanism to cast and validate inflator params. -const cast = (params: Required): Required => { +export const cast = (params: Required): Required => { const keys: Array = [ "curveRadius", "depthOffset", @@ -132,8 +391,8 @@ const cast = (params: Required): Required => { return params; }; -export function inflateText(world: HubsWorld, eid: number, params: TextParams) { - const requiredParams = cast(Object.assign({}, DEFAULTS, params) as Required); +const tmpColor = new Color(); +function createText(requiredParams: Required) { const text = new TroikaText(); text.material!.toneMapped = false; @@ -145,7 +404,7 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) { text.anchorX = requiredParams.anchorX; text.anchorY = requiredParams.anchorY; text.clipRect = requiredParams.clipRect; - text.color = requiredParams.color; + text.color = tmpColor.set(requiredParams.color).getHex(); text.curveRadius = requiredParams.curveRadius; text.depthOffset = requiredParams.depthOffset; text.direction = requiredParams.direction; @@ -173,6 +432,52 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) { text.sync(); + return text; +} + +export function inflateText(world: HubsWorld, eid: number, params: TextParams) { + const requiredParams = Object.assign({}, DEFAULTS, params) as Required; + const text = createText(requiredParams); + + addComponent(world, TextTag, eid); + addObject3DComponent(world, eid, text); +} + +export function inflateGLTFText(world: HubsWorld, eid: number, params: TextParams) { + const requiredParams = Object.assign({}, DEFAULTS, params) as Required; + const text = createText(requiredParams); + addComponent(world, TextTag, eid); + addComponent(world, Networked, eid); + addComponent(world, NetworkedText, eid); addObject3DComponent(world, eid, text); + + NetworkedText.text[eid] = APP.getSid(requiredParams.value); + NetworkedText.fontSize[eid] = requiredParams.fontSize; + NetworkedText.textAlign[eid] = textAlignToFlag(requiredParams.textAlign); + NetworkedText.anchorX[eid] = anchorXToFlag(requiredParams.anchorX); + NetworkedText.anchorY[eid] = anchorYToFlag(requiredParams.anchorY); + NetworkedText.color[eid] = tmpColor.set(requiredParams.color).getHex(); + NetworkedText.letterSpacing[eid] = requiredParams.letterSpacing; + NetworkedText.lineHeight[eid] = numberOrStringToString(requiredParams.lineHeight); + NetworkedText.outlineWidth[eid] = numberOrStringToString(requiredParams.outlineWidth); + NetworkedText.outlineColor[eid] = tmpColor.set(requiredParams.outlineColor).getHex(); + NetworkedText.outlineBlur[eid] = numberOrStringToString(requiredParams.outlineBlur); + NetworkedText.outlineOffsetX[eid] = numberOrStringToString(requiredParams.outlineOffsetX); + NetworkedText.outlineOffsetY[eid] = numberOrStringToString(requiredParams.outlineOffsetY); + NetworkedText.outlineOpacity[eid] = requiredParams.outlineOpacity; + NetworkedText.fillOpacity[eid] = requiredParams.fillOpacity; + NetworkedText.strokeWidth[eid] = numberOrStringToString(requiredParams.strokeWidth); + NetworkedText.strokeColor[eid] = tmpColor.set(requiredParams.strokeColor).getHex(); + NetworkedText.strokeOpacity[eid] = requiredParams.strokeOpacity; + NetworkedText.textIndent[eid] = requiredParams.textIndent; + NetworkedText.whiteSpace[eid] = whiteSpaceToFlag(requiredParams.whiteSpace); + NetworkedText.overflowWrap[eid] = overflowWrapToFlag(requiredParams.overflowWrap); + NetworkedText.opacity[eid] = requiredParams.opacity; + NetworkedText.side[eid] = sideToFlag(requiredParams.side); + NetworkedText.maxWidth[eid] = requiredParams.maxWidth; + NetworkedText.curveRadius[eid] = requiredParams.curveRadius; + NetworkedText.direction[eid] = directionToFlag(requiredParams.direction); + + return eid; } diff --git a/src/inflators/video.ts b/src/inflators/video.ts index 8e087390bb..3bfec5aa50 100644 --- a/src/inflators/video.ts +++ b/src/inflators/video.ts @@ -2,13 +2,16 @@ import { create360ImageMesh, createImageMesh } from "../utils/create-image-mesh" import { addComponent } from "bitecs"; import { addObject3DComponent } from "../utils/jsx-entity"; import { ProjectionMode } from "../utils/projection-mode"; -import { MediaVideo, MediaVideoData } from "../bit-components"; +import { MediaVideo, MediaVideoData, NetworkedVideo } from "../bit-components"; import { HubsWorld } from "../app"; import { EntityID } from "../utils/networking-types"; import { Texture } from "three"; export const VIDEO_FLAGS = { - CONTROLS: 1 << 0 + CONTROLS: 1 << 0, + AUTO_PLAY: 1 << 1, + LOOP: 1 << 2, + PAUSED: 1 << 3 }; export interface VideoParams { diff --git a/src/link.js b/src/link.js index c6fc89f052..beb641e3f7 100644 --- a/src/link.js +++ b/src/link.js @@ -10,10 +10,11 @@ import LinkRoot from "./react-components/link-root"; import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/link", "Hubs Device Link"); +const store = getStore(); const linkChannel = new LinkChannel(store); (async () => { diff --git a/src/load-media-on-paste-or-drop.ts b/src/load-media-on-paste-or-drop.ts index 1d41d0fafe..17b48448fe 100644 --- a/src/load-media-on-paste-or-drop.ts +++ b/src/load-media-on-paste-or-drop.ts @@ -107,6 +107,7 @@ function onDrop(e: DragEvent) { if (qsTruthy("debugLocalScene")) { URL.revokeObjectURL(lastDebugScene); if (!e.dataTransfer?.files.length) return; + e.preventDefault(); const url = URL.createObjectURL(e.dataTransfer.files[0]); APP.hubChannel!.updateScene(url); lastDebugScene = url; diff --git a/src/message-dispatch.js b/src/message-dispatch.js index d555db8208..59cf6f884d 100644 --- a/src/message-dispatch.js +++ b/src/message-dispatch.js @@ -11,7 +11,6 @@ import { createNetworkedEntity } from "./utils/create-networked-entity"; import { add, testAsset, respawn } from "./utils/chat-commands"; import { isLockedDownDemoRoom } from "./utils/hub-utils"; import { loadState, clearState } from "./utils/entity-state-utils"; -import { shouldUseNewLoader } from "./utils/bit-utils"; let uiRoot; // Handles user-entered messages @@ -24,6 +23,15 @@ export default class MessageDispatch extends EventTarget { this.remountUI = remountUI; this.mediaSearchStore = mediaSearchStore; this.presenceLogEntries = []; + this.chatCommands = new Map(); + } + + registerChatCommand(name, callback) { + if (!this.chatCommands.has(name)) { + this.chatCommands.set(name, callback); + } else { + throw Error(`Error registering chat command ${name}: command already registered`); + } } addToPresenceLog(entry) { @@ -140,22 +148,6 @@ export default class MessageDispatch extends EventTarget { this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK); } break; - case "duck": - if (shouldUseNewLoader()) { - const avatarPov = document.querySelector("#avatar-pov-node").object3D; - const eid = createNetworkedEntity(APP.world, "duck"); - const obj = APP.world.eid2obj.get(eid); - obj.position.copy(avatarPov.localToWorld(new THREE.Vector3(0, 0, -1.5))); - obj.lookAt(avatarPov.getWorldPosition(new THREE.Vector3())); - } else { - spawnChatMessage(getAbsoluteHref(location.href, ducky)); - } - if (Math.random() < 0.01) { - this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_SPECIAL_QUACK); - } else { - this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK); - } - break; case "cube": { const avatarPov = document.querySelector("#avatar-pov-node").object3D; const eid = createNetworkedEntity(APP.world, "cube"); @@ -269,5 +261,9 @@ export default class MessageDispatch extends EventTarget { } break; } + + if (this.chatCommands.has(command)) { + this.chatCommands.get(command)(APP, args); + } }; } diff --git a/src/prefabs/duck.tsx b/src/prefabs/duck.tsx deleted file mode 100644 index 380053c7d0..0000000000 --- a/src/prefabs/duck.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** @jsx createElementEntity */ -import { createElementEntity, EntityDef } from "../utils/jsx-entity"; -import { COLLISION_LAYERS } from "../constants"; -import { FLOATY_OBJECT_FLAGS } from "../systems/floaty-object-system"; -import ducky from "../assets/models/DuckyMesh.glb"; -import { getAbsoluteHref } from "../utils/media-url-utils"; -import { Fit, Shape } from "../inflators/physics-shape"; - -export function DuckPrefab(): EntityDef { - return ( - - ); -} diff --git a/src/prefabs/loading-object.js b/src/prefabs/loading-object.js index 68c488725a..673c799535 100644 --- a/src/prefabs/loading-object.js +++ b/src/prefabs/loading-object.js @@ -5,6 +5,7 @@ import { createElementEntity } from "../utils/jsx-entity"; import { loadModel } from "../components/gltf-model-plus"; import loadingObjectSrc from "../assets/models/LoadingObject_Atom.glb"; import { cloneObject3D, disposeNode } from "../utils/three-utils"; +import { LOOP_ANIMATION_DEFAULTS } from "../inflators/loop-animation"; // TODO We should have an explicit "preload assets" step let loadingObject = new Mesh(new BoxGeometry(), new MeshBasicMaterial()); @@ -16,5 +17,12 @@ loadModel(loadingObjectSrc, null, true).then(gltf => { // TODO: Do we really need to clone the loadingObject every time? // Should we use a pool? export function LoadingObject() { - return ; + return ( + + ); } diff --git a/src/prefabs/prefabs.ts b/src/prefabs/prefabs.ts index 1584653614..915da578f6 100644 --- a/src/prefabs/prefabs.ts +++ b/src/prefabs/prefabs.ts @@ -1,14 +1,8 @@ -import { MediaLoaderParams } from "../inflators/media-loader"; import { CameraPrefab, CubeMediaFramePrefab } from "../prefabs/camera-tool"; import { MediaPrefab } from "../prefabs/media"; -import { EntityDef } from "../utils/jsx-entity"; -import { DuckPrefab } from "./duck"; +import { PrefabDefinitionT, PrefabNameT } from "../types"; -type CameraPrefabT = () => EntityDef; -type CubeMediaPrefabT = () => EntityDef; -type MediaPrefabT = (params: MediaLoaderParams) => EntityDef; - -type Permission = +export type Permission = | "spawn_camera" | "spawn_and_move_media" | "update_hub" @@ -22,15 +16,7 @@ type Permission = | "kick_users" | "mute_users"; -export type PrefabDefinition = { - permission: Permission; - template: CameraPrefabT | CubeMediaPrefabT | MediaPrefabT; -}; - -export type PrefabName = "camera" | "cube" | "media" | "duck"; - -export const prefabs = new Map(); +export const prefabs = new Map(); prefabs.set("camera", { permission: "spawn_camera", template: CameraPrefab }); prefabs.set("cube", { permission: "spawn_and_move_media", template: CubeMediaFramePrefab }); prefabs.set("media", { permission: "spawn_and_move_media", template: MediaPrefab }); -prefabs.set("duck", { permission: "spawn_and_move_media", template: DuckPrefab }); diff --git a/src/react-components/debug-panel/ECSSidebar.js b/src/react-components/debug-panel/ECSSidebar.js index 1793990ad8..e76246d6e3 100644 --- a/src/react-components/debug-panel/ECSSidebar.js +++ b/src/react-components/debug-panel/ECSSidebar.js @@ -70,9 +70,28 @@ function MaterialItem(props) { ); } +function TextureItem(props) { + const { tex, setSelectedObj } = props; + const displayName = formatObjectName(tex); + return ( +
+
{ + e.preventDefault(); + setSelectedObj(tex); + }} + > + {displayName} + {` [${tex.eid}]`} +
+
+ ); +} + export function formatComponentProps(eid, component) { const formatted = Object.keys(component).reduce((str, k, i, arr) => { - const val = component[k][eid]; + const val = component[k] instanceof Map ? component[k].get(eid) : component[k][eid]; const isStr = component[k][bitComponents.$isStringType]; str += ` ${k}: `; if (ArrayBuffer.isView(val)) { @@ -144,8 +163,10 @@ function RefreshButton({ onClick }) { ); } +export const extraSections = new Array(); const object3dQuery = defineQuery([bitComponents.Object3DTag]); const materialQuery = defineQuery([bitComponents.MaterialTag]); +const textureQuery = defineQuery([bitComponents.TextureTag]); function ECSDebugSidebar({ onClose, toggleObjExpand, @@ -159,6 +180,8 @@ function ECSDebugSidebar({ .map(eid => APP.world.eid2obj.get(eid)) .filter(o => !o.parent); const materials = materialQuery(APP.world).map(eid => APP.world.eid2mat.get(eid)); + const textures = textureQuery(APP.world).map(eid => APP.world.eid2tex.get(eid)); + const envRoot = document.getElementById("environment-root").object3D; return ( +
+ +
{orphaned.map(o => (
- {materials.map(m => ( - - ))} + + + + {materials.map(m => m && )} +
+
+ + + + {textures.map(t => t && )}
+ {extraSections.map(section => section(APP.world, setSelectedObj))}
{selectedObj && }
diff --git a/src/react-components/home/HomePage.js b/src/react-components/home/HomePage.js index bbe3480de2..691b6e110d 100644 --- a/src/react-components/home/HomePage.js +++ b/src/react-components/home/HomePage.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import classNames from "classnames"; import configs from "../../utils/configs"; @@ -28,12 +28,13 @@ export function HomePage() { const { results: favoriteRooms } = useFavoriteRooms(); const { results: publicRooms } = usePublicRooms(); + const qs = useMemo(() => new URLSearchParams(location.search), []); + const [newScene] = useState(qs.has("new")); + const sortedFavoriteRooms = Array.from(favoriteRooms).sort((a, b) => b.member_count - a.member_count); const sortedPublicRooms = Array.from(publicRooms).sort((a, b) => b.member_count - a.member_count); const wrapInBold = chunk => {chunk}; useEffect(() => { - const qs = new URLSearchParams(location.search); - // Support legacy sign in urls. if (qs.has("sign_in")) { const redirectUrl = new URL("/signin", window.location); @@ -49,142 +50,144 @@ export function HomePage() { qs.delete("new"); createAndRedirectToNewHub(null, null, true, qs); } - }, []); + }, [qs]); const canCreateRooms = !configs.feature("disable_room_creation") || auth.isAdmin; const email = auth.email; return ( - - -
- {auth.isSignedIn ? ( -
- - - - - - + !newScene && ( + + +
+ {auth.isSignedIn ? ( +
+ + + + + + +
+ ) : ( + + )} +
+
- ) : ( - - )} -
- -
-
-
{configs.translation("app-description")}
- {canCreateRooms && } - -
-
- {intl.formatMessage( -
-
-
- {configs.feature("show_feature_panels") && ( - - - -

- -

-

- +

{configs.translation("app-description")}
+ {canCreateRooms && } + +
+
+ {intl.formatMessage( -

- - - -

- +

+
+
+ {configs.feature("show_feature_panels") && ( + + + +

+ +

+

+ +

+
+ + +

+ +

+

+ +

+
+ + +

+ +

+

+ +

+
+
+ )} + {sortedPublicRooms.length > 0 && ( + +

+

-

- -

- - - -

- + + + {sortedPublicRooms.map(room => { + return ( + + scaledThumbnailUrlFor(entry.images.preview.url, width, height) + } + /> + ); + })} + + + + )} + {sortedFavoriteRooms.length > 0 && ( + +

+

-

- -

+ + + {sortedFavoriteRooms.map(room => { + return ( + + scaledThumbnailUrlFor(entry.images.preview.url, width, height) + } + /> + ); + })} + + +
+ )} + {isHmc() ? ( + + - - )} - {sortedPublicRooms.length > 0 && ( - -

- -

- - - {sortedPublicRooms.map(room => { - return ( - - scaledThumbnailUrlFor(entry.images.preview.url, width, height) - } - /> - ); - })} - - -
- )} - {sortedFavoriteRooms.length > 0 && ( - -

- -

- - - {sortedFavoriteRooms.map(room => { - return ( - - scaledThumbnailUrlFor(entry.images.preview.url, width, height) - } - /> - ); - })} - - -
- )} - {isHmc() ? ( - - - - ) : null} - + ) : null} + + ) ); } diff --git a/src/react-components/preferences-screen.js b/src/react-components/preferences-screen.js index 9d577c57c0..507568ce6b 100644 --- a/src/react-components/preferences-screen.js +++ b/src/react-components/preferences-screen.js @@ -27,6 +27,7 @@ import { isLockedDownDemoRoom } from "../utils/hub-utils"; import dropdownArrowUrl from "../assets/images/dropdown_arrow.png"; import dropdownArrow2xUrl from "../assets/images/dropdown_arrow@2x.png"; import { PermissionNotification } from "./room/PermissionNotifications"; +import { getAddonsPreferencesCategories, getAddonsPreferencesLabels } from "../addons"; export const CLIPPING_THRESHOLD_MIN = 0.0; export const CLIPPING_THRESHOLD_MAX = 0.1; @@ -582,12 +583,26 @@ class PreferenceListItem extends Component { const isCheckbox = this.props.itemProps.prefType === PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX; const isCustomComponent = this.props.itemProps.prefType === PREFERENCE_LIST_ITEM_TYPE.CUSTOM_COMPONENT; const isSmallScreen = window.innerWidth < 600; - const label = preferenceLabels[this.props.storeKey] && ( + let labelText; + if (preferenceLabels[this.props.storeKey]) { + labelText = intl.formatMessage(preferenceLabels[this.props.storeKey]); + } else { + const addonLabels = getAddonsPreferencesLabels(); + labelText = addonLabels.get(this.props.storeKey); + } + let labelTooltip; + if (this.props.itemProps.tooltipKey) { + labelTooltip = intl.formatMessage(preferenceLabels[this.props.itemProps.tooltipKey]); + } else { + const addonLabels = getAddonsPreferencesLabels(); + labelTooltip = addonLabels.get(this.props.storeKey); + } + const label = ( - {intl.formatMessage(preferenceLabels[this.props.storeKey])} + {labelText} ); const prefSchema = this.props.store.schema.definitions.preferences.properties; @@ -695,7 +710,8 @@ const CATEGORY_MOVEMENT = 3; const CATEGORY_TOUCHSCREEN = 4; const CATEGORY_ACCESSIBILITY = 5; const CATEGORY_GRAPHICS = 6; -const TOP_LEVEL_CATEGORIES = [CATEGORY_AUDIO, CATEGORY_CONTROLS, CATEGORY_MISC]; +const CATEGORY_ADDONS = 7; +const TOP_LEVEL_CATEGORIES = [CATEGORY_AUDIO, CATEGORY_CONTROLS, CATEGORY_MISC, CATEGORY_ADDONS]; const categoryNames = defineMessages({ [CATEGORY_AUDIO]: { id: "preferences-screen.category.audio", defaultMessage: "Audio" }, [CATEGORY_CONTROLS]: { id: "preferences-screen.category.controls", defaultMessage: "Controls" }, @@ -703,7 +719,8 @@ const categoryNames = defineMessages({ [CATEGORY_MOVEMENT]: { id: "preferences-screen.category.movement", defaultMessage: "Movement" }, [CATEGORY_TOUCHSCREEN]: { id: "preferences-screen.category.touchscreen", defaultMessage: "Touchscreen" }, [CATEGORY_ACCESSIBILITY]: { id: "preferences-screen.category.accessibility", defaultMessage: "Accessibility" }, - [CATEGORY_GRAPHICS]: { id: "preferences-screen.category.graphics", defaultMessage: "Graphics" } + [CATEGORY_GRAPHICS]: { id: "preferences-screen.category.graphics", defaultMessage: "Graphics" }, + [CATEGORY_ADDONS]: { id: "preferences-screen.category.add-ons", defaultMessage: "Add-Ons" } }); function NavItem({ ariaLabel, title, onClick, selected }) { @@ -1427,6 +1444,11 @@ class PreferencesScreen extends Component { ); } + const addOnCategories = []; + getAddonsPreferencesCategories(APP).forEach((prefItems, prefCatName) => { + addOnCategories.push({ name: prefCatName, items: prefItems.map(toItem).filter(item => !!item) }); + }); + return new Map([ [ CATEGORY_AUDIO, @@ -1460,7 +1482,8 @@ class PreferencesScreen extends Component { items: items.get(CATEGORY_GRAPHICS) } ] - ] + ], + [CATEGORY_ADDONS, addOnCategories] ]); } diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index 8495344336..b75ab7ddb0 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -327,6 +327,8 @@ export function formatSystemMessage(entry, intl) { values={{ hubName: {entry.hubName} }} /> ); + case "script_message": + return "script: " + entry.msg; case "log": return intl.formatMessage(logMessages[entry.messageType], entry.props); default: diff --git a/src/react-components/room/RoomSettingsSidebar.js b/src/react-components/room/RoomSettingsSidebar.js index 319350dc74..6edc8dd18f 100644 --- a/src/react-components/room/RoomSettingsSidebar.js +++ b/src/react-components/room/RoomSettingsSidebar.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import PropTypes from "prop-types"; import { useForm } from "react-hook-form"; import styles from "./RoomSettingsSidebar.scss"; @@ -20,6 +20,8 @@ import { canShare, shareInviteUrl } from "../../utils/share"; import { ReactComponent as ShareIcon } from "../icons/Share.svg"; import { Checkbox } from "@mozilla/lilypad-ui"; import configs from "../../utils/configs"; +import { addons, isAddonEnabled } from "../../addons"; +import { shouldUseNewLoader } from "../../hubs"; export function RoomSettingsSidebar({ showBackButton, @@ -58,6 +60,22 @@ export function RoomSettingsSidebar({ const [isShareInEnglish, setIsShareInEnglish] = useState(false); + const handleAddonChange = useCallback( + evt => { + setValue(`user_data.addons.${evt.target.id}`, evt.target.checked); + }, + [setValue] + ); + + const [bitECSLoaderEnabled, setBitECSLoaderEnabled] = useState(shouldUseNewLoader()); + const handleBitECSChange = useCallback( + evt => { + setValue("user_data.hubs_use_bitecs_based_client", evt.target.checked); + setBitECSLoaderEnabled(evt.target.checked); + }, + [setValue, setBitECSLoaderEnabled] + ); + return ( } @@ -245,15 +263,37 @@ export function RoomSettingsSidebar({ defaultMessage="Enable bitECS based Client" /> } + defaultChecked={shouldUseNewLoader()} + onChange={handleBitECSChange} description={ } - {...register("user_data.hubs_use_bitecs_based_client")} /> + } fullWidth> + {!bitECSLoaderEnabled && ( + + )} + {[...addons.entries()].map(([id, addon]) => ( + + ))} + diff --git a/src/react-components/room/RoomSettingsSidebar.scss b/src/react-components/room/RoomSettingsSidebar.scss index 720b13f2ad..ef318f534b 100644 --- a/src/react-components/room/RoomSettingsSidebar.scss +++ b/src/react-components/room/RoomSettingsSidebar.scss @@ -1,6 +1,7 @@ @use "../styles/theme.scss"; -:local(.room-permissions), :local(.permissions-group) { +:local(.room-permissions), +:local(.permissions-group) { margin-left: 20px; & > * { @@ -15,4 +16,12 @@ :local(.confirm-revoke-button) { display: inline; color: theme.$link-color; -} \ No newline at end of file +} + +:local(.label) { + margin-bottom: 8px; + color: theme.$red; + align-self: flex-start; + font-weight: theme.$font-weight-bold; + font-size: theme.$font-size-sm; +} diff --git a/src/react-components/room/RoomSidebar.js b/src/react-components/room/RoomSidebar.js index 98438a0b1c..f722075a8b 100644 --- a/src/react-components/room/RoomSidebar.js +++ b/src/react-components/room/RoomSidebar.js @@ -70,12 +70,12 @@ function SceneAttribution({ attribution }) { } SceneAttribution.propTypes = { - attribution: { + attribution: PropTypes.shape({ name: PropTypes.string, title: PropTypes.string, author: PropTypes.string, url: PropTypes.string - } + }) }; // To assist with content control, we avoid displaying scene links to users who are not the scene diff --git a/src/react-components/room/contexts/ChatContext.tsx b/src/react-components/room/contexts/ChatContext.tsx index bb56c0f22e..6ba664e28b 100644 --- a/src/react-components/room/contexts/ChatContext.tsx +++ b/src/react-components/room/contexts/ChatContext.tsx @@ -81,6 +81,7 @@ function updateMessageGroups(messageGroups: any[], newMessage: NewMessageT) { case "scene_changed": case "hub_name_changed": case "hub_changed": + case "script_message": case "log": return [ ...messageGroups, diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index c1dc959dbe..56f64ee280 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -153,6 +153,7 @@ class UIRoot extends Component { initialIsFavorited: PropTypes.bool, showSignInDialog: PropTypes.bool, showBitECSBasedClientRefreshPrompt: PropTypes.bool, + showAddonRefreshPrompt: PropTypes.bool, signInMessage: PropTypes.object, onContinueAfterSignIn: PropTypes.func, showSafariMicDialog: PropTypes.bool, @@ -1705,6 +1706,14 @@ class UIRoot extends Component { /> )} + {this.props.showAddonRefreshPrompt && ( +
+ +
+ )} ); diff --git a/src/scene.js b/src/scene.js index acd784692b..aab334b4e9 100644 --- a/src/scene.js +++ b/src/scene.js @@ -10,7 +10,7 @@ import registerTelemetry from "./telemetry"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; import { connectToReticulum, fetchReticulumAuthenticatedWithToken } from "./utils/phoenix-utils"; import "./utils/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; function mountUI(props = {}) { const container = document.getElementById("ui-root"); @@ -89,6 +89,7 @@ function onReady() { disableiOSZoom(); + const store = getStore(); const sceneId = parseSceneId(document.location); console.log(`Scene ID: ${sceneId}`); remountUI({ sceneId, store }); diff --git a/src/schema.toml b/src/schema.toml index f36c360117..495c52c1f9 100644 --- a/src/schema.toml +++ b/src/schema.toml @@ -36,6 +36,8 @@ features.disable_room_creation = { category = "rooms", type = "boolean", name = features.require_account_for_join = { category = "rooms", type = "boolean", name = "Require accounts for room access", description = "Require accounts for accessing rooms." } features.default_room_size = { category = "rooms", type = "number", name = "Default room size", description = "Default room size for new rooms. This does not include users in the lobby." } features.max_room_size = { category = "rooms", type = "number", name = "Maximum room size", description = "Maximum room size visitors can set." } +features.bitecs_loader = { category = "features", type="boolean", name="Use BitECS loader", description="Use the BitECS based loader by default in all rooms" } +features.addons_config = { category = "features", type="json", name="Add-ons config JSON", description="Add-ons config JSON file" } features.show_feature_panels = { category = "features", type = "boolean", internal = "true" } features.show_join_us_dialog = { category = "features", type = "boolean", internal = "true" } @@ -92,5 +94,5 @@ links.promotion = { category = "links", type = "string", name = "Promotion Info" links.remixing = { category = "links", type = "string", name = "Remixing Info", description = "Link to info about remixing info and licensing."} links.model_collection = { category = "links", type = "string", name = "Model Collection", description = "Link to a collection of recommended models."} -auth.login_subject = { category = "auth", type="string", name="Magic Link Email Subject", description="Customize the email subject line for users logging in" } +login_subject = { category = "auth", type="string", name="Magic Link Email Subject", description="Customize the email subject line for users logging in" } auth.login_body = { category = "auth", type="longstring", name="Magic Link Email Body", description="Customize message. Add '{{ link }}' to insert the magic link, otherwise it will be appended at the end." } diff --git a/src/signin.js b/src/signin.js index 2337e18c5b..122abed7cc 100644 --- a/src/signin.js +++ b/src/signin.js @@ -10,10 +10,11 @@ import "./react-components/styles/global.scss"; import "./assets/stylesheets/globals.scss"; import { Center } from "./react-components/layout/Center"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/signin", "Hubs Sign In Page"); +const store = getStore(); window.APP = { store }; function SignInRoot() { diff --git a/src/systems/bit-constraints-system.js b/src/systems/bit-constraints-system.js index 1fa79aea1a..d75ed8a7b1 100644 --- a/src/systems/bit-constraints-system.js +++ b/src/systems/bit-constraints-system.js @@ -19,8 +19,10 @@ import { ConstraintHandLeft, ConstraintHandRight, ConstraintRemoteLeft, - ConstraintRemoteRight + ConstraintRemoteRight, + NetworkedRigidBody } from "../bit-components"; +import { Type, getBodyFromRigidBody, getBodyTypeFromType } from "../inflators/rigid-body"; const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]); const queryEnterRemoteRight = enterQuery(queryRemoteRight); @@ -45,7 +47,7 @@ function add(world, physicsSystem, interactor, constraintComponent, entities) { for (let i = 0; i < entities.length; i++) { const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor)); if (!entityExists(world, eid)) continue; - physicsSystem.updateRigidBodyOptions(eid, grabBodyOptions); + physicsSystem.updateRigidBody(eid, grabBodyOptions); physicsSystem.addConstraint(interactor, Rigidbody.bodyId[eid], Rigidbody.bodyId[interactor], {}); addComponent(world, Constraint, eid); addComponent(world, constraintComponent, eid); @@ -57,8 +59,16 @@ function remove(world, offersConstraint, constraintComponent, physicsSystem, int const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor)); if (!entityExists(world, eid)) continue; if (hasComponent(world, offersConstraint, entities[i]) && hasComponent(world, Rigidbody, eid)) { - physicsSystem.updateRigidBodyOptions(eid, releaseBodyOptions); + physicsSystem.updateRigidBody(eid, { + type: getBodyTypeFromType(NetworkedRigidBody.prevType[eid]), + ...releaseBodyOptions + }); physicsSystem.removeConstraint(interactor); + if (Rigidbody.type[eid] === Type.DYNAMIC) { + physicsSystem.activateBody(Rigidbody.bodyId[eid]); + // This shouldn't be necessary but for some reason it doesn't activate the body if we don't update the body afterwards + physicsSystem.updateRigidBody(eid, getBodyFromRigidBody(eid)); + } removeComponent(world, constraintComponent, eid); if ( !hasComponent(world, ConstraintHandLeft, eid) && diff --git a/src/systems/bit-media-frames.js b/src/systems/bit-media-frames.js index f56ed3ad79..c5f57b5d18 100644 --- a/src/systems/bit-media-frames.js +++ b/src/systems/bit-media-frames.js @@ -19,29 +19,31 @@ import { MediaContentBounds, MediaFrame, MediaImage, - MediaLoaded, MediaPDF, MediaVideo, Networked, NetworkedMediaFrame, Owned, - Rigidbody + Rigidbody, + Holdable } from "../bit-components"; import { MediaType } from "../utils/media-utils"; import { cloneObject3D, disposeNode, setMatrixWorld } from "../utils/three-utils"; import { takeOwnership } from "../utils/take-ownership"; import { takeSoftOwnership } from "../utils/take-soft-ownership"; -import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils"; +import { findAncestorWithComponent, findChildWithComponent, findChildrenWithComponent } from "../utils/bit-utils"; import { addObject3DComponent } from "../utils/jsx-entity"; import { updateMaterials } from "../utils/material-utils"; import { MEDIA_FRAME_FLAGS, AxisAlignType } from "../inflators/media-frame"; import { Matrix4, NormalBlending, Quaternion, RGBAFormat, Vector3 } from "three"; +import { COLLISION_LAYERS } from "../constants"; +import { HOLDABLE_FLAGS } from "../inflators/holdable"; const EMPTY_COLOR = 0x6fc0fd; const HOVER_COLOR = 0x2f80ed; const FULL_COLOR = 0x808080; -const mediaFramesQuery = defineQuery([MediaFrame]); +const mediaFramesQuery = defineQuery([MediaFrame, NetworkedMediaFrame]); const enteredMediaFramesQuery = enterQuery(mediaFramesQuery); const exitedMediaFramesQuery = exitQuery(mediaFramesQuery); @@ -54,11 +56,19 @@ function mediaTypeMaskFor(world, eid) { mediaTypeMask |= el.components["media-image"] && MediaType.IMAGE; mediaTypeMask |= el.components["media-pdf"] && MediaType.PDF; } else { - const mediaEid = findChildWithComponent(world, MediaLoaded, eid); - mediaTypeMask |= hasComponent(world, GLTFModel, mediaEid) && MediaType.MODEL; - mediaTypeMask |= hasComponent(world, MediaVideo, mediaEid) && MediaType.VIDEO; - mediaTypeMask |= hasComponent(world, MediaImage, mediaEid) && MediaType.IMAGE; - mediaTypeMask |= hasComponent(world, MediaPDF, mediaEid) && MediaType.PDF; + const rigidBody = findAncestorWithComponent(world, Rigidbody, eid); + if (rigidBody && Rigidbody.collisionFilterMask[rigidBody] & COLLISION_LAYERS.MEDIA_FRAMES) { + const interactable = findChildWithComponent(world, Holdable, eid); + if (interactable) { + mediaTypeMask |= hasComponent(world, GLTFModel, interactable) && MediaType.MODEL; + mediaTypeMask |= hasComponent(world, MediaVideo, interactable) && MediaType.VIDEO; + mediaTypeMask |= hasComponent(world, MediaImage, interactable) && MediaType.IMAGE; + mediaTypeMask |= hasComponent(world, MediaPDF, interactable) && MediaType.PDF; + if (mediaTypeMask === 0) { + mediaTypeMask |= MediaType.MODEL; + } + } + } } return mediaTypeMask; } @@ -320,40 +330,68 @@ export function mediaFramesSystem(world, physicsSystem) { const isFrameDeleting = findAncestorWithComponent(world, Deleting, frame); const isFrameOwned = hasComponent(world, Owned, frame); - if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) { - snapToFrame(world, frame, capturedEid); - physicsSystem.updateRigidBodyOptions(capturedEid, { type: "kinematic" }); - } else if ( - (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) || - (capturedEid && isCapturedOwned && !isCapturedColliding) || - isFrameDeleting - ) { - takeOwnership(world, frame); - NetworkedMediaFrame.capturedNid[frame] = 0; - NetworkedMediaFrame.scale[frame].set(zero); - // TODO BUG: If an entity I do not own is capturedEid by the media frame, - // and then I take ownership of the entity (by grabbing it), - // the physics system does not immediately notice the entity isCapturedColliding with the frame, - // so I immediately think the frame should be emptied. - } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) { + if (!hasComponent(world, Owned, frame)) { + if (MediaFrame.flags[frame] !== NetworkedMediaFrame.flags[frame]) { + MediaFrame.flags[frame] = NetworkedMediaFrame.flags[frame]; + } + } + + if (!hasComponent(world, Owned, frame)) { + if (MediaFrame.mediaType[frame] !== NetworkedMediaFrame.mediaType[frame]) { + MediaFrame.mediaType[frame] = NetworkedMediaFrame.mediaType[frame]; + } + } + + if (capturedEid) { + const grabbables = findChildrenWithComponent(world, Holdable, capturedEid); + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.LOCKED) { + grabbables.forEach(eid => (Holdable.flags[eid] &= ~HOLDABLE_FLAGS.ENABLED)); + } else { + grabbables.forEach(eid => (Holdable.flags[eid] |= HOLDABLE_FLAGS.ENABLED)); + } + } + + if ((MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) === 0) { NetworkedMediaFrame.capturedNid[frame] = 0; NetworkedMediaFrame.scale[frame].set(zero); - } else if (!NetworkedMediaFrame.capturedNid[frame]) { - const capturable = getCapturableEntity(world, physicsSystem, frame); - if ( - capturable && - (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) && - !findChildWithComponent(world, Held, capturable) && - !inOtherFrame(world, frame, capturable) + } + + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) { + if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) { + snapToFrame(world, frame, capturedEid); + physicsSystem.updateRigidBody(capturedEid, { type: "kinematic" }); + } else if ( + (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) || + (capturedEid && isCapturedOwned && !isCapturedColliding) || + isFrameDeleting ) { takeOwnership(world, frame); - takeOwnership(world, capturable); - NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable]; - const obj = world.eid2obj.get(capturable); - obj.updateMatrices(); - tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]); - snapToFrame(world, frame, capturable); - physicsSystem.updateRigidBodyOptions(capturable, { type: "kinematic" }); + NetworkedMediaFrame.capturedNid[frame] = 0; + NetworkedMediaFrame.scale[frame].set(zero); + // TODO BUG: If an entity I do not own is capturedEid by the media frame, + // and then I take ownership of the entity (by grabbing it), + // the physics system does not immediately notice the entity isCapturedColliding with the frame, + // so I immediately think the frame should be emptied. + } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) { + NetworkedMediaFrame.capturedNid[frame] = 0; + NetworkedMediaFrame.scale[frame].set(zero); + } else if (!NetworkedMediaFrame.capturedNid[frame]) { + const capturable = getCapturableEntity(world, physicsSystem, frame); + if ( + capturable && + (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) && + !findChildWithComponent(world, Held, capturable) && + !inOtherFrame(world, frame, capturable) + ) { + takeOwnership(world, frame); + takeOwnership(world, capturable); + NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable]; + const obj = world.eid2obj.get(capturable); + obj.updateMatrices(); + tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]); + snapToFrame(world, frame, capturable); + physicsSystem.updateRigidBody(capturable, { type: "kinematic" }); + } } } @@ -366,12 +404,14 @@ export function mediaFramesSystem(world, physicsSystem) { // TODO: If you are resetting scale because you lost a race for the frame, // you should probably also move the object away from the frame. setMatrixScale(world.eid2obj.get(capturedEid), MediaFrame.scale[frame]); - physicsSystem.updateRigidBodyOptions(capturedEid, { type: "dynamic" }); + physicsSystem.updateRigidBody(capturedEid, { type: "dynamic" }); } MediaFrame.capturedNid[frame] = NetworkedMediaFrame.capturedNid[frame]; MediaFrame.scale[frame].set(NetworkedMediaFrame.scale[frame]); - display(world, physicsSystem, frame, capturedEid, heldMediaTypes); + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) { + display(world, physicsSystem, frame, capturedEid, heldMediaTypes); + } } } diff --git a/src/systems/bit-physics.ts b/src/systems/bit-physics.ts index a07c325956..d9b891d2c4 100644 --- a/src/systems/bit-physics.ts +++ b/src/systems/bit-physics.ts @@ -1,10 +1,12 @@ import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent, Not } from "bitecs"; -import { Object3DTag, Rigidbody, PhysicsShape, AEntity } from "../bit-components"; +import { Object3DTag, Rigidbody, PhysicsShape, AEntity, TextTag } from "../bit-components"; import { getShapeFromPhysicsShape } from "../inflators/physics-shape"; -import { findAncestorWithComponent } from "../utils/bit-utils"; +import { findAncestorWithComponent, hasAnyComponent } from "../utils/bit-utils"; import { getBodyFromRigidBody } from "../inflators/rigid-body"; import { HubsWorld } from "../app"; import { PhysicsSystem } from "./physics-system"; +import { EntityID } from "../utils/networking-types"; +import { Object3D } from "three"; const rigidbodyQuery = defineQuery([Rigidbody, Object3DTag, Not(AEntity)]); const rigidbodyEnteredQuery = enterQuery(rigidbodyQuery); @@ -13,23 +15,49 @@ const shapeQuery = defineQuery([PhysicsShape]); const shapeEnterQuery = enterQuery(shapeQuery); const shapeExitQuery = exitQuery(shapeQuery); +// We don't want to add physics shape for some child entities +// ie. If the object already has a physics shape we don't want to create another one. +// If the object has a text component, we don't want to create a physics shape for it. +const NO_PHYSICS_COMPONENTS = [Rigidbody, TextTag]; + function addPhysicsShapes(world: HubsWorld, physicsSystem: PhysicsSystem, eid: number) { const bodyId = PhysicsShape.bodyId[eid]; const obj = world.eid2obj.get(eid)!; - const shape = getShapeFromPhysicsShape(eid); - const shapeId = physicsSystem.addShapes(bodyId, obj, shape); - PhysicsShape.shapeId[eid] = shapeId; + + // Avoid adding physics shapes for child entities with specific components. + if (obj) { + const hidden = new Map(); + obj.traverse((child: Object3D) => { + if (child.eid! !== eid && hasAnyComponent(world, NO_PHYSICS_COMPONENTS, child.eid!)) { + hidden.set(child.eid!, child.parent!.eid!); + } + }); + for (let child of hidden.keys()) { + const childObj = world.eid2obj.get(child)!; + childObj.removeFromParent(); + } + + const shape = getShapeFromPhysicsShape(eid); + const shapeId = physicsSystem.addShapes(bodyId, obj, shape); + PhysicsShape.shapeId[eid] = shapeId; + + hidden.forEach((parent: EntityID, child: EntityID) => { + const parentObj = world.eid2obj.get(parent)!; + const childObj = world.eid2obj.get(child)!; + parentObj.add(childObj); + }); + } } export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSystem) => { - rigidbodyEnteredQuery(world).forEach(eid => { + rigidbodyEnteredQuery(world).forEach((eid: EntityID) => { const obj = world.eid2obj.get(eid); const body = getBodyFromRigidBody(eid); const bodyId = physicsSystem.addBody(obj, body); Rigidbody.bodyId[eid] = bodyId; }); - shapeEnterQuery(world).forEach(eid => { + shapeEnterQuery(world).forEach((eid: EntityID) => { const bodyEid = findAncestorWithComponent(world, Rigidbody, eid); if (bodyEid) { PhysicsShape.bodyId[eid] = Rigidbody.bodyId[bodyEid]; @@ -39,9 +67,11 @@ export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSyst } }); - shapeExitQuery(world).forEach(eid => physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid])); + shapeExitQuery(world).forEach((eid: EntityID) => + physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]) + ); - rigidbodyExitedQuery(world).forEach(eid => { + rigidbodyExitedQuery(world).forEach((eid: EntityID) => { if (entityExists(world, eid) && hasComponent(world, PhysicsShape, eid)) { physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]); // The PhysicsShape is still on this entity! diff --git a/src/systems/floaty-object-system.js b/src/systems/floaty-object-system.js index f857fa8e70..82db35efc8 100644 --- a/src/systems/floaty-object-system.js +++ b/src/systems/floaty-object-system.js @@ -53,7 +53,7 @@ function makeKinematicOnRelease(world) { const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; makeKinematicOnReleaseExitQuery(world).forEach(eid => { if (!entityExists(world, eid) || !hasComponent(world, Owned, eid)) return; - physicsSystem.updateRigidBodyOptions(eid, { type: "kinematic" }); + physicsSystem.updateRigidBody(eid, { type: "kinematic" }); }); } @@ -73,17 +73,17 @@ export const floatyObjectSystem = world => { const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; enteredFloatyObjectsQuery(world).forEach(eid => { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { type: "kinematic", gravity: { x: 0, y: 0, z: 0 } }); }); enterHeldFloatyObjectsQuery(world).forEach(eid => { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: 0, z: 0 }, type: "dynamic", - collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES + collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS }); }); @@ -95,17 +95,17 @@ export const floatyObjectSystem = world => { const bodyData = physicsSystem.bodyUuidToData.get(bodyId); if (FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.MODIFY_GRAVITY_ON_RELEASE) { if (bodyData.linearVelocity < 1.85) { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: 0, z: 0 }, angularDamping: FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.REDUCE_ANGULAR_FLOAT ? 0.89 : 0.5, linearDamping: 0.95, linearSleepingThreshold: 0.1, angularSleepingThreshold: 0.1, - collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES + collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS }); addComponent(world, MakeStaticWhenAtRest, eid); } else { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: FloatyObject.releaseGravity[eid], z: 0 }, angularDamping: 0.01, linearDamping: 0.01, @@ -131,7 +131,7 @@ export const floatyObjectSystem = world => { const angle = Math.random() * Math.PI * 2; const x = Math.cos(angle); const z = Math.sin(angle); - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x, y: force, z }, angularDamping: 0.01, linearDamping: 0.01, @@ -142,7 +142,7 @@ export const floatyObjectSystem = world => { removeComponent(world, MakeStaticWhenAtRest, eid); } } else { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { collisionFilterMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE, gravity: { x: 0, y: -9.8, z: 0 } }); diff --git a/src/systems/hold-system.js b/src/systems/hold-system.js index 72316e506a..68ff4ae74a 100644 --- a/src/systems/hold-system.js +++ b/src/systems/hold-system.js @@ -13,14 +13,14 @@ import { HeldHandLeft, AEntity, Networked, - MediaLoader, - Deletable + Rigidbody } from "../bit-components"; import { canMove } from "../utils/permissions-utils"; import { canMove as canMoveEntity } from "../utils/bit-permissions-utils"; import { isPinned } from "../bit-systems/networking"; import { takeOwnership } from "../utils/take-ownership"; import { findAncestorWithComponents } from "../utils/bit-utils"; +import { HOLDABLE_FLAGS } from "../inflators/holdable"; const GRAB_REMOTE_RIGHT = paths.actions.cursor.right.grab; const DROP_REMOTE_RIGHT = paths.actions.cursor.right.drop; @@ -75,18 +75,22 @@ export function isAEntityPinned(world, eid) { // Alternate solution: Simply recognize an entity as pinned if its any // ancestor is pinned (in hold-system) unless there is a case that // descendant entity under pinned entity wants to be grabbable. +// +// Update: As now we are supporting grabbable scene objects, we look for the +// root holdable entity with a rigid body as the only rigid body +// in the hierarchy should be at the root of the grabbable object. function grab(world, userinput, queryHovered, held, grabPath) { const hovered = queryHovered(world)[0]; - // Special path for Dropped/Pasted Media with new loader enabled. Check the comment above. - const mediaRoot = findAncestorWithComponents(world, [Deletable, MediaLoader, Holdable], hovered); - const target = mediaRoot ? mediaRoot : hovered; + const interactable = findAncestorWithComponents(world, [Holdable, Rigidbody], hovered); + const target = interactable ? interactable : hovered; const isEntityPinned = isPinned(target) || isAEntityPinned(world, target); if ( target && userinput.get(grabPath) && (!isEntityPinned || AFRAME.scenes[0].is("frozen")) && + Holdable.flags[interactable] & HOLDABLE_FLAGS.ENABLED && hasPermissionToGrab(world, target) ) { if (hasComponent(world, Networked, target)) { diff --git a/src/systems/hubs-systems.ts b/src/systems/hubs-systems.ts index 96a6e8c86f..2281f524fb 100644 --- a/src/systems/hubs-systems.ts +++ b/src/systems/hubs-systems.ts @@ -76,7 +76,6 @@ import { textSystem } from "../bit-systems/text"; import { audioTargetSystem } from "../bit-systems/audio-target-system"; import { scenePreviewCameraSystem } from "../bit-systems/scene-preview-camera-system"; import { linearTransformSystem } from "../bit-systems/linear-transform"; -import { quackSystem } from "../bit-systems/quack"; import { mixerAnimatableSystem } from "../bit-systems/mixer-animatable"; import { loopAnimationSystem } from "../bit-systems/loop-animation"; import { linkSystem } from "../bit-systems/link-system"; @@ -93,6 +92,8 @@ import { linkedPDFSystem } from "../bit-systems/linked-pdf-system"; import { inspectSystem } from "../bit-systems/inspect-system"; import { snapMediaSystem } from "../bit-systems/snap-media-system"; import { scaleWhenGrabbedSystem } from "../bit-systems/scale-when-grabbed-system"; +import { interactableSystem } from "../bit-systems/interactable-system"; +import { SystemConfigT } from "../types"; declare global { interface Window { @@ -198,6 +199,10 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene aframeSystems[systemNames[i]].tick(t, dt); } + APP.addon_systems.setup.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + networkReceiveSystem(world); onOwnershipLost(world); sceneLoadingSystem(world, hubsSystems.environmentSystem, hubsSystems.characterController); @@ -213,10 +218,13 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene buttonSystems(world); sfxButtonSystem(world, aframeSystems["hubs-systems"].soundEffectsSystem); + APP.addon_systems.prePhysics.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + physicsCompatSystem(world, hubsSystems.physicsSystem); hubsSystems.physicsSystem.tick(dt); constraintsSystem(world, hubsSystems.physicsSystem); - floatyObjectSystem(world); hoverableVisualsSystem(world); @@ -240,6 +248,8 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene hubsSystems.hoverMenuSystem.tick(); hubsSystems.positionAtBorderSystem.tick(); hubsSystems.twoPointStretchingSystem.tick(); + interactableSystem(world); + floatyObjectSystem(world); hubsSystems.holdableButtonSystem.tick(); hubsSystems.hoverButtonSystem.tick(); @@ -281,7 +291,6 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene hubsSystems.nameTagSystem.tick(); simpleWaterSystem(world); linearTransformSystem(world); - quackSystem(world); followInFovSystem(world); linkedMediaSystem(world); linkedVideoSystem(world); @@ -303,6 +312,10 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene bitPenCompatSystem(world, aframeSystems["pen-tools"]); snapMediaSystem(world, aframeSystems["hubs-systems"].soundEffectsSystem); + APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + deleteEntitySystem(world, aframeSystems.userinput); destroyAtExtremeDistanceSystem(world); removeNetworkedObjectButtonSystem(world); @@ -318,15 +331,28 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene networkDebugSystem(world, scene); } + APP.addon_systems.beforeMatricesUpdate.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + scene.updateMatrixWorld(); renderer.info.reset(); + + APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + if (APP.fx.composer) { APP.fx.composer.render(); } else { renderer.render(scene, camera); } + APP.addon_systems.afterRender.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + // tock()s on components and system will fire here. (As well as any other time render() is called without unbinding onAfterRender) // TODO inline invoking tocks instead of using onAfterRender registered in a-scene } diff --git a/src/systems/networked-transform.js b/src/systems/networked-transform.js index 9a501c25d7..81ab689a6c 100644 --- a/src/systems/networked-transform.js +++ b/src/systems/networked-transform.js @@ -1,12 +1,24 @@ -import { addComponent, defineQuery, hasComponent } from "bitecs"; -import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned } from "../bit-components"; +import { addComponent, defineQuery, hasComponent, enterQuery } from "bitecs"; +import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned, Networked } from "../bit-components"; import { millisecondsBetweenTicks } from "../bit-systems/networking"; const query = defineQuery([NetworkedTransform]); const tmpVec = new THREE.Vector3(); const tmpQuat = new THREE.Quaternion(); + +const networkedTransformEnterQuery = enterQuery(query); export function networkedTransformSystem(world) { + networkedTransformEnterQuery(world).forEach(eid => { + // If it's a scene object that has not been owned yet, + // we initialize its networked transform to the initial object transform. + if (Networked.creator[eid] === APP.getSid("scene") && Networked.owner[eid] === APP.getSid("reticulum")) { + const obj = world.eid2obj.get(eid); + NetworkedTransform.position[eid].set(obj.position.toArray()); + NetworkedTransform.rotation[eid].set(obj.quaternion.toArray()); + NetworkedTransform.scale[eid].set(obj.scale.toArray()); + } + }); const ents = query(world); for (let i = 0; i < ents.length; i++) { const eid = ents[i]; diff --git a/src/systems/on-ownership-lost.js b/src/systems/on-ownership-lost.js index ae055891c6..e392265ebf 100644 --- a/src/systems/on-ownership-lost.js +++ b/src/systems/on-ownership-lost.js @@ -1,4 +1,3 @@ -import { COLLISION_LAYERS } from "../constants"; import { exitQuery, defineQuery, removeComponent, hasComponent, entityExists } from "bitecs"; import { Held, @@ -13,7 +12,6 @@ import { // TODO this seems wrong, nothing sets it back unless its a floaty object const exitOwned = exitQuery(defineQuery([Owned])); const componentsToRemove = [Held, HeldHandRight, HeldHandLeft, HeldRemoteRight, HeldRemoteLeft]; -const kinematicOptions = { type: "kinematic", collisionFilterMask: COLLISION_LAYERS.UNOWNED_INTERACTABLE }; export function onOwnershipLost(world) { const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; @@ -27,7 +25,10 @@ export function onOwnershipLost(world) { } if (hasComponent(world, Rigidbody, eid)) { - physicsSystem.updateRigidBodyOptions(eid, kinematicOptions); + physicsSystem.updateRigidBody(eid, { + type: "kinematic", + collisionFilterMask: Rigidbody.initialCollisionFilterMask[eid] + }); } } } diff --git a/src/systems/physics-system.js b/src/systems/physics-system.js index 1489cafad9..20e92fcc06 100644 --- a/src/systems/physics-system.js +++ b/src/systems/physics-system.js @@ -3,7 +3,7 @@ import { AmmoDebugConstants, DefaultBufferSize } from "ammo-debug-drawer"; import configs from "../utils/configs"; import ammoWasmUrl from "ammo.js/builds/ammo.wasm.wasm"; import { Rigidbody } from "../bit-components"; -import { updateRigiBodyParams } from "../inflators/rigid-body"; +import { updateBodyParams } from "../inflators/rigid-body"; const MESSAGE_TYPES = CONSTANTS.MESSAGE_TYPES, TYPE = CONSTANTS.TYPE, @@ -231,6 +231,7 @@ export class PhysicsSystem { const bodyId = this.nextBodyUuid; this.nextBodyUuid += 1; + object3D.updateMatrices(); this.workerHelpers.addBody(bodyId, object3D, options); this.bodyUuidToData.set(bodyId, { @@ -250,7 +251,7 @@ export class PhysicsSystem { updateRigidBody(eid, options) { const bodyId = Rigidbody.bodyId[eid]; - updateRigiBodyParams(eid, options); + updateBodyParams(eid, options); if (this.bodyUuidToData.has(bodyId)) { this.bodyUuidToData.get(bodyId).options = options; this.workerHelpers.updateBody(bodyId, options); @@ -259,18 +260,6 @@ export class PhysicsSystem { } } - updateRigidBodyOptions(eid, options) { - const bodyId = Rigidbody.bodyId[eid]; - updateRigiBodyParams(eid, options); - const bodyData = this.bodyUuidToData.get(bodyId); - if (!bodyData) { - // TODO: Fix me. - console.warn("updateBodyOptions called for invalid bodyId"); - return; - } - this.workerHelpers.updateBody(bodyId, Object.assign(this.bodyUuidToData.get(bodyId).options, options)); - } - removeBody(uuid) { const bodyData = this.bodyUuidToData.get(uuid); if (!bodyData) { @@ -285,8 +274,10 @@ export class PhysicsSystem { if (bodyData.isInitialized) { delete this.indexToUuid[bodyData.index]; bodyData.collisions.forEach(otherId => { - const otherData = this.bodyUuidToData.get(otherId).collisions; - otherData.splice(otherData.indexOf(uuid), 1); + const collisions = this.bodyUuidToData.get(otherId)?.collisions; + // This can happen when removing multiple bodies in a frame + if (!collisions) return; + collisions.splice(collisions.indexOf(uuid), 1); }); this.bodyUuids.splice(this.bodyUuids.indexOf(uuid), 1); this.bodyUuidToData.delete(uuid); diff --git a/src/systems/remove-object3D-system.js b/src/systems/remove-object3D-system.js index a163834c5f..d7cc9dc04a 100644 --- a/src/systems/remove-object3D-system.js +++ b/src/systems/remove-object3D-system.js @@ -5,6 +5,7 @@ import { GLTFModel, LightTag, MaterialTag, + TextureTag, MediaFrame, MediaImage, MediaVideo, @@ -93,6 +94,7 @@ const cleanupAudioDebugSystem = cleanupOnExit(NavMesh, eid => cleanupAudioDebugN // which means we will remove each descendent from its parent. const exitedObject3DQuery = exitQuery(defineQuery([Object3DTag])); const exitedMaterialQuery = exitQuery(defineQuery([MaterialTag])); +const exitedTextureQuery = exitQuery(defineQuery([TextureTag])); export function removeObject3DSystem(world) { function removeObjFromMap(eid) { const o = world.eid2obj.get(eid); @@ -104,6 +106,11 @@ export function removeObject3DSystem(world) { world.eid2mat.delete(eid); m.eid = 0; } + function removeFromTexMap(eid) { + const m = world.eid2tex.get(eid); + world.eid2tex.delete(eid); + m.eid = 0; + } // TODO write removeObject3DEntity to do this work up-front, // keeping the scene graph consistent and avoiding the second exitedObject3DQuery in this system. @@ -145,4 +152,5 @@ export function removeObject3DSystem(world) { entities.forEach(removeObjFromMap); exitedObject3DQuery(world).forEach(removeObjFromMap); exitedMaterialQuery(world).forEach(removeFromMatMap); + exitedTextureQuery(world).forEach(removeFromTexMap); } diff --git a/src/systems/single-action-button-system.js b/src/systems/single-action-button-system.js index 850b94eeca..28e389c724 100644 --- a/src/systems/single-action-button-system.js +++ b/src/systems/single-action-button-system.js @@ -1,4 +1,4 @@ -import { addComponent, defineQuery, hasComponent, removeComponent } from "bitecs"; +import { addComponent, defineQuery, enterQuery, hasComponent, removeComponent } from "bitecs"; import { HoverButton, HoveredHandLeft, @@ -80,13 +80,16 @@ function applyTheme() { textHoverColor: new THREE.Color(0xffffff) }; } -onThemeChanged(applyTheme); -applyTheme(); const hoverComponents = [HoveredRemoteRight, HoveredRemoteLeft, HoveredHandRight, HoveredHandLeft]; const hoverButtonsQuery = defineQuery([HoverButton]); +const hoverButtonsEnterQuery = enterQuery(hoverButtonsQuery); function hoverButtonSystem(world) { + if (hoverButtonsEnterQuery(world).length > 0) { + onThemeChanged(applyTheme); + applyTheme(); + } hoverButtonsQuery(world).forEach(function (eid) { const obj = world.eid2obj.get(eid); const isHovered = hasAnyComponent(world, hoverComponents, eid); diff --git a/src/systems/sound-effects-system.js b/src/systems/sound-effects-system.js index 906594955d..b615dbb68c 100644 --- a/src/systems/sound-effects-system.js +++ b/src/systems/sound-effects-system.js @@ -54,6 +54,17 @@ function decodeAudioData(audioContext, arrayBuffer) { }); } +function load(system, url) { + let audioBufferPromise = system.loading.get(url); + if (!audioBufferPromise) { + audioBufferPromise = fetch(url) + .then(r => r.arrayBuffer()) + .then(arrayBuffer => decodeAudioData(system.audioContext, arrayBuffer)); + system.loading.set(url, audioBufferPromise); + } + return audioBufferPromise; +} + export class SoundEffectsSystem { constructor(scene) { this.pendingAudioSourceNodes = []; @@ -92,20 +103,10 @@ export class SoundEffectsSystem { [SOUND_SPAWN_EMOJI, URL_SPAWN_EMOJI], [SOUND_SPEAKER_TONE, URL_SPEAKER_TONE] ]; - const loading = new Map(); - const load = url => { - let audioBufferPromise = loading.get(url); - if (!audioBufferPromise) { - audioBufferPromise = fetch(url) - .then(r => r.arrayBuffer()) - .then(arrayBuffer => decodeAudioData(this.audioContext, arrayBuffer)); - loading.set(url, audioBufferPromise); - } - return audioBufferPromise; - }; + this.loading = new Map(); this.sounds = new Map(); soundsAndUrls.map(([sound, url]) => { - load(url).then(audioBuffer => { + load(this, url).then(audioBuffer => { this.sounds.set(sound, audioBuffer); }); }); @@ -122,6 +123,20 @@ export class SoundEffectsSystem { }); } + registerSound(url) { + return new Promise((resolve, reject) => { + load(this, url) + .then(audioBuffer => { + soundEnum++; + this.sounds.set(soundEnum, audioBuffer); + resolve({ id: soundEnum, url }); + }) + .catch(() => { + reject(); + }); + }); + } + enqueueSound(sound, loop) { if (this.isDisabled) return null; const audioBuffer = this.sounds.get(sound); diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js index 7ed593a746..4a9d0e492f 100644 --- a/src/systems/userinput/userinput.js +++ b/src/systems/userinput/userinput.js @@ -44,6 +44,9 @@ import { gamepadBindings } from "./bindings/generic-gamepad"; import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "../../utils/vr-caps-detect"; import { hackyMobileSafariTest } from "../../utils/detect-touchscreen"; import { ArrayBackedSet } from "./array-backed-set"; +import { addSetsToBindings } from "./bindings/utils"; +import { InputDeviceE } from "../../types"; +import deepmerge from "deepmerge"; function arrayContentsDiffer(a, b) { if (a.length !== b.length) return true; @@ -191,6 +194,25 @@ function computeExecutionStrategy(sortedBindings, masks, activeSets) { return { actives, masked }; } +const DeviceToBindingsMapping = { + [InputDeviceE.Cardboard]: cardboardUserBindings, + [InputDeviceE.Daydream]: daydreamUserBindings, + [InputDeviceE.Gamepad]: gamepadBindings, + [InputDeviceE.KeyboardMouse]: keyboardMouseUserBindings, + [InputDeviceE.OculusGo]: oculusGoUserBindings, + [InputDeviceE.OculusTouch]: oculusTouchUserBindings, + [InputDeviceE.TouchScreen]: touchscreenUserBindings, + [InputDeviceE.Vive]: viveUserBindings, + [InputDeviceE.WebXR]: webXRUserBindings, + [InputDeviceE.WindowsMixedReality]: wmrUserBindings, + [InputDeviceE.XboxController]: xboxControllerUserBindings, + [InputDeviceE.GearVR]: gearVRControllerUserBindings, + [InputDeviceE.ViveCosmos]: viveCosmosUserBindings, + [InputDeviceE.ViveFocusPlus]: viveFocusPlusUserBindings, + [InputDeviceE.ViveWand]: viveWandUserBindings, + [InputDeviceE.ValveIndex]: indexUserBindings +}; + AFRAME.registerSystem("userinput", { get(path) { if (!this.frame) return; @@ -560,5 +582,20 @@ AFRAME.registerSystem("userinput", { this.prevSortedBindings = this.sortedBindings; this.maybeToggleXboxMapping(); + }, + registerPaths(newPaths) { + for (const path of newPaths) { + if (path.value in paths[path.type]) { + throw Error(`Path ${path.key} already registered`); + } + paths[path.type][path.value] = `/${path.type}/${path.value}`; + } + }, + registerBindings(device, bindings) { + bindings = addSetsToBindings(bindings); + for (const key in bindings) { + DeviceToBindingsMapping[device][key] = deepmerge(DeviceToBindingsMapping[device][key], bindings[key]); + } + this.registeredMappingsChanged = true; } }); diff --git a/src/tokens.js b/src/tokens.js index b300dcd13b..236e614f13 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -10,10 +10,11 @@ import "./react-components/styles/global.scss"; import { TokenPageLayout } from "./react-components/tokens/TokenPageLayout"; import configs from "./utils/configs"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/tokens", "Backend API Tokens Page"); +const store = getStore(); window.APP = { store }; function TokensRoot() { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000000..db174a5ff0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,255 @@ +import { AScene, HubsSystems } from "aframe"; +import { App, HubsWorld } from "./app"; +import { Permission } from "./prefabs/prefabs"; +import { EntityDef } from "./utils/jsx-entity"; +import { NetworkSchema } from "./utils/network-schemas"; +import { EntityID } from "./utils/networking-types"; +import { IComponent } from "bitecs"; + +export enum SystemOrderE { + Setup = 0, + PrePhysics = 100, + PostPhysics = 200, + BeforeMatricesUpdate = 300, + BeforeRender = 400, + AfterRender = 500 +} + +export enum PostProcessOrderE { + AfterScene = 0, + AfterBloom = 1, + AfterUI = 2, + AfterAA = 3 +} + +export type CoreSystemKeyT = keyof AScene["systems"]; +export type HubsSystemKeyT = keyof HubsSystems; +export type SystemKeyT = CoreSystemKeyT | HubsSystemKeyT; + +export enum SystemsE { + PhysicsSystem = "physicsSystem", + AudioSystem = "audioSystem", + SoundEffectsSystem = "soundEffectsSystem", + CameraSystem = "cameraSystem", + CharacterControllerSystem = "characterController", + WaypointSystem = "waypointSystem", + UserInputSystem = "userinput", + NavMesh = "nav" +} + +export interface SystemT { + (app: App): void; +} + +export type ComponentDataT = { + [key: string]: any; +}; + +export interface InflatorT { + (world: HubsWorld, eid: EntityID, componentProps?: ComponentDataT): EntityID; +} + +export interface InflatorParamT { + id: string; + inflator: InflatorT; +} + +export type InflatorConfigT = { + common?: InflatorParamT; + jsx?: InflatorParamT; + gltf?: InflatorParamT; +}; + +export enum PermissionE { + SPAWN_CAMERA = "spawn_camera", + SPAWN_AND_MOVE_MEDIA = "spawn_and_move_media", + UPDATE_HUB = "update_hub", + PIN_OBJECTS = "pin_objects", + SPAWN_EMOJI = "spawn_emoji", + AMPLIFY_AUDIO = "amplify_audio", + FLY = "fly", + VOICE_CHAT = "voice_chat", + SPAWN_DRAWING = "spawn_drawing", + TWEET = "tweet", + KICK_USERS = "kick_users", + MUTE_USERS = "mute_users" +} + +export type PrefabTemplateFn = (params: ComponentDataT) => EntityDef; +export type PermissionT = Permission; +export type PrefabNameT = string; +export type PrefabDefinitionT = { + permission: Permission; + template: PrefabTemplateFn; +}; +export interface PrefabConfigT { + id: PrefabNameT; + config: PrefabDefinitionT; +} + +export type NetworkSchemaT = NetworkSchema; +export interface NetworkSchemaConfigT { + component: IComponent; + schema: NetworkSchemaT; +} +export type SystemConfigT = { system: SystemT; order: number }; + +export type ChatCommandCallbackFn = (app: App, args: string[]) => void; +export interface ChatCommandConfigT { + id: string; + command: ChatCommandCallbackFn; +} + +/** + * This has to be kept in sync with the preferences-screen PREFERENCE_LIST_ITEM_TYPE + */ +export enum PREFERENCE_LIST_ITEM_TYPE { + CHECK_BOX = 1, + SELECT = 2, + NUMBER_WITH_RANGE = 3, + MAX_RESOLUTION = 4, + MAP_COUNT = 5, + CUSTOM_COMPONENT = 6 +} + +export type PreferenceDefConfigT = { + type: "string" | "number" | "bool" | "object"; + default: string | number | boolean | object | undefined; +}; + +export type PreferenceSelectT = { + value: string | number; + text: string; +}; + +export type PreferenceRangeT = { + min: number; + max: number; + step: number; + digits: number; +}; + +export type PreferenceUIConfigT = + | { + prefType: PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX; + description: string; + promptForRefresh?: boolean; + hidden?: () => boolean; + disableIfFalse?: string; + } + | { + prefType: PREFERENCE_LIST_ITEM_TYPE.NUMBER_WITH_RANGE; + description: string; + promptForRefresh?: boolean; + hidden?: () => boolean; + disableIfFalse?: string; + min: number; + max: number; + step: number; + digits: number; + } + | { + prefType: PREFERENCE_LIST_ITEM_TYPE.SELECT; + description: string; + promptForRefresh?: boolean; + hidden?: () => boolean; + disableIfFalse?: string; + options?: PreferenceSelectT[]; + } + | { + prefType: PREFERENCE_LIST_ITEM_TYPE.MAP_COUNT; + description: string; + promptForRefresh?: boolean; + hidden?: () => boolean; + disableIfFalse?: string; + defaultValue?: number; + text?: string; + }; + +export type PreferencePrefsScreenItemT = { key: string | PreferenceUIConfigT }; +export type PreferencePrefsScreenCategory = Map; +export type PreferenceScreenLabelT = Map; +export type PreferenceScreenDefT = Map; + +export type PreferenceConfigT = { + [key: string]: { + prefDefinition: PreferenceDefConfigT; + prefConfig: PreferenceUIConfigT; + }; +}; + +export type SoundDefT = { + id: number; + url: string; +}; + +export enum InputSetsE { + global = "global", + inputFocused = "inputFocused", + rightCursorHoveringOnPen = "rightCursorHoveringOnPen", + rightCursorHoveringOnCamera = "rightCursorHoveringOnCamera", + rightCursorHoveringOnInteractable = "rightCursorHoveringOnInteractable", + rightCursorHoveringOnUI = "rightCursorHoveringOnUI", + rightCursorHoveringOnVideo = "rightCursorHoveringOnVideo", + rightCursorHoveringOnNothing = "rightCursorHoveringOnNothing", + rightCursorHoldingPen = "rightCursorHoldingPen", + rightCursorHoldingCamera = "rightCursorHoldingCamera", + rightCursorHoldingInteractable = "rightCursorHoldingInteractable", + rightCursorHoldingUI = "rightCursorHoldingUI", + rightCursorHoldingNothing = "rightCursorHoldingNothing", + leftCursorHoveringOnPen = "leftCursorHoveringOnPen", + leftCursorHoveringOnCamera = "leftCursorHoveringOnCamera", + leftCursorHoveringOnInteractable = "leftCursorHoveringOnInteractable", + leftCursorHoveringOnUI = "leftCursorHoveringOnUI", + leftCursorHoveringOnVideo = "leftCursorHoveringOnVideo", + leftCursorHoveringOnNothing = "leftCursorHoveringOnNothing", + leftCursorHoldingPen = "leftCursorHoldingPen", + leftCursorHoldingCamera = "leftCursorHoldingCamera", + leftCursorHoldingInteractable = "leftCursorHoldingInteractable", + leftCursorHoldingUI = "leftCursorHoldingUI", + leftCursorHoldingNothing = "leftCursorHoldingNothing", + rightHandTeleporting = "rightHandTeleporting", + rightHandHoveringOnPen = "rightHandHoveringOnPen", + rightHandHoveringOnCamera = "rightHandHoveringOnCamera", + rightHandHoveringOnInteractable = "rightHandHoveringOnInteractable", + rightHandHoveringOnNothing = "rightHandHoveringOnNothing", + rightHandHoldingPen = "rightHandHoldingPen", + rightHandHoldingCamera = "rightHandHoldingCamera", + rightHandHoldingInteractable = "rightHandHoldingInteractable", + leftHandTeleporting = "leftHandTeleporting", + leftHandHoveringOnPen = "leftHandHoveringOnPen", + leftHandHoveringOnCamera = "leftHandHoveringOnCamera", + leftHandHoveringOnInteractable = "leftHandHoveringOnInteractable", + leftHandHoldingPen = "leftHandHoldingPen", + leftHandHoldingCamera = "leftHandHoldingCamera", + leftHandHoldingInteractable = "leftHandHoldingInteractable", + leftHandHoveringOnNothing = "leftHandHoveringOnNothing", + debugUserInput = "debugUserInput", + inspecting = "inspecting" +} + +export enum InputPathsE { + noop = "noop", + actions = "actions", + haptics = "haptics", + device = "device" +} + +export enum InputDeviceE { + Cardboard, + Daydream, + Gamepad, + KeyboardMouse, + OculusGo, + OculusTouch, + TouchScreen, + Vive, + WebXR, + WindowsMixedReality, + XboxController, + GearVR, + ViveCosmos, + ViveFocusPlus, + ViveWand, + ValveIndex +} diff --git a/src/utils/assign-network-ids.ts b/src/utils/assign-network-ids.ts index ae5399bc8d..1c4aaba4ed 100644 --- a/src/utils/assign-network-ids.ts +++ b/src/utils/assign-network-ids.ts @@ -2,6 +2,7 @@ import { hasComponent } from "bitecs"; import { HubsWorld } from "../app"; import { Networked } from "../bit-components"; import { ClientID, EntityID, NetworkID } from "./networking-types"; +import { Material, Texture } from "three"; export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, eid: EntityID, creator: ClientID) { let i = 0; @@ -12,6 +13,16 @@ export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, e i += 1; } }); + i = 0; + world.eid2mat.forEach((mat: Material, matEid: EntityID) => { + setInitialNetworkedData(matEid, `${rootNid}.mat.${i}`, rootNid); + i += 1; + }); + i = 0; + world.eid2tex.forEach((tex: Texture, texEid: EntityID) => { + setInitialNetworkedData(texEid, `${rootNid}.tex.${i}`, rootNid); + i += 1; + }); } export function setNetworkedDataWithoutRoot(world: HubsWorld, rootNid: NetworkID, childEid: EntityID) { diff --git a/src/utils/bit-utils.ts b/src/utils/bit-utils.ts index 009fb52882..6a4dcab82c 100644 --- a/src/utils/bit-utils.ts +++ b/src/utils/bit-utils.ts @@ -5,6 +5,7 @@ import { HubsWorld } from "../app"; import { findAncestor, findAncestors, traverseSome } from "./three-utils"; import { EntityID } from "./networking-types"; import qsTruthy from "./qs_truthy"; +import configs from "./configs"; export type ElOrEid = EntityID | AElement; @@ -25,8 +26,15 @@ export function hasAnyComponent(world: HubsWorld, components: Component[], eid: return false; } -export function findAncestorEntity(world: HubsWorld, eid: number, predicate: (eid: number) => boolean) { - const obj = findAncestor(world.eid2obj.get(eid)!, (o: Object3D) => !!(o.eid && predicate(o.eid))) as Object3D | null; +export function findAncestorEntity( + world: HubsWorld, + eid: number, + predicate: (eid: number, world: HubsWorld) => boolean +) { + const obj = findAncestor( + world.eid2obj.get(eid)!, + (o: Object3D) => !!(o.eid && predicate(o.eid, world)) + ) as Object3D | null; return obj && obj.eid!; } @@ -36,7 +44,7 @@ export function findAncestorEntities(world: HubsWorld, eid: number, predicate: ( } export function findAncestorWithComponent(world: HubsWorld, component: Component, eid: number) { - return findAncestorEntity(world, eid, otherId => hasComponent(world, component, otherId)); + return findAncestorEntity(world, eid, (otherId, world) => hasComponent(world, component, otherId)); } export function findAncestorsWithComponent(world: HubsWorld, component: Component, eid: number): EntityID[] { @@ -69,7 +77,28 @@ export function findChildWithComponent(world: HubsWorld, component: Component, e } } +export function findChildrenWithComponent(world: HubsWorld, component: Component, eid: number) { + const obj = world.eid2obj.get(eid); + if (obj) { + const childrenEids = new Array(); + obj.traverse((otherObj: Object3D) => { + if (otherObj.eid && hasComponent(world, component, otherObj.eid)) { + childrenEids.push(otherObj.eid); + } + }); + return childrenEids; + } +} + const forceNewLoader = qsTruthy("newLoader"); export function shouldUseNewLoader() { - return forceNewLoader || APP.hub?.user_data?.hubs_use_bitecs_based_client; + if (forceNewLoader === true) { + return true; + } else if (APP.hub?.user_data?.hubs_use_bitecs_based_client !== undefined) { + return APP.hub?.user_data?.hubs_use_bitecs_based_client; + } else if (configs.feature("bitecs_loader") !== undefined) { + return configs.feature("bitecs_loader"); + } else { + return false; + } } diff --git a/src/utils/create-networked-entity.ts b/src/utils/create-networked-entity.ts index 60ebeb05a8..1a9c817d7c 100644 --- a/src/utils/create-networked-entity.ts +++ b/src/utils/create-networked-entity.ts @@ -3,18 +3,19 @@ import { HubsWorld } from "../app"; import { Networked } from "../bit-components"; import { createMessageDatas } from "../bit-systems/networking"; import { MediaLoaderParams } from "../inflators/media-loader"; -import { PrefabName, prefabs } from "../prefabs/prefabs"; +import { prefabs } from "../prefabs/prefabs"; import { renderAsEntity } from "../utils/jsx-entity"; import { hasPermissionToSpawn } from "../utils/permissions"; import { takeOwnership } from "../utils/take-ownership"; import { setNetworkedDataWithRoot } from "./assign-network-ids"; import type { ClientID, InitialData, NetworkID } from "./networking-types"; +import { PrefabNameT } from "../types"; export function createNetworkedMedia(world: HubsWorld, initialData: MediaLoaderParams) { return createNetworkedEntity(world, "media", initialData); } -export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabName, initialData: InitialData) { +export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabNameT, initialData: InitialData) { if (!hasPermissionToSpawn(NAF.clientId, prefabName)) throw new Error(`You do not have permission to spawn ${prefabName}`); const nid = NAF.utils.createNetworkId(); @@ -25,7 +26,7 @@ export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabName, export function renderAsNetworkedEntity( world: HubsWorld, - prefabName: PrefabName, + prefabName: PrefabNameT, initialData: InitialData, nid: NetworkID, creator: ClientID diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 15e9e9c9f8..497bcfa054 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -36,7 +36,6 @@ import { Billboard, MaterialTag, VideoTextureSource, - Quack, MixerAnimatableInitialize, Inspectable, ObjectMenu, @@ -57,7 +56,7 @@ import { inflateLink, LinkParams } from "../inflators/link"; import { inflateLinkLoader, LinkLoaderParams } from "../inflators/link-loader"; import { inflateLoopAnimationInitialize, LoopAnimationParams } from "../inflators/loop-animation"; import { inflateSlice9 } from "../inflators/slice9"; -import { TextParams, inflateText } from "../inflators/text"; +import { TextParams, inflateGLTFText, inflateText } from "../inflators/text"; import { BackgroundParams, EnvironmentSettingsParams, @@ -70,7 +69,6 @@ import { inflateSpawnpoint, inflateWaypoint, WaypointParams } from "../inflators import { inflateReflectionProbe, ReflectionProbeParams } from "../inflators/reflection-probe"; import { HubsWorld } from "../app"; import { Group, Material, Object3D, Texture } from "three"; -import { AlphaMode } from "./create-image-mesh"; import { MediaLoaderParams } from "../inflators/media-loader"; import { preload } from "./preload"; import { DirectionalLightParams, inflateDirectionalLight } from "../inflators/directional-light"; @@ -78,7 +76,6 @@ import { AmbientLightParams, inflateAmbientLight } from "../inflators/ambient-li import { HemisphereLightParams, inflateHemisphereLight } from "../inflators/hemisphere-light"; import { PointLightParams, inflatePointLight } from "../inflators/point-light"; import { SpotLightParams, inflateSpotLight } from "../inflators/spot-light"; -import { ProjectionMode } from "./projection-mode"; import { inflateSkybox, SkyboxParams } from "../inflators/skybox"; import { inflateSpawner, SpawnerParams } from "../inflators/spawner"; import { inflateVideoTextureTarget, VideoTextureTargetParams } from "../inflators/video-texture-target"; @@ -93,18 +90,19 @@ import { inflateAudioParams } from "../inflators/audio-params"; import { AudioSourceParams, inflateAudioSource } from "../inflators/audio-source"; import { AudioTargetParams, inflateAudioTarget } from "../inflators/audio-target"; import { PhysicsShapeParams, inflatePhysicsShape } from "../inflators/physics-shape"; -import { inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body"; +import { inflateGLTFRigidBody, inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body"; import { AmmoShapeParams, inflateAmmoShape } from "../inflators/ammo-shape"; import { BoxColliderParams, inflateBoxCollider } from "../inflators/box-collider"; import { inflateTrimesh } from "../inflators/trimesh"; import { HeightFieldParams, inflateHeightField } from "../inflators/heightfield"; import { inflateAudioSettings } from "../inflators/audio-settings"; -import { HubsVideoTexture } from "../textures/HubsVideoTexture"; import { inflateMediaLink, MediaLinkParams } from "../inflators/media-link"; import { inflateObjectMenuTarget, ObjectMenuTargetParams } from "../inflators/object-menu-target"; import { inflateObjectMenuTransform, ObjectMenuTransformParams } from "../inflators/object-menu-transform"; import { inflatePlane, PlaneParams } from "../inflators/plane"; import { FollowInFovParams, inflateFollowInFov } from "../inflators/follow-in-fov"; +import { ComponentDataT } from "../types"; +import { HoldableParams, inflateHoldable } from "../inflators/holdable"; preload( new Promise(resolve => { @@ -146,7 +144,7 @@ export type Attrs = { }; export type EntityDef = { - components: JSXComponentData; + components: ComponentDataT; attrs: Attrs; children: EntityDef[]; ref?: Ref; @@ -156,10 +154,10 @@ function isReservedAttr(attr: string): attr is keyof Attrs { return reservedAttrs.includes(attr); } -type ComponentFn = string | ((attrs: Attrs & JSXComponentData, children?: EntityDef[]) => EntityDef); +type ComponentFn = string | ((attrs: Attrs & ComponentDataT, children?: EntityDef[]) => EntityDef); export function createElementEntity( tag: "entity" | ComponentFn, - attrs: Attrs & JSXComponentData, + attrs: Attrs & ComponentDataT, ...children: EntityDef[] ): EntityDef { attrs = attrs || {}; @@ -167,7 +165,7 @@ export function createElementEntity( return tag(attrs, children); } else if (tag === "entity") { const outputAttrs: Attrs = {}; - const components: JSXComponentData & Attrs = {}; + const components: ComponentDataT & Attrs = {}; let ref = undefined; for (const attr in attrs) { @@ -177,7 +175,7 @@ export function createElementEntity( ref = attrs[attr]; } else { // if jsx transformed the attr into attr: true, change it to attr: {}. - const c = attr as keyof JSXComponentData; + const c = attr as keyof ComponentDataT; components[c] = attrs[c] === true ? {} : attrs[c]; } } @@ -224,7 +222,7 @@ export function addMaterialComponent(world: HubsWorld, eid: number, mat: Materia return eid; } -const createDefaultInflator = (C: Component, defaults = {}): InflatorFn => { +export const createDefaultInflator = (C: Component, defaults = {}): InflatorFn => { return (world, eid, componentProps) => { componentProps = Object.assign({}, defaults, componentProps); addComponent(world, C, eid, true); @@ -258,13 +256,15 @@ export interface ComponentData { hemisphereLight?: HemisphereLightParams; pointLight?: PointLightParams; spotLight?: SpotLightParams; - grabbable?: GrabbableParams; billboard?: { onlyY: boolean }; mirror?: MirrorParams; audioZone?: AudioZoneParams; audioParams?: AudioSettings; mediaFrame?: any; text?: TextParams; + networked?: any; + networkedTransform?: any; + grabbable?: GrabbableParams; } type OptionalParams = Partial | true; @@ -302,14 +302,12 @@ export interface JSXComponentData extends ComponentData { offersHandConstraint?: true; singleActionButton?: true; holdableButton?: true; - holdable?: true; + holdable?: HoldableParams; deletable?: true; makeKinematicOnRelease?: true; destroyAtExtremeDistance?: true; - quack?: true; // @TODO Define all the anys - networked?: any; textButton?: any; hoverButton?: any; hoverableVisuals?: any; @@ -317,7 +315,6 @@ export interface JSXComponentData extends ComponentData { physicsShape?: OptionalParams; floatyObject?: any; networkedFloatyObject?: any; - networkedTransform?: any; objectMenu?: { backgroundRef: Ref; pinButtonRef: Ref; @@ -378,6 +375,7 @@ export interface JSXComponentData extends ComponentData { objectMenuTransform?: OptionalParams; objectMenuTarget?: OptionalParams; plane?: PlaneParams; + text?: TextParams; } export interface GLTFComponentData extends ComponentData { @@ -399,6 +397,11 @@ export interface GLTFComponentData extends ComponentData { audioTarget: AudioTargetParams; audioSettings: SceneAudioSettings; mediaLink: MediaLinkParams; + rigidbody?: OptionalParams; + // TODO GLTFPhysicsShapeParams + physicsShape?: AmmoShapeParams; + text?: TextParams; + grabbable?: GrabbableParams; // deprecated spawnPoint?: true; @@ -416,7 +419,7 @@ export interface GLTFComponentData extends ComponentData { declare global { namespace createElementEntity.JSX { interface IntrinsicElements { - entity: JSXComponentData & + entity: ComponentDataT & Attrs & { children?: IntrinsicElements[]; }; @@ -428,7 +431,7 @@ declare global { } } -export const commonInflators: Required<{ [K in keyof ComponentData]: InflatorFn }> = { +export const commonInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { grabbable: inflateGrabbable, billboard: createDefaultInflator(Billboard), @@ -442,10 +445,12 @@ export const commonInflators: Required<{ [K in keyof ComponentData]: InflatorFn audioZone: inflateAudioZone, audioParams: inflateAudioParams, mediaFrame: inflateMediaFrame, - text: inflateText + text: inflateText, + networkedTransform: createDefaultInflator(NetworkedTransform), + networked: createDefaultInflator(Networked) }; -const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { +export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { ...commonInflators, cursorRaycastable: createDefaultInflator(CursorRaycastable), remoteHoverTarget: createDefaultInflator(RemoteHoverTarget), @@ -458,7 +463,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { textButton: createDefaultInflator(TextButton), hoverButton: createDefaultInflator(HoverButton), hoverableVisuals: createDefaultInflator(HoverableVisuals), - holdable: createDefaultInflator(Holdable), + holdable: inflateHoldable, deletable: createDefaultInflator(Deletable), rigidbody: inflateRigidBody, physicsShape: inflatePhysicsShape, @@ -466,8 +471,6 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { networkedFloatyObject: createDefaultInflator(NetworkedFloatyObject), makeKinematicOnRelease: createDefaultInflator(MakeKinematicOnRelease), destroyAtExtremeDistance: createDefaultInflator(DestroyAtExtremeDistance), - networkedTransform: createDefaultInflator(NetworkedTransform), - networked: createDefaultInflator(Networked), objectMenu: createDefaultInflator(ObjectMenu), mirrorMenu: createDefaultInflator(MirrorMenu), followInFov: inflateFollowInFov, @@ -484,9 +487,9 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { waypointPreview: createDefaultInflator(WaypointPreview), pdf: inflatePDF, mediaLoader: inflateMediaLoader, - quack: createDefaultInflator(Quack), mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, + text: inflateText, inspectable: createDefaultInflator(Inspectable), // inflators that create Object3Ds object3D: addObject3DComponent, @@ -500,7 +503,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { plane: inflatePlane }; -export const gltfInflators: Required<{ [K in keyof GLTFComponentData]: InflatorFn }> = { +export const gltfInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { ...commonInflators, pdf: inflatePDFLoader, // Temporarily reuse video loader for audio because of @@ -533,14 +536,17 @@ export const gltfInflators: Required<{ [K in keyof GLTFComponentData]: InflatorF trimesh: inflateTrimesh, heightfield: inflateHeightField, audioSettings: inflateAudioSettings, - mediaLink: inflateMediaLink + mediaLink: inflateMediaLink, + rigidbody: inflateGLTFRigidBody, + physicsShape: inflateAmmoShape, + text: inflateGLTFText }; -function jsxInflatorExists(name: string): name is keyof JSXComponentData { +function jsxInflatorExists(name: string) { return Object.prototype.hasOwnProperty.call(jsxInflators, name); } -export function gltfInflatorExists(name: string): name is keyof GLTFComponentData { +export function gltfInflatorExists(name: string) { return Object.prototype.hasOwnProperty.call(gltfInflators, name); } diff --git a/src/utils/load-model.tsx b/src/utils/load-model.tsx index 2b5313e0a7..4edb0b7213 100644 --- a/src/utils/load-model.tsx +++ b/src/utils/load-model.tsx @@ -9,7 +9,6 @@ export function* loadModel(world: HubsWorld, src: string, contentType: string, u const { scene, animations } = yield loadGLTFModel(src, contentType, useCache, null); scene.animations = animations; - scene.mixer = new THREE.AnimationMixer(scene); return renderAsEntity(world, ); } diff --git a/src/utils/load-video-texture.js b/src/utils/load-video-texture.js index a8b66495a2..08d9943524 100644 --- a/src/utils/load-video-texture.js +++ b/src/utils/load-video-texture.js @@ -48,10 +48,11 @@ export async function loadVideoTexture(src, contentType, loop, autoplay) { texture = new HubsVideoTexture(videoEl); videoEl.src = src; videoEl.onerror = failLoad; - videoEl.loop = loop; - videoEl.autoplay = autoplay; } + videoEl.loop = loop; + videoEl.autoplay = autoplay; + texture.minFilter = LinearFilter; texture.encoding = sRGBEncoding; diff --git a/src/utils/network-schemas.ts b/src/utils/network-schemas.ts index b97517af7e..87ee038ee8 100644 --- a/src/utils/network-schemas.ts +++ b/src/utils/network-schemas.ts @@ -5,6 +5,8 @@ import { NetworkedFloatyObject, NetworkedMediaFrame, NetworkedPDF, + NetworkedRigidBody, + NetworkedText, NetworkedTransform, NetworkedVideo, NetworkedWaypoint @@ -16,6 +18,8 @@ import { NetworkedTransformSchema } from "./networked-transform-schema"; import { NetworkedVideoSchema } from "./networked-video-schema"; import { NetworkedWaypointSchema } from "./networked-waypoint-schema"; import type { CursorBuffer, EntityID } from "./networking-types"; +import { NetworkedTextSchema } from "./networked-text-schema"; +import { NetworkedRigidBodySchema } from "./networked-rigid-body"; export interface StoredComponent { version: number; @@ -46,6 +50,8 @@ schemas.set(NetworkedFloatyObject, { ...defineNetworkSchema(NetworkedFloatyObject) }); schemas.set(NetworkedPDF, NetworkedPDFSchema); +schemas.set(NetworkedText, NetworkedTextSchema); +schemas.set(NetworkedRigidBody, NetworkedRigidBodySchema); export const networkableComponents = Array.from(schemas.keys()); diff --git a/src/utils/networked-media-frame-schema.ts b/src/utils/networked-media-frame-schema.ts index 175b022af6..19cd816dc6 100644 --- a/src/utils/networked-media-frame-schema.ts +++ b/src/utils/networked-media-frame-schema.ts @@ -8,16 +8,30 @@ const runtimeSerde = defineNetworkSchema(NetworkedMediaFrame); const migrations = new Map(); migrations.set(0, ({ data }: StoredComponent) => { - return { version: 1, data }; + return { version: 2, data }; }); function apply(eid: EntityID, { version, data }: StoredComponent) { - if (version !== 1) return false; - - const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data; - write(NetworkedMediaFrame.capturedNid, eid, capturedNid); - write(NetworkedMediaFrame.scale, eid, scale); - return true; + if (version === 1) { + const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data; + write(NetworkedMediaFrame.capturedNid, eid, capturedNid); + write(NetworkedMediaFrame.scale, eid, scale); + write(NetworkedMediaFrame.flags, eid, 0); + return true; + } else if (version === 2) { + const { + capturedNid, + scale, + flags, + mediaType + }: { capturedNid: string; scale: ArrayVec3; flags: number; mediaType: number } = data; + write(NetworkedMediaFrame.capturedNid, eid, capturedNid); + write(NetworkedMediaFrame.scale, eid, scale); + write(NetworkedMediaFrame.flags, eid, flags); + write(NetworkedMediaFrame.mediaType, eid, mediaType); + return true; + } + return false; } export const NetworkedMediaFrameSchema: NetworkSchema = { @@ -26,10 +40,12 @@ export const NetworkedMediaFrameSchema: NetworkSchema = { deserialize: runtimeSerde.deserialize, serializeForStorage: function serializeForStorage(eid: EntityID) { return { - version: 1, + version: 2, data: { capturedNid: read(NetworkedMediaFrame.capturedNid, eid), - scale: read(NetworkedMediaFrame.scale, eid) + scale: read(NetworkedMediaFrame.scale, eid), + flags: read(NetworkedMediaFrame.flags, eid), + mediaType: read(NetworkedMediaFrame.mediaType, eid) } }; }, diff --git a/src/utils/networked-rigid-body.ts b/src/utils/networked-rigid-body.ts new file mode 100644 index 0000000000..41b9cf0aaa --- /dev/null +++ b/src/utils/networked-rigid-body.ts @@ -0,0 +1,31 @@ +import { NetworkedRigidBody } from "../bit-components"; +import { defineNetworkSchema } from "./define-network-schema"; +import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas"; +import type { EntityID } from "./networking-types"; + +const runtimeSerde = defineNetworkSchema(NetworkedRigidBody); + +const migrations = new Map(); + +function apply(eid: EntityID, { version, data }: StoredComponent) { + if (version !== 1) return false; + + const { prevType }: { prevType: number } = data; + write(NetworkedRigidBody.prevType, eid, prevType); + return true; +} + +export const NetworkedRigidBodySchema: NetworkSchema = { + componentName: "networked-rigid-body", + serialize: runtimeSerde.serialize, + deserialize: runtimeSerde.deserialize, + serializeForStorage: function serializeForStorage(eid: EntityID) { + return { + version: 1, + data: { + prevType: read(NetworkedRigidBody.prevType, eid) + } + }; + }, + deserializeFromStorage: deserializerWithMigrations(migrations, apply) +}; diff --git a/src/utils/networked-text-schema.ts b/src/utils/networked-text-schema.ts new file mode 100644 index 0000000000..1f9b272f6d --- /dev/null +++ b/src/utils/networked-text-schema.ts @@ -0,0 +1,136 @@ +import { NetworkedText } from "../bit-components"; +import { defineNetworkSchema } from "./define-network-schema"; +import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas"; +import type { EntityID } from "./networking-types"; + +const migrations = new Map(); + +function apply(eid: EntityID, { version, data }: StoredComponent) { + if (version !== 1) return false; + + const { + text, + fontSize, + color, + fillOpacity, + anchorX, + anchorY, + curveRadius, + direction, + letterSpacing, + lineHeight, + maxWidth, + opacity, + outlineBlur, + outlineColor, + outlineOffsetX, + outlineOffsetY, + outlineOpacity, + outlineWidth, + overflowWrap, + side, + strokeColor, + strokeOpacity, + strokeWidth, + textAlign, + textIndent, + whiteSpace + }: { + text: string; + fontSize: number; + color: number; + fillOpacity: string; + anchorX: number; + anchorY: number; + curveRadius: number; + direction: number; + letterSpacing: number; + lineHeight: string; + maxWidth: number; + opacity: number; + outlineBlur: string; + outlineColor: number; + outlineOffsetX: string; + outlineOffsetY: string; + outlineOpacity: number; + outlineWidth: string; + overflowWrap: number; + side: number; + strokeColor: number; + strokeOpacity: number; + strokeWidth: string; + textAlign: number; + textIndent: number; + whiteSpace: number; + } = data; + write(NetworkedText.text, eid, APP.getSid(text)); + write(NetworkedText.fontSize, eid, fontSize); + write(NetworkedText.color, eid, color); + write(NetworkedText.fillOpacity, eid, APP.getSid(fillOpacity)); + write(NetworkedText.anchorX, eid, anchorX); + write(NetworkedText.anchorY, eid, anchorY); + write(NetworkedText.curveRadius, eid, curveRadius); + write(NetworkedText.direction, eid, direction); + write(NetworkedText.letterSpacing, eid, letterSpacing); + write(NetworkedText.lineHeight, eid, APP.getSid(lineHeight)); + write(NetworkedText.maxWidth, eid, maxWidth); + write(NetworkedText.opacity, eid, opacity); + write(NetworkedText.outlineBlur, eid, APP.getSid(outlineBlur)); + write(NetworkedText.outlineColor, eid, outlineColor); + write(NetworkedText.outlineOffsetX, eid, APP.getSid(outlineOffsetX)); + write(NetworkedText.outlineOffsetY, eid, APP.getSid(outlineOffsetY)); + write(NetworkedText.outlineOpacity, eid, outlineOpacity); + write(NetworkedText.outlineWidth, eid, outlineWidth); + write(NetworkedText.overflowWrap, eid, overflowWrap); + write(NetworkedText.side, eid, side); + write(NetworkedText.strokeColor, eid, strokeColor); + write(NetworkedText.strokeOpacity, eid, strokeOpacity); + write(NetworkedText.strokeWidth, eid, APP.getSid(strokeWidth)); + write(NetworkedText.textAlign, eid, textAlign); + write(NetworkedText.textIndent, eid, textIndent); + write(NetworkedText.text, eid, text); + write(NetworkedText.whiteSpace, eid, whiteSpace); + return true; +} + +const runtimeSerde = defineNetworkSchema(NetworkedText); +export const NetworkedTextSchema: NetworkSchema = { + componentName: "networked-text", + serialize: runtimeSerde.serialize, + deserialize: runtimeSerde.deserialize, + serializeForStorage: function serializeForStorage(eid: EntityID) { + return { + version: 1, + data: { + text: APP.getString(read(NetworkedText.text, eid)), + fontSize: read(NetworkedText.fontSize, eid), + color: read(NetworkedText.color, eid), + fillOpacity: APP.getString(read(NetworkedText.fillOpacity, eid)), + anchorX: read(NetworkedText.anchorX, eid), + anchorY: read(NetworkedText.anchorY, eid), + curveRadius: read(NetworkedText.curveRadius, eid), + direction: read(NetworkedText.direction, eid), + letterSpacing: read(NetworkedText.letterSpacing, eid), + lineHeight: APP.getString(read(NetworkedText.lineHeight, eid)), + maxWidth: read(NetworkedText.maxWidth, eid), + opacity: read(NetworkedText.opacity, eid), + outlineBlur: APP.getString(read(NetworkedText.outlineBlur, eid)), + outlineColor: read(NetworkedText.outlineColor, eid), + outlineOffsetX: APP.getString(read(NetworkedText.outlineOffsetX, eid)), + outlineOffsetY: APP.getString(read(NetworkedText.outlineOffsetY, eid)), + outlineOpacity: read(NetworkedText.outlineOpacity, eid), + outlineWidth: APP.getString(read(NetworkedText.outlineWidth, eid)), + overflowWrap: read(NetworkedText.overflowWrap, eid), + side: read(NetworkedText.side, eid), + strokeColor: read(NetworkedText.strokeColor, eid), + strokeOpacity: read(NetworkedText.strokeOpacity, eid), + strokeWidth: APP.getString(read(NetworkedText.strokeWidth, eid)), + textAlign: read(NetworkedText.textAlign, eid), + textIndent: read(NetworkedText.textIndent, eid), + value: read(NetworkedText.text, eid), + whiteSpace: read(NetworkedText.whiteSpace, eid) + } + }; + }, + deserializeFromStorage: deserializerWithMigrations(migrations, apply) +}; diff --git a/src/utils/networked-video-schema.ts b/src/utils/networked-video-schema.ts index c1accc7e5d..695119658d 100644 --- a/src/utils/networked-video-schema.ts +++ b/src/utils/networked-video-schema.ts @@ -8,12 +8,20 @@ const runtimeSerde = defineNetworkSchema(NetworkedVideo); const migrations = new Map(); function apply(eid: EntityID, { version, data }: StoredComponent) { - if (version !== 1) return false; - - const { time, flags }: { time: number; flags: number } = data; - write(NetworkedVideo.time, eid, time); - write(NetworkedVideo.flags, eid, flags); - return true; + if (version === 1) { + const { time, flags }: { time: number; flags: number } = data; + write(NetworkedVideo.time, eid, time); + write(NetworkedVideo.flags, eid, flags); + return true; + } else if (version === 2) { + const { time, flags, projection, src }: { time: number; flags: number; src: string; projection: number } = data; + write(NetworkedVideo.time, eid, time); + write(NetworkedVideo.flags, eid, flags); + write(NetworkedVideo.projection, eid, projection); + write(NetworkedVideo.src, eid, APP.getSid(src)); + return true; + } + return false; } export const NetworkedVideoSchema: NetworkSchema = { @@ -22,10 +30,12 @@ export const NetworkedVideoSchema: NetworkSchema = { deserialize: runtimeSerde.deserialize, serializeForStorage: function serializeForStorage(eid: EntityID) { return { - version: 1, + version: 2, data: { time: read(NetworkedVideo.time, eid), - flags: read(NetworkedVideo.flags, eid) + flags: read(NetworkedVideo.flags, eid), + projection: read(NetworkedVideo.projection, eid), + src: APP.getString(read(NetworkedVideo.src, eid)) } }; }, diff --git a/src/utils/networking-types.ts b/src/utils/networking-types.ts index 766f090173..cd46457e16 100644 --- a/src/utils/networking-types.ts +++ b/src/utils/networking-types.ts @@ -1,10 +1,10 @@ import { MediaLoaderParams } from "../inflators/media-loader"; -import { PrefabName } from "../prefabs/prefabs"; +import { PrefabNameT } from "../types"; export type EntityID = number; export type InitialData = MediaLoaderParams | any; export interface CreateMessageData { - prefabName: PrefabName; + prefabName: PrefabNameT; initialData: InitialData; } export type ClientID = string; @@ -13,7 +13,7 @@ export type StringID = number; export type CreateMessage = { version: 1; networkId: NetworkID; - prefabName: PrefabName; + prefabName: PrefabNameT; initialData: InitialData; }; export interface CursorBuffer extends Array { diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 521c28cd6f..b37e9cc902 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,7 +1,8 @@ -import { PrefabName, prefabs } from "../prefabs/prefabs"; +import { prefabs } from "../prefabs/prefabs"; +import { PrefabNameT } from "../types"; import type { ClientID } from "./networking-types"; -export function hasPermissionToSpawn(creator: ClientID, prefabName: PrefabName) { +export function hasPermissionToSpawn(creator: ClientID, prefabName: PrefabNameT) { if (creator === "reticulum") return true; const perm = prefabs.get(prefabName)!.permission; return APP.hubChannel!.userCan(creator, perm); diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index df1d4bbff9..e484780b45 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -2,7 +2,7 @@ import { Socket } from "phoenix"; import { generateHubName } from "../utils/name-generation"; import configs from "../utils/configs"; import { sleep } from "../utils/async-utils"; -import { store } from "../utils/store-instance"; +import { getStore } from "../utils/store-instance"; export function hasReticulumServer() { return !!configs.RETICULUM_SERVER; @@ -198,6 +198,7 @@ export function fetchReticulumAuthenticatedWithToken(token, url, method = "GET", }); } export function fetchReticulumAuthenticated(url, method = "GET", payload) { + const store = getStore(); return fetchReticulumAuthenticatedWithToken(store.state.credentials.token, url, method, payload); } @@ -210,6 +211,7 @@ export async function createAndRedirectToNewHub(name, sceneId, replace, qs) { } const headers = { "content-type": "application/json" }; + const store = getStore(); if (store.state && store.state.credentials.token) { headers.authorization = `bearer ${store.state.credentials.token}`; } diff --git a/src/utils/projection-mode.ts b/src/utils/projection-mode.ts index 073ed99eef..e9731045b5 100644 --- a/src/utils/projection-mode.ts +++ b/src/utils/projection-mode.ts @@ -15,3 +15,12 @@ export function getProjectionFromProjectionName(projectionName: ProjectionModeNa } return ProjectionMode.FLAT; } + +export function getProjectionNameFromProjection(projection: ProjectionMode): ProjectionModeName { + if (projection === ProjectionMode.FLAT) { + return ProjectionModeName.FLAT; + } else if (projection === ProjectionMode.SPHERE_EQUIRECTANGULAR) { + return ProjectionModeName.SPHERE_EQUIRECTANGULAR; + } + return ProjectionModeName.FLAT; +} diff --git a/src/utils/store-instance.js b/src/utils/store-instance.js new file mode 100644 index 0000000000..6332f6c8e4 --- /dev/null +++ b/src/utils/store-instance.js @@ -0,0 +1,11 @@ +import Store from "../storage/store"; + +let store; +export function getStore() { + if (store) { + return store; + } else { + store = new Store(); + return store; + } +} diff --git a/src/utils/store-instance.ts b/src/utils/store-instance.ts deleted file mode 100644 index bb7c358f31..0000000000 --- a/src/utils/store-instance.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Store from "../storage/store"; - -export const store = new Store(); diff --git a/src/utils/theme.js b/src/utils/theme.js index 654865114f..9597716f05 100644 --- a/src/utils/theme.js +++ b/src/utils/theme.js @@ -1,5 +1,5 @@ import { waitForDOMContentLoaded } from "./async-utils"; -import { store } from "./store-instance"; +import { getStore } from "./store-instance"; // NOTE these should be synchronized with the top of shared.scss const DEFAULT_ACTION_COLOR = "#FF3464"; @@ -99,6 +99,7 @@ function tryGetTheme(themeId) { } function getCurrentTheme() { + const store = getStore(); const preferredThemeId = store.state?.preferences?.theme; return tryGetTheme(preferredThemeId); } @@ -153,6 +154,7 @@ function applyThemeToBody() { } function onThemeChanged(listener) { + const store = getStore(); store.addEventListener("themechanged", listener); const [_darkModeQuery, removeDarkModeListener] = registerDarkModeQuery(listener); diff --git a/src/utils/three-utils.js b/src/utils/three-utils.js index d9808919b5..b2b9238298 100644 --- a/src/utils/three-utils.js +++ b/src/utils/three-utils.js @@ -23,16 +23,66 @@ export function getLastWorldScale(src, target) { } export function disposeMaterial(mtrl) { - if (mtrl.map) mtrl.map.dispose(); - if (mtrl.lightMap) mtrl.lightMap.dispose(); - if (mtrl.bumpMap) mtrl.bumpMap.dispose(); - if (mtrl.normalMap) mtrl.normalMap.dispose(); - if (mtrl.specularMap) mtrl.specularMap.dispose(); - if (mtrl.envMap) mtrl.envMap.dispose(); - if (mtrl.aoMap) mtrl.aoMap.dispose(); - if (mtrl.metalnessMap) mtrl.metalnessMap.dispose(); - if (mtrl.roughnessMap) mtrl.roughnessMap.dispose(); - if (mtrl.emissiveMap) mtrl.emissiveMap.dispose(); + if (mtrl.map) { + mtrl.map.dispose(); + if (mtrl.map.eid) { + removeEntity(APP.world, mtrl.map.eid); + } + } + if (mtrl.lightMap) { + mtrl.lightMap.dispose(); + if (mtrl.lightMap.eid) { + removeEntity(APP.world, mtrl.lightMap.eid); + } + } + if (mtrl.bumpMap) { + mtrl.bumpMap.dispose(); + if (mtrl.bumpMap.eid) { + removeEntity(APP.world, mtrl.bumpMap.eid); + } + } + if (mtrl.normalMap) { + mtrl.normalMap.dispose(); + if (mtrl.normalMap.eid) { + removeEntity(APP.world, mtrl.normalMap.eid); + } + } + if (mtrl.specularMap) { + mtrl.specularMap.dispose(); + if (mtrl.specularMap.eid) { + removeEntity(APP.world, mtrl.specularMap.eid); + } + } + if (mtrl.envMap) { + mtrl.envMap.dispose(); + if (mtrl.envMap.eid) { + removeEntity(APP.world, mtrl.envMap.eid); + } + } + if (mtrl.aoMap) { + mtrl.aoMap.dispose(); + if (mtrl.aoMap.eid) { + removeEntity(APP.world, mtrl.aoMap.eid); + } + } + if (mtrl.metalnessMap) { + mtrl.metalnessMap.dispose(); + if (mtrl.metalnessMap.eid) { + removeEntity(APP.world, mtrl.metalnessMap.eid); + } + } + if (mtrl.roughnessMap) { + mtrl.roughnessMap.dispose(); + if (mtrl.roughnessMap.eid) { + removeEntity(APP.world, mtrl.roughnessMap.eid); + } + } + if (mtrl.emissiveMap) { + mtrl.emissiveMap.dispose(); + if (mtrl.emissiveMap.eid) { + removeEntity(APP.world, mtrl.emissiveMap.eid); + } + } mtrl.dispose(); if (mtrl.eid) { removeEntity(APP.world, mtrl.eid); diff --git a/src/verify.js b/src/verify.js index 1546d6c562..03719c9aad 100644 --- a/src/verify.js +++ b/src/verify.js @@ -10,10 +10,11 @@ import "./assets/stylesheets/globals.scss"; import { PageContainer } from "./react-components/layout/PageContainer"; import { Center } from "./react-components/layout/Center"; import { ThemeProvider } from "./react-components/styles/theme"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; registerTelemetry("/verify", "Hubs Verify Email Page"); +const store = getStore(); window.APP = { store }; function VerifyRoot() { diff --git a/src/whats-new.js b/src/whats-new.js index de1887e296..af9d68342e 100644 --- a/src/whats-new.js +++ b/src/whats-new.js @@ -5,8 +5,9 @@ import markdownit from "markdown-it"; import { FormattedMessage } from "react-intl"; import { WrappedIntlProvider } from "./react-components/wrapped-intl-provider"; import { AuthContextProvider } from "./react-components/auth/AuthContext"; -import { store } from "./utils/store-instance"; +import { getStore } from "./utils/store-instance"; +const store = getStore(); window.APP = { store }; import registerTelemetry from "./telemetry"; diff --git a/tsconfig.json b/tsconfig.json index cbe1e73df6..60088b491a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,13 +12,7 @@ "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, - "typeRoots": [ - "./node_modules/@types", - "./types" - ] + "typeRoots": ["./node_modules/@types", "./types"] }, - "include": [ - "types/", - "src/" - ] + "include": ["types/", "src/"] } diff --git a/types/aframe.d.ts b/types/aframe.d.ts index dbad160219..5d91b871d3 100644 --- a/types/aframe.d.ts +++ b/types/aframe.d.ts @@ -8,7 +8,10 @@ declare module "aframe" { [name: string]: Object3D; }; getObject3D(string): Object3D?; - components: { [s: string]: AComponent }; + components: { + [s: string]: AComponent; + "player-info": PlayerInfo; + }; eid: number; isPlaying: boolean; } @@ -92,6 +95,10 @@ declare module "aframe" { disable(): void; } + interface PlayerInfo extends AComponent { + playerSessionId: string; + } + interface PersonalSpaceBubbleSystem extends ASystem { invaders: PersonalSpaceInvader[]; } diff --git a/types/assets.d.ts b/types/assets.d.ts index dd05f0f49d..6fb7d55dc1 100644 --- a/types/assets.d.ts +++ b/types/assets.d.ts @@ -17,3 +17,8 @@ declare module "*.glb" { const url: string; export default url; } + +declare module "*.mp3" { + const src: string; + export default url; +} diff --git a/types/three.d.ts b/types/three.d.ts index 5ef7d9bbe8..a019ee3379 100644 --- a/types/three.d.ts +++ b/types/three.d.ts @@ -21,10 +21,16 @@ declare module "three" { group: GeometryGroup ) => void; } + + interface Texture { + eid?: number; + } interface Mesh { reflectionProbeMode: "static" | "dynamic" | false; } - + interface AnimationAction { + eid?: number; + } interface Vector3 { near: Function; } diff --git a/types/troika-three-text.d.ts b/types/troika-three-text.d.ts index e099337d2e..9dbe86a730 100644 --- a/types/troika-three-text.d.ts +++ b/types/troika-three-text.d.ts @@ -3,8 +3,17 @@ declare module "troika-three-text" { export class Text extends Mesh { text: string; - anchorX: number | `${number}%` | 'left' | 'center' | 'right'; - anchorY: number | `${number}%` | 'top' | 'top-baseline' | 'top-cap' | 'top-ex' | 'middle' | 'bottom-baseline' | 'bottom'; + anchorX: number | `${number}%` | "left" | "center" | "right"; + anchorY: + | number + | `${number}%` + | "top" + | "top-baseline" + | "top-cap" + | "top-ex" + | "middle" + | "bottom-baseline" + | "bottom"; clipRect: [number, number, number, number] | null; color: string | number | Color | null; curveRadius: number; @@ -16,7 +25,7 @@ declare module "troika-three-text" { glyphGeometryDetail: number; gpuAccelerateSDF: boolean; letterSpacing: number; - lineHeight: number | 'normal'; + lineHeight: number | "normal"; material: Material | null; maxWidth: number; outlineBlur: number | `${number}%`; @@ -25,15 +34,16 @@ declare module "troika-three-text" { outlineOffsetY: number | `${number}%`; outlineOpacity: number; outlineWidth: number | `${number}%`; - overflowWrap: 'normal' | 'break-word'; + overflowWrap: "normal" | "break-word"; sdfGlyphSize: number | null; strokeColor: string | number | Color; strokeOpacity: number; strokeWidth: number | `${number}%`; - textAlign: 'left' | 'right' | 'center' | 'justify'; + textAlign: "left" | "right" | "center" | "justify"; textIndent: number; - whiteSpace: 'normal' | 'nowrap'; + whiteSpace: "normal" | "nowrap"; sync(callback?: () => void): void; + isTroikaText: true; } export const preloadFont: ( diff --git a/webpack.config.js b/webpack.config.js index 974c0bd98b..0a3f660491 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -285,6 +285,9 @@ module.exports = async (env, argv) => { // .replaceAll("connect-src", "connect-src https://example.com"); } + const addonsConfigFilePath = "./addons.json"; + const addonsConfig = JSON.parse(fs.readFileSync(addonsConfigFilePath, "utf-8")); + const internalHostname = process.env.INTERNAL_HOSTNAME || "hubs.local"; return { cache: { @@ -303,7 +306,9 @@ module.exports = async (env, argv) => { "three/examples/js/libs/basis/basis_transcoder.js": basisTranscoderPath, "three/examples/js/libs/draco/gltf/draco_wasm_wrapper.js": dracoWasmWrapperPath, "three/examples/js/libs/basis/basis_transcoder.wasm": basisWasmPath, - "three/examples/js/libs/draco/gltf/draco_decoder.wasm": dracoWasmPath + "three/examples/js/libs/draco/gltf/draco_decoder.wasm": dracoWasmPath, + + hubs$: path.resolve(__dirname, "./src/hubs.js") }, // Allows using symlinks in node_modules symlinks: false, @@ -320,7 +325,7 @@ module.exports = async (env, argv) => { entry: { support: path.join(__dirname, "src", "support.js"), index: path.join(__dirname, "src", "index.js"), - hub: path.join(__dirname, "src", "hub.js"), + hub: [path.join(__dirname, "src", "hub.js"), ...addonsConfig.addons], scene: path.join(__dirname, "src", "scene.js"), avatar: path.join(__dirname, "src", "avatar.js"), link: path.join(__dirname, "src", "link.js"), @@ -336,6 +341,9 @@ module.exports = async (env, argv) => { filename: "assets/js/[name]-[chunkhash].js", publicPath: process.env.BASE_ASSETS_PATH || "" }, + optimization: { + minimize: argv.mode === "production" ? true : false + }, target: ["web", "es5"], // use es5 for webpack runtime to maximize compatibility devtool: argv.mode === "production" ? "source-map" : "inline-source-map", devServer: {