diff --git a/src/bit-components.js b/src/bit-components.js index 0059457998b..c94f0533ab7 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -161,6 +161,12 @@ export const LoadedByMediaLoader = defineComponent(); export const MediaContentBounds = defineComponent({ bounds: [Types.f32, 3] }); +export const MediaInfo = defineComponent({ + accessibleUrl: Types.ui32, + contentType: Types.ui32 +}); +MediaInfo.accessibleUrl[$isStringType] = true; +MediaInfo.contentType[$isStringType] = true; export const SceneRoot = defineComponent(); export const NavMesh = defineComponent(); diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index cdb1ef64c4b..2826ce7b3b0 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -5,6 +5,7 @@ import { GLTFModel, LoadedByMediaLoader, MediaContentBounds, + MediaInfo, MediaLoaded, MediaLoader, Networked, @@ -207,6 +208,9 @@ function* loadMedia(world: HubsWorld, eid: EntityID) { } media = yield* loader(world, eid, urlData); addComponent(world, MediaLoaded, media); + addComponent(world, MediaInfo, media); + MediaInfo.accessibleUrl[media] = APP.getSid(urlData.accessibleUrl); + MediaInfo.contentType[media] = APP.getSid(urlData.contentType); } catch (e) { console.error(e); media = renderAsEntity(world, ErrorObject()); @@ -252,6 +256,9 @@ const jobs = new JobRunner(); const mediaLoaderQuery = defineQuery([MediaLoader]); const mediaLoaderEnterQuery = enterQuery(mediaLoaderQuery); const mediaLoaderExitQuery = exitQuery(mediaLoaderQuery); +const mediaLoadedQuery = defineQuery([MediaLoaded]); +const mediaLoadedEnterQuery = enterQuery(mediaLoadedQuery); +const mediaLoadedExitQuery = exitQuery(mediaLoadedQuery); export function mediaLoadingSystem(world: HubsWorld) { mediaLoaderEnterQuery(world).forEach(function (eid) { jobs.add(eid, clearRollbacks => loadAndAnimateMedia(world, eid, clearRollbacks)); @@ -261,5 +268,8 @@ export function mediaLoadingSystem(world: HubsWorld) { jobs.stop(eid); }); + mediaLoadedEnterQuery(world).forEach(() => APP.scene?.emit("listed_media_changed")); + mediaLoadedExitQuery(world).forEach(() => APP.scene?.emit("listed_media_changed")); + jobs.tick(); } diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 0c631213852..e76eb883549 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -111,7 +111,7 @@ AFRAME.registerComponent("media-loader", { setMatrixWorld(mesh, originalMeshMatrix); } else { // Move the mesh such that the center of its bounding box is in the same position as the parent matrix position - const box = getBox(this.el, mesh); + const box = getBox(this.el.object3D, mesh); const scaleCoefficient = fitToBox ? getScaleCoefficient(0.5, box) : 1; const { min, max } = box; center.addVectors(min, max).multiplyScalar(0.5 * scaleCoefficient); @@ -283,7 +283,7 @@ AFRAME.registerComponent("media-loader", { } // TODO this does duplicate work in some cases, but finish() is the only consistent place to do it - const contentBounds = getBox(this.el, this.el.getObject3D("mesh")).getSize(new THREE.Vector3()); + const contentBounds = getBox(this.el.object3D, this.el.getObject3D("mesh")).getSize(new THREE.Vector3()); addComponent(APP.world, MediaContentBounds, el.eid); MediaContentBounds.bounds[el.eid].set(contentBounds.toArray()); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 2c17fb5cef6..7843bc0e808 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -111,7 +111,7 @@ AFRAME.registerComponent("super-spawner", { ? 1 : 0.5; - const scaleCoefficient = getScaleCoefficient(boxSize, getBox(spawnedEntity, spawnedEntity.object3D)); + const scaleCoefficient = getScaleCoefficient(boxSize, getBox(spawnedEntity.object3D, spawnedEntity.object3D)); this.spawnedMediaScale.divideScalar(scaleCoefficient); }, diff --git a/src/prefabs/media.tsx b/src/prefabs/media.tsx index 6718c5a2390..cacb88ef5c4 100644 --- a/src/prefabs/media.tsx +++ b/src/prefabs/media.tsx @@ -28,6 +28,7 @@ export function MediaPrefab(params: MediaLoaderParams): EntityDef { collisionMask: COLLISION_LAYERS.HANDS }} scale={[1, 1, 1]} + inspectable /> ); } diff --git a/src/react-components/room/hooks/useObjectList.js b/src/react-components/room/hooks/useObjectList.js index 26367bdb859..79e9f7c77fb 100644 --- a/src/react-components/room/hooks/useObjectList.js +++ b/src/react-components/room/hooks/useObjectList.js @@ -1,11 +1,21 @@ import React, { useState, useEffect, useContext, createContext, useCallback, Children, cloneElement } from "react"; import PropTypes from "prop-types"; -import { mediaSort, getMediaType } from "../../../utils/media-sorting.js"; +import { mediaSort, mediaSortAframe, getMediaType, getMediaTypeAframe } from "../../../utils/media-sorting.js"; +import { shouldUseNewLoader } from "../../../utils/bit-utils"; +import { defineQuery, hasComponent } from "bitecs"; +import { MediaInfo } from "../../../bit-components.js"; -function getDisplayString(el) { +function getUrl(eid) { + return hasComponent(APP.world, MediaInfo, eid) ? APP.getString(MediaInfo.accessibleUrl[eid]) : ""; +} + +function getUrlAframe(el) { // Having a listed-media component does not guarantee the existence of a media-loader component, // so don't crash if there isn't one. - const url = (el.components["media-loader"] && el.components["media-loader"].data.src) || ""; + return (el.components["media-loader"] && el.components["media-loader"].data.src) || ""; +} + +function getDisplayString(url) { const split = url.split("/"); const resourceName = split[split.length - 1].split("?")[0]; let httpIndex = -1; @@ -46,12 +56,14 @@ function handleInspect(scene, object, callback) { callback(object); - if (object.el.object3D !== cameraSystem.inspectable) { + const object3D = shouldUseNewLoader() ? APP.world.eid2obj.get(object.eid) : object.el.object3D; + + if (object3D !== cameraSystem.inspectable) { if (cameraSystem.inspectable) { cameraSystem.uninspect(false); } - cameraSystem.inspect(object.el, 1.5, false); + cameraSystem.inspect(object3D, 1.5, false); } } @@ -63,10 +75,12 @@ function handleDeselect(scene, object, callback) { cameraSystem.uninspect(false); if (object) { - cameraSystem.inspect(object.el, 1.5, false); + const object3D = shouldUseNewLoader() ? APP.world.eid2obj.get(object.eid) : object.el.object3D; + cameraSystem.inspect(object3D, 1.5, false); } } +const queryListedMedia = defineQuery([MediaInfo]); export function ObjectListProvider({ scene, children }) { const [objects, setObjects] = useState([]); const [focusedObject, setFocusedObject] = useState(null); // The object currently shown in the viewport @@ -76,14 +90,26 @@ export function ObjectListProvider({ scene, children }) { useEffect(() => { function updateMediaEntities() { - const objects = scene.systems["listed-media"].els.sort(mediaSort).map(el => ({ - id: el.object3D.id, - name: getDisplayString(el), - type: getMediaType(el), - el - })); - - setObjects(objects); + if (shouldUseNewLoader()) { + const objects = queryListedMedia(APP.world) + .sort(mediaSort) + .map(eid => ({ + id: APP.world.eid2obj.get(eid)?.id, + name: getDisplayString(getUrl(eid)), + type: getMediaType(eid), + eid: eid + })); + setObjects(objects); + } else { + const objects = scene.systems["listed-media"].els.sort(mediaSortAframe).map(el => ({ + id: el.object3D.id, + name: getDisplayString(getUrlAframe(el)), + type: getMediaTypeAframe(el), + eid: el.eid, + el + })); + setObjects(objects); + } } let timeout; @@ -108,23 +134,45 @@ export function ObjectListProvider({ scene, children }) { function onInspectTargetChanged() { const cameraSystem = scene.systems["hubs-systems"].cameraSystem; - const inspectedEl = cameraSystem.inspectable && cameraSystem.inspectable.el; - - if (inspectedEl) { - const object = objects.find(o => o.el === inspectedEl); - - if (object) { - setSelectedObject(object); + if (shouldUseNewLoader()) { + const inspectedEid = cameraSystem.inspectable && cameraSystem.inspectable.eid; + + if (inspectedEid) { + const object = objects.find(o => o.eid === inspectedEid); + + if (object) { + setSelectedObject(object); + } else { + setSelectedObject({ + id: APP.world.eid2obj.get(inspectedEid)?.id, + name: getDisplayString(getUrl(inspectedEid)), + type: getMediaType(inspectedEid), + eid: inspectedEid + }); + } } else { - setSelectedObject({ - id: inspectedEl.object3D.id, - name: getDisplayString(inspectedEl), - type: getMediaType(inspectedEl), - el: inspectedEl - }); + setSelectedObject(null); } } else { - setSelectedObject(null); + const inspectedEl = cameraSystem.inspectable && cameraSystem.inspectable.el; + + if (inspectedEl) { + const object = objects.find(o => o.el === inspectedEl); + + if (object) { + setSelectedObject(object); + } else { + setSelectedObject({ + id: inspectedEl.object3D.id, + name: getDisplayString(getUrlAframe(inspectedEl)), + type: getMediaTypeAframe(inspectedEl), + eid: inspectedEl.eid, + el: inspectedEl + }); + } + } else { + setSelectedObject(null); + } } } diff --git a/src/react-components/room/object-hooks.js b/src/react-components/room/object-hooks.js index 1cc4861c6ae..50553a5966c 100644 --- a/src/react-components/room/object-hooks.js +++ b/src/react-components/room/object-hooks.js @@ -1,24 +1,36 @@ import { useEffect, useState, useCallback, useMemo } from "react"; import { removeNetworkedObject } from "../../utils/removeNetworkedObject"; +import { shouldUseNewLoader } from "../../utils/bit-utils"; import { rotateInPlaceAroundWorldUp, affixToWorldUp } from "../../utils/three-utils"; import { getPromotionTokenForFile } from "../../utils/media-utils"; import { hasComponent } from "bitecs"; -import { Static } from "../../bit-components"; import { isPinned as getPinnedState } from "../../bit-systems/networking"; +import { MediaInfo, Static } from "../../bit-components"; +import { deleteTheDeletableAncestor } from "../../bit-systems/delete-entity-system"; export function isMe(object) { - return object.el.id === "avatar-rig"; + return object.id === "avatar-rig"; } export function isPlayer(object) { - return !!object.el.components["networked-avatar"]; + if (shouldUseNewLoader()) { + // TODO Add when networked avatar is migrated + return false; + } else { + return !!object.el.components["networked-avatar"]; + } } export function getObjectUrl(object) { - const mediaLoader = object.el.components["media-loader"]; - - const url = - mediaLoader && ((mediaLoader.data.mediaOptions && mediaLoader.data.mediaOptions.href) || mediaLoader.data.src); + let url; + if (shouldUseNewLoader()) { + const urlSid = MediaInfo.accessibleUrl[object.eid]; + url = APP.getString(urlSid); + } else { + const mediaLoader = object.el.components["media-loader"]; + url = + mediaLoader && ((mediaLoader.data.mediaOptions && mediaLoader.data.mediaOptions.href) || mediaLoader.data.src); + } if (url && !url.startsWith("hubs://")) { return url; @@ -28,7 +40,7 @@ export function getObjectUrl(object) { } export function usePinObject(hubChannel, scene, object) { - const [isPinned, setIsPinned] = useState(getPinnedState(object.el.eid)); + const [isPinned, setIsPinned] = useState(getPinnedState(object.eid)); const pinObject = useCallback(() => { const el = object.el; @@ -51,6 +63,11 @@ export function usePinObject(hubChannel, scene, object) { }, [isPinned, pinObject, unpinObject]); useEffect(() => { + // TODO Add when pinning is migrated + if (shouldUseNewLoader()) { + return; + } + const el = object.el; function onPinStateChanged() { @@ -65,6 +82,11 @@ export function usePinObject(hubChannel, scene, object) { }; }, [object]); + if (shouldUseNewLoader()) { + // TODO Add when pinning is migrated + return false; + } + const el = object.el; let userOwnsFile = false; @@ -114,16 +136,20 @@ export function useGoToSelectedObject(scene, object) { export function useRemoveObject(hubChannel, scene, object) { const removeObject = useCallback(() => { - removeNetworkedObject(scene, object.el); + if (shouldUseNewLoader()) { + deleteTheDeletableAncestor(APP.world, object.eid); + } else { + removeNetworkedObject(scene, object.el); + } }, [scene, object]); - const el = object.el; + const eid = object.eid; const canRemoveObject = !!( scene.is("entered") && !isPlayer(object) && - !getPinnedState(el.eid) && - !hasComponent(APP.world, Static, el.eid) && + !getPinnedState(eid) && + !hasComponent(APP.world, Static, eid) && hubChannel.can("spawn_and_move_media") ); diff --git a/src/systems/camera-system.js b/src/systems/camera-system.js index ee6cdbeee70..988e7132b3e 100644 --- a/src/systems/camera-system.js +++ b/src/systems/camera-system.js @@ -8,8 +8,19 @@ import { qsGet } from "../utils/qs_truthy"; const customFOV = qsGet("fov"); const enableThirdPersonMode = qsTruthy("thirdPerson"); import { Layers } from "../camera-layers"; +import { Inspectable } from "../bit-components"; +import { findAncestorWithComponent, shouldUseNewLoader } from "../utils/bit-utils"; + +function getInspectableInHierarchy(eid) { + let inspectable = findAncestorWithComponent(APP.world, Inspectable, eid); + if (!inspectable) { + console.warn("could not find inspectable in hierarchy"); + inspectable = eid; + } + return APP.world.eid2obj.get(inspectable); +} -function getInspectableInHierarchy(el) { +function getInspectableInHierarchyAframe(el) { let inspectable = el; while (inspectable) { if (isTagged(inspectable, "inspectable")) { @@ -36,8 +47,14 @@ function pivotFor(el) { return child.object3D; } -export function getInspectableAndPivot(el) { - const inspectable = getInspectableInHierarchy(el); +function getInspectableAndPivot(eid) { + const inspectable = getInspectableInHierarchy(eid); + // TODO Add support for pivotFor (avatars only) + return { inspectable, pivot: inspectable }; +} + +function getInspectableAndPivotAframe(el) { + const inspectable = getInspectableInHierarchyAframe(el); const pivot = pivotFor(inspectable.el); return { inspectable, pivot }; } @@ -119,22 +136,16 @@ const moveRigSoCameraLooksAtPivot = (function () { decompose(camera.matrixWorld, cwp, cwq); rig.getWorldQuaternion(cwq); - const box = getBox(inspectable.el, inspectable.el.getObject3D("mesh") || inspectable, true); + const box = getBox(inspectable, inspectable, true); if (box.min.x === Infinity) { // fix edgecase where inspectable object has no mesh / dimensions box.min.subVectors(owp, defaultBoxMax); box.max.addVectors(owp, defaultBoxMax); } box.getCenter(center); - const vrMode = inspectable.el.sceneEl.is("vr-mode"); + const vrMode = APP.scene.is("vr-mode"); const dist = - calculateViewingDistance( - inspectable.el.sceneEl.camera.fov, - inspectable.el.sceneEl.camera.aspect, - box, - center, - vrMode - ) * distanceMod; + calculateViewingDistance(APP.scene.camera.fov, APP.scene.camera.aspect, box, center, vrMode) * distanceMod; target.position.addVectors( owp, oForw @@ -252,15 +263,17 @@ export class CameraSystem { this.mode = NEXT_MODES[this.mode] || 0; } - inspect(el, distanceMod, fireChangeEvent = true) { - const { inspectable, pivot } = getInspectableAndPivot(el); - + inspect(obj, distanceMod, fireChangeEvent = true) { this.verticalDelta = 0; this.horizontalDelta = 0; this.inspectZoom = 0; + if (this.mode === CAMERA_MODE_INSPECT) { return; } + + const { inspectable, pivot } = shouldUseNewLoader() ? getInspectableAndPivot(obj.eid) : getInspectableAndPivotAframe(obj.el); + const scene = AFRAME.scenes[0]; scene.object3D.traverse(ensureLightsAreSeenByCamera); scene.classList.add("hand-cursor"); @@ -282,7 +295,16 @@ export class CameraSystem { this.viewingCamera.updateMatrices(); this.snapshot.matrixWorld.copy(this.viewingRig.object3D.matrixWorld); - this.snapshot.audio = !(inspectable.el && isTagged(inspectable.el, "preventAudioBoost")) && getAudio(inspectable); + let preventAudioBoost; + + if (shouldUseNewLoader()) { + // TODO Add when avatar is migrated + preventAudioBoost = false; + } else { + preventAudioBoost = inspectable.el && isTagged(inspectable.el, "preventAudioBoost"); + } + + this.snapshot.audio = !preventAudioBoost && getAudio(inspectable); if (this.snapshot.audio) { this.snapshot.audio.updateMatrices(); this.snapshot.audioTransform.copy(this.snapshot.audio.matrixWorld); @@ -427,7 +449,7 @@ export class CameraSystem { const hoverEl = this.interaction.state.rightRemote.hovered || this.interaction.state.leftRemote.hovered; if (hoverEl) { - this.inspect(hoverEl, 1.5); + this.inspect(hoverEl.object3D, 1.5); } } else if (this.mode === CAMERA_MODE_INSPECT && this.userinput.get(paths.actions.stopInspecting)) { scene.emit("uninspect"); diff --git a/src/utils/auto-box-collider.js b/src/utils/auto-box-collider.js index 689c598c682..c6f5e852a2c 100644 --- a/src/utils/auto-box-collider.js +++ b/src/utils/auto-box-collider.js @@ -90,13 +90,13 @@ export const computeObjectAABB = (function () { })(); const rotation = new THREE.Euler(); -export function getBox(entity, boxRoot, worldSpace) { +export function getBox(obj, boxRoot, worldSpace) { const box = new THREE.Box3(); - rotation.copy(entity.object3D.rotation); - entity.object3D.rotation.set(0, 0, 0); + rotation.copy(obj.rotation); + obj.rotation.set(0, 0, 0); - entity.object3D.updateMatrices(true, true); + obj.updateMatrices(true, true); boxRoot.updateMatrices(true, true); boxRoot.updateMatrixWorld(true); @@ -104,11 +104,11 @@ export function getBox(entity, boxRoot, worldSpace) { if (!box.isEmpty()) { if (!worldSpace) { - entity.object3D.worldToLocal(box.min); - entity.object3D.worldToLocal(box.max); + obj.worldToLocal(box.min); + obj.worldToLocal(box.max); } - entity.object3D.rotation.copy(rotation); - entity.object3D.matrixNeedsUpdate = true; + obj.rotation.copy(rotation); + obj.matrixNeedsUpdate = true; } boxRoot.matrixWorldNeedsUpdate = true; diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 524a95fca85..f28210286d1 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -38,8 +38,8 @@ import { MaterialTag, VideoTextureSource, Quack, - Mirror, - MixerAnimatableInitialize + MixerAnimatableInitialize, + Inspectable } from "../bit-components"; import { inflateMediaLoader } from "../inflators/media-loader"; import { inflateMediaFrame } from "../inflators/media-frame"; @@ -359,6 +359,7 @@ export interface JSXComponentData extends ComponentData { waypointPreview?: boolean; pdf?: PDFParams; loopAnimation?: LoopAnimationParams; + inspectable?: boolean; } export interface GLTFComponentData extends ComponentData { @@ -464,6 +465,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { quack: createDefaultInflator(Quack), mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, + inspectable: createDefaultInflator(Inspectable), // inflators that create Object3Ds object3D: addObject3DComponent, slice9: inflateSlice9, diff --git a/src/utils/media-sorting.js b/src/utils/media-sorting.js index 7cf1d440153..f3b237fb996 100644 --- a/src/utils/media-sorting.js +++ b/src/utils/media-sorting.js @@ -1,9 +1,5 @@ -import { faVideo } from "@fortawesome/free-solid-svg-icons/faVideo"; -import { faMusic } from "@fortawesome/free-solid-svg-icons/faMusic"; -import { faImage } from "@fortawesome/free-solid-svg-icons/faImage"; -import { faNewspaper } from "@fortawesome/free-solid-svg-icons/faNewspaper"; -import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; -import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; +import { hasComponent } from "bitecs"; +import { GLTFModel, MediaImage, MediaInfo, MediaPDF, MediaVideo } from "../bit-components"; export const SORT_ORDER_VIDEO = 0; export const SORT_ORDER_AUDIO = 1; @@ -12,7 +8,24 @@ export const SORT_ORDER_PDF = 3; export const SORT_ORDER_MODEL = 4; export const SORT_ORDER_UNIDENTIFIED = 5; -export function mediaSortOrder(el) { +function mediaSortOrder(eid) { + if (hasComponent(APP.world, MediaVideo, eid)) { + if (hasComponent(APP.world, MediaInfo, eid)) { + const contentTypeSid = MediaInfo.contentType[eid]; + const contentType = APP.getString(contentTypeSid); + if (contentType.startsWith("audio/")) { + return SORT_ORDER_AUDIO; + } + } + return SORT_ORDER_VIDEO; + } + if (hasComponent(APP.world, MediaImage, eid)) return SORT_ORDER_IMAGE; + if (hasComponent(APP.world, MediaPDF, eid)) return SORT_ORDER_PDF; + if (hasComponent(APP.world, GLTFModel, eid)) return SORT_ORDER_MODEL; + return SORT_ORDER_UNIDENTIFIED; +} + +function mediaSortOrderAframe(el) { if (el.components["media-video"] && el.components["media-video"].data.contentType.startsWith("audio/")) { return SORT_ORDER_AUDIO; } @@ -23,18 +36,13 @@ export function mediaSortOrder(el) { return SORT_ORDER_UNIDENTIFIED; } -export function mediaSort(el1, el2) { - return mediaSortOrder(el1) - mediaSortOrder(el2); +export function mediaSort(eid1, eid2) { + return mediaSortOrder(eid1) - mediaSortOrder(eid2); } -export const DISPLAY_IMAGE = new Map([ - [SORT_ORDER_VIDEO, faVideo], - [SORT_ORDER_AUDIO, faMusic], - [SORT_ORDER_IMAGE, faImage], - [SORT_ORDER_PDF, faNewspaper], - [SORT_ORDER_UNIDENTIFIED, faQuestion], - [SORT_ORDER_MODEL, faCube] -]); +export function mediaSortAframe(el1, el2) { + return mediaSortOrderAframe(el1) - mediaSortOrderAframe(el2); +} const SORT_ORDER_TO_TYPE = { [SORT_ORDER_VIDEO]: "video", @@ -44,7 +52,12 @@ const SORT_ORDER_TO_TYPE = { [SORT_ORDER_MODEL]: "model" }; -export function getMediaType(el) { - const order = mediaSortOrder(el); +export function getMediaType(eid) { + const order = mediaSortOrder(eid); + return SORT_ORDER_TO_TYPE[order]; +} + +export function getMediaTypeAframe(el) { + const order = mediaSortOrderAframe(el); return SORT_ORDER_TO_TYPE[order]; }