Skip to content

Commit

Permalink
bitECS object list support
Browse files Browse the repository at this point in the history
This commit adds bitECS object list support.

**Basic Strategy**

Reuse the existing A-Frame based code as much as possible for now.

In the related functions that take A-Frame element, take Object3D
instead. Object3D has a reference to A-Frame element in the A-Frame
based implementation and has a reference to Entity ID in the bitECS.
The functions process with A-Frame element or Entity ID depending on
whether new loader enabled.

Explicitly use shouldUseNewLoader() and/or make two functions, one
for A-Frame based and another one for bitECS based implementation,
if different logics are needed between A-Frame based and bitECS
based implementations. Duplicated codes may not be perfectly
removed but it would be simpler to follow the code rather than
the new loader pretends the old one. And it would be easier to edit
the code when we will get rid of A-Frame.

**Changes**

- Introduce MediaInfo component and save url and content type
  into it when loading media in media-loader. The info is used
  in the object list
- Fire listed_media_changed event when media is loaded via
  media-loader and when the loaded media entity is removed
- Take Object3D instead of A-Frame element in the related functions
- Use shouldUseNewLoader() and/or make two separated functions
  for A-Frame and bitECS where the different logics are needed

**Future TODOs**

- Support avatars
- Support Pinning
  • Loading branch information
keianhzo authored and takahirox committed Sep 8, 2023
1 parent 9b216c8 commit ed7f19c
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 89 deletions.
6 changes: 6 additions & 0 deletions src/bit-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions src/bit-systems/media-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GLTFModel,
LoadedByMediaLoader,
MediaContentBounds,
MediaInfo,
MediaLoaded,
MediaLoader,
Networked,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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));
Expand All @@ -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();
}
4 changes: 2 additions & 2 deletions src/components/media-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());

Expand Down
2 changes: 1 addition & 1 deletion src/components/super-spawner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},

Expand Down
1 change: 1 addition & 0 deletions src/prefabs/media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function MediaPrefab(params: MediaLoaderParams): EntityDef {
collisionMask: COLLISION_LAYERS.HANDS
}}
scale={[1, 1, 1]}
inspectable
/>
);
}
104 changes: 76 additions & 28 deletions src/react-components/room/hooks/useObjectList.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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);
}
}
}

Expand Down
50 changes: 38 additions & 12 deletions src/react-components/room/object-hooks.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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() {
Expand All @@ -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;
Expand Down Expand Up @@ -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")
);

Expand Down
Loading

0 comments on commit ed7f19c

Please sign in to comment.