From eccb8409b1bf3cdebe5dacff3c7719154f406399 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Mon, 29 Jan 2024 11:09:30 +0100 Subject: [PATCH 01/40] Initial add-ons API commit --- addons.json | 3 + package-lock.json | 19 ++ package.json | 2 + src/addons.ts | 184 ++++++++++++++++++ src/app.ts | 29 ++- src/bit-components.js | 1 - src/bit-systems/quack.ts | 18 -- src/hub.js | 32 ++- src/hubs.js | 27 +++ src/message-dispatch.js | 30 ++- src/prefabs/duck.tsx | 52 ----- src/prefabs/prefabs.ts | 20 +- .../room/RoomSettingsSidebar.js | 11 ++ src/react-components/ui-root.js | 9 + src/schema.toml | 4 +- src/systems/hubs-systems.ts | 32 ++- src/systems/sound-effects-system.js | 39 ++-- src/systems/userinput/userinput.js | 37 ++++ src/types.ts | 172 ++++++++++++++++ src/utils/create-networked-entity.ts | 7 +- src/utils/jsx-entity.ts | 32 ++- src/utils/networking-types.ts | 6 +- src/utils/permissions.ts | 5 +- tsconfig.json | 10 +- types/assets.d.ts | 5 + webpack.config.js | 9 +- 26 files changed, 635 insertions(+), 160 deletions(-) create mode 100644 addons.json create mode 100644 src/addons.ts delete mode 100644 src/bit-systems/quack.ts create mode 100644 src/hubs.js delete mode 100644 src/prefabs/duck.tsx create mode 100644 src/types.ts diff --git a/addons.json b/addons.json new file mode 100644 index 0000000000..8faa66d089 --- /dev/null +++ b/addons.json @@ -0,0 +1,3 @@ +{ + "addons": ["hubs-duck-addon", "hubs-portals-addon"] +} diff --git a/package-lock.json b/package-lock.json index a7e4d7d7b1..b6541eeeba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,8 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -16460,6 +16462,23 @@ "node": ">= 6" } }, + "node_modules/hubs-duck-addon": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#9bc41593e9a69b32f3e91a87ff89ffa1daa49f28", + "license": "MPL-2.0", + "peerDependencies": { + "three": "^0.141.0" + } + }, + "node_modules/hubs-portals-addon": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-portals-addon.git#9a92d909dc2c8badc08f29d62967c0d22458f002", + "license": "MPL-2.0", + "peerDependencies": { + "bitecs": "github:mozilla/bitECS#hubs-patches", + "three": "^0.141.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", diff --git a/package.json b/package.json index b71723c7c3..1f3a891916 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,8 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", diff --git a/src/addons.ts b/src/addons.ts new file mode 100644 index 0000000000..9be4e7ba04 --- /dev/null +++ b/src/addons.ts @@ -0,0 +1,184 @@ +import { App } from "./app"; +import { prefabs } from "./prefabs/prefabs"; + +import { + InflatorConfigT, + SystemConfigT, + SystemOrderE, + PrefabConfigT, + NetworkSchemaConfigT, + ChatCommandConfigT +} from "./types"; +import configs from "./utils/configs"; +import { commonInflators, gltfInflators, jsxInflators } from "./utils/jsx-entity"; +import { networkableComponents, schemas } from "./utils/network-schemas"; + +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.MatricesUpdate) { + slot = APP.addon_systems.postPhysics; + } else if (system.order < SystemOrderE.BeforeRender) { + slot = APP.addon_systems.beforeRender; + } else if (system.order < SystemOrderE.AfterRender) { + slot = APP.addon_systems.afterRender; + } else if (system.order < SystemOrderE.PostProcessing) { + slot = APP.addon_systems.postProcessing; + } else { + slot = APP.addon_systems.tearDown; + } + 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 AddonOnReadyFn = (app: App, config?: JSON) => void; + +export interface InternalAddonConfigT { + name: AddonNameT; + description?: AddonDescriptionT; + onReady?: AddonOnReadyFn; + system?: SystemConfigT | SystemConfigT[]; + inflator?: InflatorConfigT | InflatorConfigT[]; + prefab?: PrefabConfigT | PrefabConfigT[]; + networkSchema?: NetworkSchemaConfigT | NetworkSchemaConfigT[]; + chatCommand?: ChatCommandConfigT | ChatCommandConfigT[]; + enabled?: boolean; + config?: JSON | undefined; +} +type AddonConfigT = Omit; + +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); +} + +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 (app.hub?.user_data && `addon_${id}` in app.hub.user_data) { + addon.enabled = app.hub.user_data[`addon_${id}`]; + } else { + addon.enabled = false; + } + + if (!addon.enabled) { + 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) { + let config; + const addonsConfig = configs.feature("addons_config"); + if (addonsConfig && id in addonsConfig) { + config = addonsConfig[id]; + } + addon.onReady(app, config); + } + } + pendingAddons.clear(); + }); +} diff --git a/src/app.ts b/src/app.ts index 16a03c475c..afe0198704 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,8 @@ import SceneEntryManager from "./scene-entry-manager"; import { store } 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 { @@ -63,7 +65,7 @@ export function getScene() { return promiseToScene; } -interface HubDescription { +export interface HubDescription { hub_id: string; user_data?: any; } @@ -104,6 +106,18 @@ 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 }>(), + matricesUpdate: new Array<{ order: number; system: SystemT }>(), + beforeRender: new Array<{ order: number; system: SystemT }>(), + render: new Array<{ order: number; system: SystemT }>(), + afterRender: new Array<{ order: number; system: SystemT }>(), + postProcessing: new Array<{ order: number; system: SystemT }>(), + tearDown: new Array<{ order: number; system: SystemT }>() + }; + RENDER_ORDER = { HUD_BACKGROUND: 1, HUD_ICONS: 2, @@ -159,6 +173,19 @@ export class App { return this.sid2str.get(sid); } + notifyOnInit() { + onAddonsInit(this); + } + + 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 7febf81698..93a284edf1 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -447,7 +447,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/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/hub.js b/src/hub.js index fb609a6b7e..cf80b6d21e 100644 --- a/src/hub.js +++ b/src/hub.js @@ -273,6 +273,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 { addons } from "./addons"; const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; @@ -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) { @@ -1388,16 +1399,27 @@ 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) { showBitECSBasedClientRefreshPrompt = true; setTimeout(() => { document.location.reload(); }, 5000); } + let showAddonRefreshPrompt = false; + [...addons.keys()].map(id => { + const key = `addon_${id}`; + const oldAddonState = !!window.APP.hub.user_data && window.APP.hub.user_data[key]; + const newAddonState = !!hub.user_data && hub.user_data[key]; + 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 +1506,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..121abfccf1 --- /dev/null +++ b/src/hubs.js @@ -0,0 +1,27 @@ +export * from "./utils/create-networked-entity"; +export * from "./bit-components"; +export * from "./utils/bit-utils"; +export * from "./addons"; +export * from "./inflators/physics-shape"; +export * from "./constants"; +export * from "./utils/jsx-entity"; +export * from "./utils/media-url-utils"; +export * from "./systems/floaty-object-system"; +export * from "./types"; +export * from "./camera-layers"; +export * from "./bit-systems/delete-entity-system"; +export * from "./utils/bit-pinning-helper"; +export * from "./components/gltf-model-plus"; +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 "./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"; 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/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/room/RoomSettingsSidebar.js b/src/react-components/room/RoomSettingsSidebar.js index fff9f2918e..31607cf861 100644 --- a/src/react-components/room/RoomSettingsSidebar.js +++ b/src/react-components/room/RoomSettingsSidebar.js @@ -16,6 +16,7 @@ import { BackButton } from "../input/BackButton"; import { SceneInfo } from "./RoomSidebar"; import { Column } from "../layout/Column"; import { InviteLinkInputField } from "./InviteLinkInputField"; +import { addons } from "../../addons"; export function RoomSettingsSidebar({ showBackButton, @@ -216,6 +217,16 @@ export function RoomSettingsSidebar({ {...register("user_data.hubs_use_bitecs_based_client")} /> + + {[...addons.entries()].map(([id, addon]) => ( + + ))} + diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 2b07fcbdcc..52410e7f9c 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/schema.toml b/src/schema.toml index f36c360117..69f7aec198 100644 --- a/src/schema.toml +++ b/src/schema.toml @@ -49,6 +49,8 @@ features.show_newsletter_signup = { category = "features", type = "boolean", int features.change_hub_near_room_links = { category = "features", type = "boolean", internal = "true" } features.is_locked_down_demo_room = { category = "features", type = "string", internal = "true", description = "A comma separated list of hubIds to be designated as demo rooms with simplified UI." } +features.addons_config = { category = "features", type="longstring", name="Add-ons config JSON", description="Add-ons config JSON file" } + images.logo = { category = "images", type = "file", name = "Hub Logo", description = "Appears throughout your hub including lobby and loading screens." } images.logo_dark = { category = "images", type = "file", name = "Hub logo for dark mode", description = "The hub logo which appears for visitors who have dark mode enabled." } images.favicon = { category = "images", type = "file", name = "Favicon", description = "The favicon is the small picture which appears in the web browser tab." } @@ -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/systems/hubs-systems.ts b/src/systems/hubs-systems.ts index e656237cd3..0fbd77a15c 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"; @@ -94,6 +93,7 @@ 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 { @@ -199,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); @@ -214,11 +218,19 @@ 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); + APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + hoverableVisualsSystem(world); // We run this earlier in the frame so things have a chance to override properties run by animations @@ -283,7 +295,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); @@ -322,13 +333,30 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene scene.updateMatrixWorld(); + APP.addon_systems.matricesUpdate.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + renderer.info.reset(); if (APP.fx.composer) { + APP.addon_systems.postProcessing.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); APP.fx.composer.render(); } else { + APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); 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 + + APP.addon_systems.tearDown.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); } 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/types.ts b/src/types.ts new file mode 100644 index 0000000000..d5ecb3a251 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,172 @@ +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, + MatricesUpdate = 300, + BeforeRender = 400, + AfterRender = 500, + PostProcessing = 600, + TearDown = 700 +} + +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; +} + +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/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 8273eef3c9..5aff6253f4 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, @@ -102,6 +101,7 @@ import { inflateObjectMenuTarget, ObjectMenuTargetParams } from "../inflators/ob 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"; preload( new Promise(resolve => { @@ -143,7 +143,7 @@ export type Attrs = { }; export type EntityDef = { - components: JSXComponentData; + components: ComponentDataT; attrs: Attrs; children: EntityDef[]; ref?: Ref; @@ -153,10 +153,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 || {}; @@ -164,7 +164,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) { @@ -174,7 +174,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]; } } @@ -221,7 +221,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); @@ -305,7 +305,6 @@ export interface JSXComponentData extends ComponentData { deletable?: true; makeKinematicOnRelease?: true; destroyAtExtremeDistance?: true; - quack?: true; // @TODO Define all the anys textButton?: any; @@ -417,7 +416,7 @@ export interface GLTFComponentData extends ComponentData { declare global { namespace createElementEntity.JSX { interface IntrinsicElements { - entity: JSXComponentData & + entity: ComponentDataT & Attrs & { children?: IntrinsicElements[]; }; @@ -429,7 +428,8 @@ 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), // inflators that create Object3Ds @@ -444,11 +444,10 @@ export const commonInflators: Required<{ [K in keyof ComponentData]: InflatorFn mediaFrame: inflateMediaFrame, text: inflateText, networkedTransform: createDefaultInflator(NetworkedTransform), - networked: createDefaultInflator(Networked), - grabbable: inflateGrabbable + 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), @@ -485,7 +484,6 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { waypointPreview: createDefaultInflator(WaypointPreview), pdf: inflatePDF, mediaLoader: inflateMediaLoader, - quack: createDefaultInflator(Quack), mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, inspectable: createDefaultInflator(Inspectable), @@ -501,7 +499,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 @@ -539,11 +537,11 @@ export const gltfInflators: Required<{ [K in keyof GLTFComponentData]: InflatorF physicsShape: inflateAmmoShape }; -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/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/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/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/webpack.config.js b/webpack.config.js index 974c0bd98b..c52b924df0 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"), From 4e69cb622d2171e4086c1649eaba17f9ca2b03f9 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 15 Feb 2024 12:14:16 +0100 Subject: [PATCH 02/40] Try to get some more info about the build error --- RetPageOriginDockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 1700393d75..8914b95607 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,7 +6,7 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci +run npm ci -dd copy admin/package.json admin/ copy admin/package-lock.json admin/ run cd admin && npm ci --legacy-peer-deps && cd .. From 1b48d6e5602f4d4e7b481b2e528a92dee270cf2f Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 15 Feb 2024 12:52:50 +0100 Subject: [PATCH 03/40] Do not install dev dependencies in production --- RetPageOriginDockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 8914b95607..837122a4bc 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -9,7 +9,7 @@ copy package-lock.json ./ run npm ci -dd copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --legacy-peer-deps && cd .. +run cd admin && NODE_ENV=production npm ci --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From 3cc6d092eab60bbde1cdad59d77dd34b8bf8d78d Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 15 Feb 2024 14:20:25 +0100 Subject: [PATCH 04/40] Revert tests --- RetPageOriginDockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 837122a4bc..1700393d75 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci -dd +run npm ci copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && NODE_ENV=production npm ci --legacy-peer-deps && cd .. +run cd admin && npm ci --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From f3d84cc3ca13ccc8700a797014a60339ffc4209b Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 15 Feb 2024 14:51:56 +0100 Subject: [PATCH 05/40] organize types --- src/hubs.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/hubs.js b/src/hubs.js index 121abfccf1..a335c5af2e 100644 --- a/src/hubs.js +++ b/src/hubs.js @@ -1,17 +1,13 @@ -export * from "./utils/create-networked-entity"; export * from "./bit-components"; -export * from "./utils/bit-utils"; export * from "./addons"; -export * from "./inflators/physics-shape"; +export * from "./types"; +export * from "./camera-layers"; export * from "./constants"; +export * from "./utils/bit-utils"; export * from "./utils/jsx-entity"; export * from "./utils/media-url-utils"; -export * from "./systems/floaty-object-system"; -export * from "./types"; -export * from "./camera-layers"; -export * from "./bit-systems/delete-entity-system"; export * from "./utils/bit-pinning-helper"; -export * from "./components/gltf-model-plus"; +export * from "./utils/create-networked-entity"; export * from "./utils/material-utils"; export * from "./utils/network-schemas"; export * from "./utils/define-network-schema"; @@ -19,6 +15,11 @@ export * from "./utils/animate"; export * from "./utils/easing"; export * from "./utils/coroutine"; export * from "./utils/coroutine-utils"; +export * from "./components/gltf-model-plus"; +export * from "./inflators/physics-shape"; +export * from "./inflators/media-frame"; +export * from "./bit-systems/delete-entity-system"; +export * from "./systems/floaty-object-system"; export * from "./systems/userinput/paths"; export * from "./systems/userinput/sets"; export * from "./systems/userinput/userinput"; From 4ff2d0e022da2859e3ae03c7c8599960c7e54036 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 22 Feb 2024 19:00:05 +0100 Subject: [PATCH 06/40] Test --- RetPageOriginDockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 1700393d75..6ef96adf8a 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci +run npm ci --force copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --legacy-peer-deps && cd .. +run cd admin && npm ci --force --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From 460930fea95ecd97b6ba1cd464abd6622ef76bd0 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 10 Apr 2024 09:47:04 +0200 Subject: [PATCH 07/40] Omit dev packages when installing deps in production --- RetPageOriginDockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 6ef96adf8a..e2e5cb0076 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci --force +run npm ci --omit=dev copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --force --legacy-peer-deps && cd .. +run cd admin && npm ci --omit=dev --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From 3726836d20f77a81cb531398dd148355fdcb8469 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 10 Apr 2024 13:39:23 +0200 Subject: [PATCH 08/40] Package lock update --- RetPageOriginDockerfile | 4 ++-- admin/package-lock.json | 48 ++++++++++++++++++++++++----------------- package.json | 6 +++--- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index e2e5cb0076..1700393d75 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci --omit=dev +run npm ci copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --omit=dev --legacy-peer-deps && cd .. +run cd admin && npm ci --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null diff --git a/admin/package-lock.json b/admin/package-lock.json index 788709be8c..dcb978fe40 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -78,7 +78,7 @@ "@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:mozillareality/aframe#hubs/master", @@ -96,13 +96,15 @@ "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:mozillareality/hubs-duck-addon", + "hubs-portals-addon": "github:mozillareality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -135,6 +137,7 @@ "screenfull": "^4.0.1", "sdp-transform": "^2.14.1", "semver": "^7.3.2", + "stream-browserify": "^3.0.0", "three": "github:mozillareality/three.js#65b5105908f5f135cad25fed07e25f15f3876777", "three-ammo": "github:mozillareality/three-ammo", "three-gltf-extensions": "^0.0.14", @@ -142,6 +145,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", @@ -216,6 +219,7 @@ "selfsigned": "^2.0.1", "shelljs": "^0.8.5", "spritesheet-js": "github:mozillareality/spritesheet.js#hubs/master", + "storybook": "^7.0.20", "style-loader": "^3.3.1", "stylelint": "^14.9.1", "stylelint-config-recommended-scss": "^7.0.0", @@ -15117,16 +15121,15 @@ "@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", @@ -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,6 +15179,8 @@ "html-webpack-plugin": "^5.5.0", "html2canvas": "^1.0.0-rc.7", "htmlhint": "^1.1.4", + "hubs-duck-addon": "github:mozillareality/hubs-duck-addon", + "hubs-portals-addon": "github:mozillareality/hubs-portals-addon", "internal-ip": "^7.0.0", "js-cookie": "^2.2.0", "jsdom": "^20.0.0", @@ -15229,6 +15234,8 @@ "semver": "^7.3.2", "shelljs": "^0.8.5", "spritesheet-js": "github:mozillareality/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", @@ -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", diff --git a/package.json b/package.json index 1f3a891916..be322f2d7f 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,8 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", - "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", + "hubs-duck-addon": "github:mozillareality/hubs-duck-addon", + "hubs-portals-addon": "github:mozillareality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -236,4 +236,4 @@ "optionalDependencies": { "fsevents": "^2.2.1" } -} +} \ No newline at end of file From 2781f1ff995a978c37804ed35d6e8a464cf79dc3 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 10 Apr 2024 13:49:33 +0200 Subject: [PATCH 09/40] Increase log level --- RetPageOriginDockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 1700393d75..71622dfebe 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci +run npm ci --loglevel verbose copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --legacy-peer-deps && cd .. +run cd admin && npm ci --loglevel verbose --legacy-peer-deps && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From ca5a91e09baeedf7b960800b1528d8df3a074e90 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 10 Apr 2024 14:34:08 +0200 Subject: [PATCH 10/40] Try --legacy-peer-deps --- RetPageOriginDockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index 71622dfebe..df437a6f97 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,7 +6,7 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci --loglevel verbose +run npm ci --loglevel verbose --legacy-peer-deps copy admin/package.json admin/ copy admin/package-lock.json admin/ run cd admin && npm ci --loglevel verbose --legacy-peer-deps && cd .. From 4a432f93575b4a268486b31b6f6c33af4e5d19ca Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 17 Apr 2024 17:22:01 +0200 Subject: [PATCH 11/40] Use webpack in addons instead of tsup --- package-lock.json | 15 +++------------ package.json | 5 ++--- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6541eeeba..db4538b143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,8 +46,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", - "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#webpack", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -16464,19 +16463,11 @@ }, "node_modules/hubs-duck-addon": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#9bc41593e9a69b32f3e91a87ff89ffa1daa49f28", - "license": "MPL-2.0", - "peerDependencies": { - "three": "^0.141.0" - } - }, - "node_modules/hubs-portals-addon": { - "version": "1.0.0", - "resolved": "git+ssh://git@github.com/MozillaReality/hubs-portals-addon.git#9a92d909dc2c8badc08f29d62967c0d22458f002", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#09d4a2646e6083804286983aca3cfaf1165bcd46", "license": "MPL-2.0", "peerDependencies": { "bitecs": "github:mozilla/bitECS#hubs-patches", - "three": "^0.141.0" + "three": "github:mozillareality/three.js#65b5105908f5f135cad25fed07e25f15f3876777" } }, "node_modules/human-signals": { diff --git a/package.json b/package.json index be322f2d7f..4712688cfb 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:mozillareality/hubs-duck-addon", - "hubs-portals-addon": "github:mozillareality/hubs-portals-addon", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#webpack", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -236,4 +235,4 @@ "optionalDependencies": { "fsevents": "^2.2.1" } -} \ No newline at end of file +} From f0da86ab470cc14c6c9eeef273d46aac99583e22 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 17 Apr 2024 17:37:28 +0200 Subject: [PATCH 12/40] Revert docker image changes --- RetPageOriginDockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetPageOriginDockerfile b/RetPageOriginDockerfile index df437a6f97..4ae1bc068d 100644 --- a/RetPageOriginDockerfile +++ b/RetPageOriginDockerfile @@ -6,10 +6,10 @@ from node:16.16 as builder run mkdir -p /hubs/admin/ && cd /hubs copy package.json ./ copy package-lock.json ./ -run npm ci --loglevel verbose --legacy-peer-deps +run npm ci copy admin/package.json admin/ copy admin/package-lock.json admin/ -run cd admin && npm ci --loglevel verbose --legacy-peer-deps && cd .. +run cd admin && npm ci && cd .. copy . . env BASE_ASSETS_PATH="{{rawhubs-base-assets-path}}" run npm run build 1> /dev/null From 174c6facbd763e124dcefaecaf2fb1bda03517e5 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 17 Apr 2024 17:56:40 +0200 Subject: [PATCH 13/40] Remove portals addon --- addons.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons.json b/addons.json index 8faa66d089..e062441328 100644 --- a/addons.json +++ b/addons.json @@ -1,3 +1,5 @@ { - "addons": ["hubs-duck-addon", "hubs-portals-addon"] -} + "addons": [ + "hubs-duck-addon" + ] +} \ No newline at end of file From 56f6567c9a375e328f6e8b204bf7f80226881876 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 18 Apr 2024 14:07:28 +0200 Subject: [PATCH 14/40] Update addon version --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db4538b143..31be3fafde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#webpack", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -16463,7 +16463,8 @@ }, "node_modules/hubs-duck-addon": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#09d4a2646e6083804286983aca3cfaf1165bcd46", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", + "integrity": "sha512-CzpTLQqpYXsOq0tu0ui//msVwHz5hthlMAEHXIIhzi2mv/7t6h2GI8Y9X8KFiStbDDtYUpGrZNelGeDQIZ8NJg==", "license": "MPL-2.0", "peerDependencies": { "bitecs": "github:mozilla/bitECS#hubs-patches", diff --git a/package.json b/package.json index 4712688cfb..dde83d4c0f 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#webpack", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", From f3d12c4cef2037a99ac42df9ce9b51ba65bcb784 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 24 Apr 2024 09:47:22 +0200 Subject: [PATCH 15/40] Update addons --- addons.json | 3 ++- package-lock.json | 15 ++++++++++++--- package.json | 3 ++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/addons.json b/addons.json index e062441328..8d9496e075 100644 --- a/addons.json +++ b/addons.json @@ -1,5 +1,6 @@ { "addons": [ - "hubs-duck-addon" + "hubs-duck-addon", + "hubs-portals-addon" ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 31be3fafde..f182e3a194 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,8 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -16463,8 +16464,16 @@ }, "node_modules/hubs-duck-addon": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", - "integrity": "sha512-CzpTLQqpYXsOq0tu0ui//msVwHz5hthlMAEHXIIhzi2mv/7t6h2GI8Y9X8KFiStbDDtYUpGrZNelGeDQIZ8NJg==", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#475a1e34504cbccd8b7125d16a601af76c9d0a4c", + "license": "MPL-2.0", + "peerDependencies": { + "bitecs": "github:mozilla/bitECS#hubs-patches", + "three": "github:mozillareality/three.js#65b5105908f5f135cad25fed07e25f15f3876777" + } + }, + "node_modules/hubs-portals-addon": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-portals-addon.git#828cf9000a49e037ad40f05f1a9a5ff679aed851", "license": "MPL-2.0", "peerDependencies": { "bitecs": "github:mozilla/bitECS#hubs-patches", diff --git a/package.json b/package.json index dde83d4c0f..1f3a891916 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,8 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#8fe76cf21ace54e0c7dc14bec292d598bbc6ae79", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", From 22408979465ba6f6537e08aa229b38b7f8423de2 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 8 May 2024 17:45:27 +0200 Subject: [PATCH 16/40] BGs initial support --- addons.json | 3 +- src/addons.ts | 76 ++-- src/app.ts | 10 +- src/bit-components.js | 47 ++- src/bit-systems/audio-emitter-system.ts | 8 + src/bit-systems/loop-animation.ts | 8 +- src/bit-systems/media-loading.ts | 6 +- src/bit-systems/text.ts | 112 +++++- src/bit-systems/video-system.ts | 157 ++++++++- src/components/gltf-model-plus.js | 14 + src/constants.ts | 3 +- src/hub.html | 5 +- src/hub.js | 7 +- src/hubs.js | 14 + src/inflators/grabbable.ts | 4 +- src/inflators/holdable.ts | 20 ++ src/inflators/loop-animation.ts | 10 +- src/inflators/model.tsx | 14 +- src/inflators/text.ts | 333 +++++++++++++++++- src/inflators/video.ts | 7 +- src/load-media-on-paste-or-drop.ts | 1 + src/prefabs/loading-object.js | 10 +- .../debug-panel/ECSSidebar.js | 48 ++- src/react-components/room/ChatSidebar.js | 2 + .../room/RoomSettingsSidebar.js | 27 +- .../room/RoomSettingsSidebar.scss | 13 +- .../room/contexts/ChatContext.tsx | 1 + src/schema.toml | 4 +- src/systems/bit-media-frames.js | 95 +++-- src/systems/hold-system.js | 2 + src/systems/hubs-systems.ts | 36 +- src/systems/remove-object3D-system.js | 8 + src/types.ts | 6 +- src/utils/assign-network-ids.ts | 11 + src/utils/bit-utils.ts | 13 + src/utils/jsx-entity.ts | 13 +- src/utils/load-model.tsx | 1 - src/utils/load-video-texture.js | 5 +- src/utils/network-schemas.ts | 3 + src/utils/networked-text-schema.ts | 136 +++++++ src/utils/networked-video-schema.ts | 26 +- src/utils/projection-mode.ts | 9 + src/utils/three-utils.js | 70 +++- types/aframe.d.ts | 9 +- types/three.d.ts | 8 +- types/troika-three-text.d.ts | 22 +- 46 files changed, 1251 insertions(+), 186 deletions(-) create mode 100644 src/inflators/holdable.ts create mode 100644 src/utils/networked-text-schema.ts diff --git a/addons.json b/addons.json index 8d9496e075..cee928ef4e 100644 --- a/addons.json +++ b/addons.json @@ -1,6 +1,7 @@ { "addons": [ "hubs-duck-addon", - "hubs-portals-addon" + "hubs-portals-addon", + "hubs-behavior-graphs-addon" ] } \ No newline at end of file diff --git a/src/addons.ts b/src/addons.ts index 9be4e7ba04..286ca27c27 100644 --- a/src/addons.ts +++ b/src/addons.ts @@ -1,4 +1,5 @@ -import { App } from "./app"; +import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader"; +import { App, HubsWorld } from "./app"; import { prefabs } from "./prefabs/prefabs"; import { @@ -12,6 +13,11 @@ import { 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"; function getNextIdx(slot: Array, system: SystemConfigT) { return slot.findIndex(item => { @@ -25,16 +31,14 @@ function registerSystem(system: SystemConfigT) { slot = APP.addon_systems.setup; } else if (system.order < SystemOrderE.PostPhysics) { slot = APP.addon_systems.prePhysics; - } else if (system.order < SystemOrderE.MatricesUpdate) { + } else if (system.order < SystemOrderE.BeforeMatricesUpdate) { slot = APP.addon_systems.postPhysics; } else if (system.order < SystemOrderE.BeforeRender) { - slot = APP.addon_systems.beforeRender; + slot = APP.addon_systems.postPhysics; } else if (system.order < SystemOrderE.AfterRender) { - slot = APP.addon_systems.afterRender; - } else if (system.order < SystemOrderE.PostProcessing) { - slot = APP.addon_systems.postProcessing; + slot = APP.addon_systems.beforeRender; } else { - slot = APP.addon_systems.tearDown; + slot = APP.addon_systems.afterRender; } const nextIdx = getNextIdx(slot, system); slot.splice(nextIdx, 0, system); @@ -92,6 +96,10 @@ export interface InternalAddonConfigT { config?: JSON | undefined; } type AddonConfigT = Omit; +export type AdminAddonConfig = { + enabled: boolean; + config: JSON; +}; const pendingAddons = new Map(); export const addons = new Map(); @@ -101,6 +109,44 @@ export function registerAddon(id: AddonIdT, config: AddonConfigT) { pendingAddons.set(id, config); } +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); +} + +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 (shouldUseNewLoader()) { + 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; +} + export function onAddonsInit(app: App) { app.scene?.addEventListener("hub_updated", () => { for (const [id, addon] of pendingAddons) { @@ -110,13 +156,7 @@ export function onAddonsInit(app: App) { addons.set(id, addon); } - if (app.hub?.user_data && `addon_${id}` in app.hub.user_data) { - addon.enabled = app.hub.user_data[`addon_${id}`]; - } else { - addon.enabled = false; - } - - if (!addon.enabled) { + if (!isAddonEnabled(app, id)) { continue; } @@ -171,12 +211,8 @@ export function onAddonsInit(app: App) { } if (addon.onReady) { - let config; - const addonsConfig = configs.feature("addons_config"); - if (addonsConfig && id in addonsConfig) { - config = addonsConfig[id]; - } - addon.onReady(app, config); + const adminAddonConfig = getAddonConfig(id); + addon.onReady(app, adminAddonConfig.config); } } pendingAddons.clear(); diff --git a/src/app.ts b/src/app.ts index afe0198704..4a927ef10a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,6 +19,7 @@ import { PositionalAudio, Scene, sRGBEncoding, + Texture, WebGLRenderer } from "three"; import { AudioSettings, SourceType } from "./components/audio-params"; @@ -54,6 +55,7 @@ export interface HubsWorld extends IWorld { nid2eid: Map; eid2obj: Map; eid2mat: Map; + eid2tex: Map; time: { delta: number; elapsed: number; tick: number }; } @@ -110,12 +112,9 @@ export class App { setup: new Array<{ order: number; system: SystemT }>(), prePhysics: new Array<{ order: number; system: SystemT }>(), postPhysics: new Array<{ order: number; system: SystemT }>(), - matricesUpdate: new Array<{ order: number; system: SystemT }>(), + beforeMatricesUpdate: new Array<{ order: number; system: SystemT }>(), beforeRender: new Array<{ order: number; system: SystemT }>(), - render: new Array<{ order: number; system: SystemT }>(), - afterRender: new Array<{ order: number; system: SystemT }>(), - postProcessing: new Array<{ order: number; system: SystemT }>(), - tearDown: new Array<{ order: number; system: SystemT }>() + afterRender: new Array<{ order: number; system: SystemT }>() }; RENDER_ORDER = { @@ -135,6 +134,7 @@ export class App { // 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(); diff --git a/src/bit-components.js b/src/bit-components.js index 93a284edf1..09d6bb53d8 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -38,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({ @@ -61,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(); @@ -276,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, @@ -393,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], 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/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 b82e7cfcc4..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); 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/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 12090f11b9..2b778a400f 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/constants.ts b/src/constants.ts index 1fcb5e30e8..b69ca21272 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,9 +6,10 @@ 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 } 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 cf80b6d21e..66a29bf348 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1399,7 +1399,7 @@ 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(); @@ -1407,9 +1407,8 @@ document.addEventListener("DOMContentLoaded", async () => { } let showAddonRefreshPrompt = false; [...addons.keys()].map(id => { - const key = `addon_${id}`; - const oldAddonState = !!window.APP.hub.user_data && window.APP.hub.user_data[key]; - const newAddonState = !!hub.user_data && hub.user_data[key]; + 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(() => { diff --git a/src/hubs.js b/src/hubs.js index a335c5af2e..965e5b907d 100644 --- a/src/hubs.js +++ b/src/hubs.js @@ -1,8 +1,10 @@ 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"; @@ -15,10 +17,20 @@ 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 "./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"; @@ -26,3 +38,5 @@ 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/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/model.tsx b/src/inflators/model.tsx index ad26136b51..8632f98bc7 100644 --- a/src/inflators/model.tsx +++ b/src/inflators/model.tsx @@ -7,16 +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", "animation-mixer", "loop-animation"]; -function inflateComponents( +export function inflateComponents( world: HubsWorld, eid: number, components: { [componentName: string]: any }, @@ -51,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(); @@ -158,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/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/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/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/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/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index c7dab949fa..bd411ea078 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -324,6 +324,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 31607cf861..3d84ce08d2 100644 --- a/src/react-components/room/RoomSettingsSidebar.js +++ b/src/react-components/room/RoomSettingsSidebar.js @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { useForm } from "react-hook-form"; import styles from "./RoomSettingsSidebar.scss"; @@ -16,7 +16,8 @@ import { BackButton } from "../input/BackButton"; import { SceneInfo } from "./RoomSidebar"; import { Column } from "../layout/Column"; import { InviteLinkInputField } from "./InviteLinkInputField"; -import { addons } from "../../addons"; +import { addons, isAddonEnabled } from "../../addons"; +import { shouldUseNewLoader } from "../../hubs"; export function RoomSettingsSidebar({ showBackButton, @@ -53,6 +54,13 @@ export function RoomSettingsSidebar({ } }, [spawnAndMoveMedia, setValue]); + const handleAddonChange = useCallback( + evt => { + setValue(`user_data.addons.${evt.target.id}`, evt.target.checked); + }, + [setValue] + ); + return ( } @@ -217,12 +225,23 @@ export function RoomSettingsSidebar({ {...register("user_data.hubs_use_bitecs_based_client")} /> - + } fullWidth> + {!shouldUseNewLoader() && ( + + )} {[...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/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/schema.toml b/src/schema.toml index 69f7aec198..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" } @@ -49,8 +51,6 @@ features.show_newsletter_signup = { category = "features", type = "boolean", int features.change_hub_near_room_links = { category = "features", type = "boolean", internal = "true" } features.is_locked_down_demo_room = { category = "features", type = "string", internal = "true", description = "A comma separated list of hubIds to be designated as demo rooms with simplified UI." } -features.addons_config = { category = "features", type="longstring", name="Add-ons config JSON", description="Add-ons config JSON file" } - images.logo = { category = "images", type = "file", name = "Hub Logo", description = "Appears throughout your hub including lobby and loading screens." } images.logo_dark = { category = "images", type = "file", name = "Hub logo for dark mode", description = "The hub logo which appears for visitors who have dark mode enabled." } images.favicon = { category = "images", type = "file", name = "Favicon", description = "The favicon is the small picture which appears in the web browser tab." } diff --git a/src/systems/bit-media-frames.js b/src/systems/bit-media-frames.js index d264bc52a9..c5f57b5d18 100644 --- a/src/systems/bit-media-frames.js +++ b/src/systems/bit-media-frames.js @@ -31,12 +31,13 @@ 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; @@ -329,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.updateRigidBody(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.updateRigidBody(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" }); + } } } @@ -381,6 +410,8 @@ export function mediaFramesSystem(world, physicsSystem) { 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/hold-system.js b/src/systems/hold-system.js index 6c2c1abd37..68ff4ae74a 100644 --- a/src/systems/hold-system.js +++ b/src/systems/hold-system.js @@ -20,6 +20,7 @@ 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; @@ -89,6 +90,7 @@ function grab(world, userinput, queryHovered, held, grabPath) { 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 0fbd77a15c..2281f524fb 100644 --- a/src/systems/hubs-systems.ts +++ b/src/systems/hubs-systems.ts @@ -225,11 +225,6 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene physicsCompatSystem(world, hubsSystems.physicsSystem); hubsSystems.physicsSystem.tick(dt); constraintsSystem(world, hubsSystems.physicsSystem); - floatyObjectSystem(world); - - APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); hoverableVisualsSystem(world); @@ -254,6 +249,7 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene hubsSystems.positionAtBorderSystem.tick(); hubsSystems.twoPointStretchingSystem.tick(); interactableSystem(world); + floatyObjectSystem(world); hubsSystems.holdableButtonSystem.tick(); hubsSystems.hoverButtonSystem.tick(); @@ -316,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); @@ -331,32 +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(); - APP.addon_systems.matricesUpdate.forEach((systemConfig: SystemConfigT) => { + renderer.info.reset(); + + APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { systemConfig.system(APP); }); - renderer.info.reset(); if (APP.fx.composer) { - APP.addon_systems.postProcessing.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); APP.fx.composer.render(); } else { - APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); 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 - - APP.addon_systems.tearDown.forEach((systemConfig: SystemConfigT) => { + 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/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/types.ts b/src/types.ts index d5ecb3a251..f2e595eaa5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,11 +10,9 @@ export enum SystemOrderE { Setup = 0, PrePhysics = 100, PostPhysics = 200, - MatricesUpdate = 300, + BeforeMatricesUpdate = 300, BeforeRender = 400, - AfterRender = 500, - PostProcessing = 600, - TearDown = 700 + AfterRender = 500 } export type CoreSystemKeyT = keyof AScene["systems"]; 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 bfc954b308..df6509f9b8 100644 --- a/src/utils/bit-utils.ts +++ b/src/utils/bit-utils.ts @@ -76,6 +76,19 @@ 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; diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 5aff6253f4..497bcfa054 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -56,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, @@ -102,6 +102,7 @@ import { inflateObjectMenuTransform, ObjectMenuTransformParams } from "../inflat 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 => { @@ -301,7 +302,7 @@ export interface JSXComponentData extends ComponentData { offersHandConstraint?: true; singleActionButton?: true; holdableButton?: true; - holdable?: true; + holdable?: HoldableParams; deletable?: true; makeKinematicOnRelease?: true; destroyAtExtremeDistance?: true; @@ -374,6 +375,7 @@ export interface JSXComponentData extends ComponentData { objectMenuTransform?: OptionalParams; objectMenuTarget?: OptionalParams; plane?: PlaneParams; + text?: TextParams; } export interface GLTFComponentData extends ComponentData { @@ -398,6 +400,7 @@ export interface GLTFComponentData extends ComponentData { rigidbody?: OptionalParams; // TODO GLTFPhysicsShapeParams physicsShape?: AmmoShapeParams; + text?: TextParams; grabbable?: GrabbableParams; // deprecated @@ -460,7 +463,7 @@ export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> textButton: createDefaultInflator(TextButton), hoverButton: createDefaultInflator(HoverButton), hoverableVisuals: createDefaultInflator(HoverableVisuals), - holdable: createDefaultInflator(Holdable), + holdable: inflateHoldable, deletable: createDefaultInflator(Deletable), rigidbody: inflateRigidBody, physicsShape: inflatePhysicsShape, @@ -486,6 +489,7 @@ export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> mediaLoader: inflateMediaLoader, mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, + text: inflateText, inspectable: createDefaultInflator(Inspectable), // inflators that create Object3Ds object3D: addObject3DComponent, @@ -534,7 +538,8 @@ export const gltfInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn } audioSettings: inflateAudioSettings, mediaLink: inflateMediaLink, rigidbody: inflateGLTFRigidBody, - physicsShape: inflateAmmoShape + physicsShape: inflateAmmoShape, + text: inflateGLTFText }; function jsxInflatorExists(name: string) { 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 86637b4dbf..87ee038ee8 100644 --- a/src/utils/network-schemas.ts +++ b/src/utils/network-schemas.ts @@ -6,6 +6,7 @@ import { NetworkedMediaFrame, NetworkedPDF, NetworkedRigidBody, + NetworkedText, NetworkedTransform, NetworkedVideo, NetworkedWaypoint @@ -17,6 +18,7 @@ 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 { @@ -48,6 +50,7 @@ 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-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/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/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/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/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: ( From 0956082eeed1b5954763de9b45aecf8b230dc78f Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 8 May 2024 20:36:59 +0200 Subject: [PATCH 17/40] Packages updates --- package-lock.json | 75 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 5 ++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f182e3a194..3204205f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,8 +46,9 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", - "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", + "hubs-behavior-graphs-addon": "github:MozillaReality/hubs-behavior-graphs-addon#583c4d1348bcc267185e49e784d29623cd1c5f3c", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#475a1e34504cbccd8b7125d16a601af76c9d0a4c", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon#828cf9000a49e037ad40f05f1a9a5ff679aed851", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", @@ -3746,6 +3747,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", @@ -9253,6 +9262,11 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, + "node_modules/@types/draco3d": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.9.tgz", + "integrity": "sha512-4MMUjMQb4yA5fJ4osXx+QxGHt0/ZSy4spT6jL1HM7Tn8OJEC35siqdnpOo+HxPhYjqEFumKfGVF9hJfdyKBIBA==" + }, "node_modules/@types/ejs": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.2.tgz", @@ -9478,6 +9492,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", @@ -9600,8 +9619,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", @@ -13401,6 +13419,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", @@ -14789,6 +14812,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", @@ -16462,9 +16490,26 @@ "node": ">= 6" } }, + "node_modules/hubs-behavior-graphs-addon": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/MozillaReality/hubs-behavior-graphs-addon.git#583c4d1348bcc267185e49e784d29623cd1c5f3c", + "integrity": "sha512-TPfYbUqT6oEOfPoKTXZGnpgdYg0wHSO6ao8UwoqoFuaXippfum52QkNxGcx/sImWYvp/w/1T7+UUFmXmaIsvMA==", + "license": "MPL-2.0", + "dependencies": { + "@oveddan-behave-graph/core": "^0.11.1" + }, + "peerDependencies": { + "aframe": "github:mozillareality/aframe#hubs/master", + "bitecs": "github:mozilla/bitECS#hubs-patches", + "react": "^18.2.0", + "three": "github:mozillareality/three.js#65b5105908f5f135cad25fed07e25f15f3876777", + "troika-three-text": "^0.45.0" + } + }, "node_modules/hubs-duck-addon": { "version": "1.0.0", "resolved": "git+ssh://git@github.com/MozillaReality/hubs-duck-addon.git#475a1e34504cbccd8b7125d16a601af76c9d0a4c", + "integrity": "sha512-aknmYw1Xh3E0Dro48SpU3Xi1mgE4nphsSm2wDC+HyVw2vWOJjbMS8nKufrPucPuJahyc+xrmz53YCS8OyWPCRw==", "license": "MPL-2.0", "peerDependencies": { "bitecs": "github:mozilla/bitECS#hubs-patches", @@ -16474,6 +16519,7 @@ "node_modules/hubs-portals-addon": { "version": "1.0.0", "resolved": "git+ssh://git@github.com/MozillaReality/hubs-portals-addon.git#828cf9000a49e037ad40f05f1a9a5ff679aed851", + "integrity": "sha512-pAGBrQdnX6u0Zkf7c+Vrzs7ZspvyGwQRIW1mSiZLqEkDpl4bXRb79DZ4diD/kX/KgmcnBMq6vsLtqAZhJFWNOA==", "license": "MPL-2.0", "peerDependencies": { "bitecs": "github:mozilla/bitECS#hubs-patches", @@ -20346,6 +20392,11 @@ "three": ">= 0.138.0 < 0.153.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", @@ -24001,6 +24052,22 @@ "three": "0.x.x" } }, + "node_modules/three-stdlib": { + "version": "2.29.11", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.29.11.tgz", + "integrity": "sha512-vSyUavKCwOJQd2ch9IHhyJVx6eNG3y+z3/LulqHM7OLMy81OqnmIrvc2b4phKr/c1aVjHRNG2X7JOklfrDDcmQ==", + "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", diff --git a/package.json b/package.json index 1f3a891916..283b9d3260 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,9 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon", - "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon", + "hubs-behavior-graphs-addon": "github:MozillaReality/hubs-behavior-graphs-addon#583c4d1348bcc267185e49e784d29623cd1c5f3c", + "hubs-duck-addon": "github:MozillaReality/hubs-duck-addon#475a1e34504cbccd8b7125d16a601af76c9d0a4c", + "hubs-portals-addon": "github:MozillaReality/hubs-portals-addon#828cf9000a49e037ad40f05f1a9a5ff679aed851", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", From 5920ae48e03f4f0de4827cd478599624d84efdd3 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 9 May 2024 07:38:37 +0200 Subject: [PATCH 18/40] Better handle of bitECS/addons state --- .../room/RoomSettingsSidebar.js | 23 +++++++++++++++---- src/utils/bit-utils.ts | 10 +++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/react-components/room/RoomSettingsSidebar.js b/src/react-components/room/RoomSettingsSidebar.js index 3d84ce08d2..b8a36cef50 100644 --- a/src/react-components/room/RoomSettingsSidebar.js +++ b/src/react-components/room/RoomSettingsSidebar.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } 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"; @@ -61,6 +61,20 @@ export function RoomSettingsSidebar({ [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); + if (!evt.target.checked) { + [...addons.entries()].map(([id, _]) => { + setValue(`user_data.addons.${id}`, evt.target.checked); + }); + } + }, + [setValue, setBitECSLoaderEnabled] + ); + return ( } @@ -216,17 +230,18 @@ export function RoomSettingsSidebar({ defaultMessage="Enable bitECS based Client" /> } + defaultChecked={shouldUseNewLoader()} + onChange={handleBitECSChange} description={ } - {...register("user_data.hubs_use_bitecs_based_client")} /> } fullWidth> - {!shouldUseNewLoader() && ( + {!bitECSLoaderEnabled && (