diff --git a/src/bit-components.js b/src/bit-components.js index 0059457998b..7dfb1d28017 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -156,7 +156,12 @@ export const MediaLoader = defineComponent({ }); MediaLoader.src[$isStringType] = true; MediaLoader.fileId[$isStringType] = true; -export const MediaLoaded = defineComponent(); +export const MediaLoaded = defineComponent({ + contentType: Types.ui32, + src: Types.ui32 +}); +MediaLoaded.contentType[$isStringType] = true; +MediaLoaded.src[$isStringType] = true; export const LoadedByMediaLoader = defineComponent(); export const MediaContentBounds = defineComponent({ bounds: [Types.f32, 3] diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index cdb1ef64c4b..bae9bbca4f8 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -207,6 +207,10 @@ function* loadMedia(world: HubsWorld, eid: EntityID) { } media = yield* loader(world, eid, urlData); addComponent(world, MediaLoaded, media); + const srcSid = APP.getSid(urlData.accessibleUrl); + MediaLoaded.src[media] = srcSid; + const contentTypeSid = APP.getSid(urlData.contentType); + MediaLoaded.contentType[media] = contentTypeSid; } 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(eid => APP.scene?.emit("listed_media_changed")); + mediaLoadedExitQuery(world).forEach(eid => 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..0db625dbab5 100644 --- a/src/react-components/room/hooks/useObjectList.js +++ b/src/react-components/room/hooks/useObjectList.js @@ -2,10 +2,17 @@ import React, { useState, useEffect, useContext, createContext, useCallback, Chi import PropTypes from "prop-types"; import { mediaSort, getMediaType } from "../../../utils/media-sorting.js"; -function getDisplayString(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) || ""; +function getDisplayString(elOrEid) { + let url; + if (!elOrEid.isEntity) { + const srcSid = MediaLoaded.src[elOrEid]; + url = APP.getString(srcSid); + } else { + // Having a listed-media component does not guarantee the existence of a media-loader component, + // so don't crash if there isn't one. + url = (elOrEid.components["media-loader"] && elOrEid.components["media-loader"].data.src) || ""; + } + const split = url.split("/"); const resourceName = split[split.length - 1].split("?")[0]; let httpIndex = -1; @@ -67,6 +74,7 @@ function handleDeselect(scene, object, callback) { } } +const queryListedMedia = defineQuery([MediaLoaded]); export function ObjectListProvider({ scene, children }) { const [objects, setObjects] = useState([]); const [focusedObject, setFocusedObject] = useState(null); // The object currently shown in the viewport @@ -83,7 +91,16 @@ export function ObjectListProvider({ scene, children }) { el })); - setObjects(objects); + const bitObjects = queryListedMedia(APP.world) + .sort(mediaSort) + .map(eid => ({ + id: APP.world.eid2obj.get(eid)?.id, + name: getDisplayString(eid), + type: getMediaType(eid), + el: eid + })); + + setObjects([...objects, ...bitObjects]); } let timeout; @@ -111,7 +128,13 @@ export function ObjectListProvider({ scene, children }) { const inspectedEl = cameraSystem.inspectable && cameraSystem.inspectable.el; if (inspectedEl) { - const object = objects.find(o => o.el === inspectedEl); + const object = objects.find(o => { + if (!o.el.isEntity) { + return o.el === inspectedEl.eid; + } else { + return o.el === inspectedEl; + } + }); if (object) { setSelectedObject(object); diff --git a/src/react-components/room/object-hooks.js b/src/react-components/room/object-hooks.js index 1cc4861c6ae..5742b30db4c 100644 --- a/src/react-components/room/object-hooks.js +++ b/src/react-components/room/object-hooks.js @@ -3,22 +3,33 @@ import { removeNetworkedObject } from "../../utils/removeNetworkedObject"; 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 { MediaLoaded, Pinnable, Pinned, Static } from "../../bit-components"; +import { deleteTheDeletableAncestor } from "../../bit-systems/delete-entity-system"; export function isMe(object) { return object.el.id === "avatar-rig"; } export function isPlayer(object) { - return !!object.el.components["networked-avatar"]; + if (object.el.isEntity) { + return !!object.el.components["networked-avatar"]; + } else { + // TODO Add when networked avatar is migrated + return false; + } } 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 (object.el.isEntity) { + const mediaLoader = object.el.components["media-loader"]; + url = + mediaLoader && ((mediaLoader.data.mediaOptions && mediaLoader.data.mediaOptions.href) || mediaLoader.data.src); + } else { + const urlSid = MediaLoaded.src[object.el]; + url = APP.getString(urlSid); + } if (url && !url.startsWith("hubs://")) { return url; @@ -53,6 +64,9 @@ export function usePinObject(hubChannel, scene, object) { useEffect(() => { const el = object.el; + // TODO Add when pinning is migrated + if (!el.isEntity) return; + function onPinStateChanged() { setIsPinned(getPinnedState(el.eid)); } @@ -69,7 +83,10 @@ export function usePinObject(hubChannel, scene, object) { let userOwnsFile = false; - if (el.components["media-loader"]) { + if (!el.isEntity) { + // TODO Add when pinning is migrated + return false; + } else if (el.components["media-loader"]) { const { fileIsOwned, fileId } = el.components["media-loader"].data; userOwnsFile = fileIsOwned || (fileId && getPromotionTokenForFile(fileId)); } @@ -114,7 +131,11 @@ export function useGoToSelectedObject(scene, object) { export function useRemoveObject(hubChannel, scene, object) { const removeObject = useCallback(() => { - removeNetworkedObject(scene, object.el); + if (object.el.isEntity) { + removeNetworkedObject(scene, object.el); + } else { + deleteTheDeletableAncestor(APP.world, object.el); + } }, [scene, object]); const el = object.el; diff --git a/src/systems/camera-system.js b/src/systems/camera-system.js index ee6cdbeee70..2893d60137e 100644 --- a/src/systems/camera-system.js +++ b/src/systems/camera-system.js @@ -8,17 +8,29 @@ 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 } from "../utils/bit-utils"; + +function getInspectableInHierarchy(elOrEid) { + if (elOrEid.isEntity) { + let inspectable = elOrEid; + while (inspectable) { + if (isTagged(inspectable, "inspectable")) { + return inspectable.object3D; + } + inspectable = inspectable.parentNode; + } -function getInspectableInHierarchy(el) { - let inspectable = el; - while (inspectable) { - if (isTagged(inspectable, "inspectable")) { - return inspectable.object3D; + console.warn("could not find inspectable in hierarchy"); + return elOrEid.object3D; + } else { + let inspectable = findAncestorWithComponent(APP.world, Inspectable, elOrEid); + if (!inspectable) { + console.warn("could not find inspectable in hierarchy"); + inspectable = elOrEid; } - inspectable = inspectable.parentNode; + return APP.world.eid2obj.get(inspectable); } - console.warn("could not find inspectable in hierarchy"); - return el.object3D; } function pivotFor(el) { @@ -36,9 +48,15 @@ function pivotFor(el) { return child.object3D; } -export function getInspectableAndPivot(el) { - const inspectable = getInspectableInHierarchy(el); - const pivot = pivotFor(inspectable.el); +function getInspectableAndPivot(elOrEid) { + const inspectable = getInspectableInHierarchy(elOrEid); + let pivot; + if (elOrEid.isEntity) { + pivot = pivotFor(inspectable.el); + } else { + // TODO Add support for pivotFor (avatars only) + pivot = inspectable; + } return { inspectable, pivot }; } @@ -119,22 +137,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,8 +264,8 @@ export class CameraSystem { this.mode = NEXT_MODES[this.mode] || 0; } - inspect(el, distanceMod, fireChangeEvent = true) { - const { inspectable, pivot } = getInspectableAndPivot(el); + inspect(elOrEid, distanceMod, fireChangeEvent = true) { + const { inspectable, pivot } = getInspectableAndPivot(elOrEid); this.verticalDelta = 0; this.horizontalDelta = 0; @@ -282,7 +294,13 @@ 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 = false; + if (inspectable.el) { + preventAudioBoost = isTagged(inspectable.el, "preventAudioBoost"); + } else { + // TODO Add when avatar is migrated + } + this.snapshot.audio = !preventAudioBoost && getAudio(inspectable); if (this.snapshot.audio) { this.snapshot.audio.updateMatrices(); this.snapshot.audioTransform.copy(this.snapshot.audio.matrixWorld); 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..25093fed883 100644 --- a/src/utils/media-sorting.js +++ b/src/utils/media-sorting.js @@ -4,6 +4,8 @@ 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, MediaPDF, MediaVideo } from "../bit-components"; export const SORT_ORDER_VIDEO = 0; export const SORT_ORDER_AUDIO = 1; @@ -12,19 +14,34 @@ export const SORT_ORDER_PDF = 3; export const SORT_ORDER_MODEL = 4; export const SORT_ORDER_UNIDENTIFIED = 5; -export function mediaSortOrder(el) { - if (el.components["media-video"] && el.components["media-video"].data.contentType.startsWith("audio/")) { - return SORT_ORDER_AUDIO; +export function mediaSortOrder(elOrEid) { + if (!elOrEid.isEntity) { + if (hasComponent(APP.world, MediaVideo)) { + const contentTypeSid = MediaVideo.contentType[elOrEid]; + const contentType = APP.getString(contentTypeSid); + if (contentType.startsWith("audio/")) { + return SORT_ORDER_AUDIO; + } else { + return SORT_ORDER_VIDEO; + } + } + if (hasComponent(APP.world, MediaImage)) return SORT_ORDER_IMAGE; + if (hasComponent(APP.world, MediaPDF)) return SORT_ORDER_PDF; + if (hasComponent(APP.world, GLTFModel)) return SORT_ORDER_MODEL; + } else { + if (elOrEid.components["media-video"] && elOrEid.components["media-video"].data.contentType.startsWith("audio/")) { + return SORT_ORDER_AUDIO; + } + if (elOrEid.components["media-video"]) return SORT_ORDER_VIDEO; + if (elOrEid.components["media-image"]) return SORT_ORDER_IMAGE; + if (elOrEid.components["media-pdf"]) return SORT_ORDER_PDF; + if (elOrEid.components["gltf-model-plus"]) return SORT_ORDER_MODEL; } - if (el.components["media-video"]) return SORT_ORDER_VIDEO; - if (el.components["media-image"]) return SORT_ORDER_IMAGE; - if (el.components["media-pdf"]) return SORT_ORDER_PDF; - if (el.components["gltf-model-plus"]) return SORT_ORDER_MODEL; return SORT_ORDER_UNIDENTIFIED; } -export function mediaSort(el1, el2) { - return mediaSortOrder(el1) - mediaSortOrder(el2); +export function mediaSort(elOrEid1, elOrEid2) { + return mediaSortOrder(elOrEid1) - mediaSortOrder(elOrEid2); } export const DISPLAY_IMAGE = new Map([ @@ -44,7 +61,7 @@ const SORT_ORDER_TO_TYPE = { [SORT_ORDER_MODEL]: "model" }; -export function getMediaType(el) { - const order = mediaSortOrder(el); +export function getMediaType(elOrEid) { + const order = mediaSortOrder(elOrEid); return SORT_ORDER_TO_TYPE[order]; }