From 22408979465ba6f6537e08aa229b38b7f8423de2 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 8 May 2024 17:45:27 +0200 Subject: [PATCH] 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: (