From 7299358db94def3fce4e81f3a5db174f7155f258 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Fri, 19 Jan 2024 18:25:13 +0100 Subject: [PATCH] BitECS inspect button --- src/bit-components.js | 5 ++- src/bit-systems/inspect-system.ts | 18 +++++++++ src/bit-systems/object-menu.ts | 35 +++++++++++++---- src/prefabs/object-menu.tsx | 12 ++++-- .../room/hooks/useObjectList.js | 38 +++++++++++++------ src/systems/hubs-systems.ts | 2 + 6 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 src/bit-systems/inspect-system.ts diff --git a/src/bit-components.js b/src/bit-components.js index 45aa701cee..dc00b087ec 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -100,7 +100,10 @@ export const PenActive = defineComponent(); export const PenUpdated = defineComponent(); export const HoverMenuChild = defineComponent(); export const Static = defineComponent(); -export const Inspectable = defineComponent(); +export const Inspectable = defineComponent({ + fireChangeEvent: Types.ui8 +}); +export const Inspected = defineComponent(); export const PreventAudioBoost = defineComponent(); export const IgnoreSpaceBubble = defineComponent(); export const Rigidbody = defineComponent({ diff --git a/src/bit-systems/inspect-system.ts b/src/bit-systems/inspect-system.ts new file mode 100644 index 0000000000..3cc56dfd53 --- /dev/null +++ b/src/bit-systems/inspect-system.ts @@ -0,0 +1,18 @@ +import { defineQuery, enterQuery, exitQuery } from "bitecs"; +import { HubsWorld } from "../app"; +import { Inspectable, Inspected } from "../bit-components"; +import { CameraSystem } from "../systems/camera-system"; + +const inspectedQuery = defineQuery([Inspected]); +const inspectedEnterQuery = enterQuery(inspectedQuery); +const inspectedExitQuery = exitQuery(inspectedQuery); +export function inspectSystem(world: HubsWorld, cameraSystem: CameraSystem) { + inspectedEnterQuery(world).forEach(eid => { + const obj = world.eid2obj.get(eid); + cameraSystem.inspect(obj, 1.5, Boolean(Inspectable.fireChangeEvent[eid])); + }); + inspectedExitQuery(world).forEach(eid => { + const obj = world.eid2obj.get(eid); + cameraSystem.uninspect(Boolean(Inspectable.fireChangeEvent[eid])); + }); +} diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts index b0bb1c0dde..fc6ab27b2b 100644 --- a/src/bit-systems/object-menu.ts +++ b/src/bit-systems/object-menu.ts @@ -1,4 +1,4 @@ -import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent } from "bitecs"; +import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs"; import { Matrix4, Vector3 } from "three"; import type { HubsWorld } from "../app"; import { @@ -13,7 +13,6 @@ import { RemoteRight, Rigidbody, Deleting, - Deletable, MediaLoader, ObjectDropped, FloatyObject, @@ -21,7 +20,9 @@ import { MediaVideo, MediaImage, MediaPDF, - MediaMirrored + MediaMirrored, + Inspected, + Inspectable } from "../bit-components"; import { anyEntityWith, @@ -58,6 +59,11 @@ function clicked(world: HubsWorld, eid: EntityID) { } function objectMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boolean) { + const held = heldQuery(world).map(eid => eid === ObjectMenu.inspectButtonRef[menu])[0]; + if (held) { + return ObjectMenu.targetRef[menu]; + } + if (!sceneIsFrozen) { return 0; } @@ -67,7 +73,7 @@ function objectMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boole // We should probably use something more meaningful to refer to that spawned media root than Deletable. // Maybe something like MediaRoot or SpawnedMediaRoot. const target = hoveredQuery(world).map(eid => - findAncestorWithComponents(world, [Deletable, ObjectMenuTarget], eid) + findAncestorWithComponents(world, [Inspectable, ObjectMenuTarget], eid) )[0]; if (target) { if (hasComponent(world, Deleting, target)) { @@ -81,7 +87,6 @@ function objectMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boole if (entityExists(world, ObjectMenu.targetRef[menu])) { return ObjectMenu.targetRef[menu]; } - return 0; } @@ -192,8 +197,6 @@ function handleClicks(world: HubsWorld, menu: EntityID, hubChannel: HubChannel) deleteTheDeletableAncestor(world, ObjectMenu.targetRef[menu]); } else if (clicked(world, ObjectMenu.dropButtonRef[menu])) { addComponent(world, ObjectDropped, ObjectMenu.targetRef[menu]); - } else if (clicked(world, ObjectMenu.inspectButtonRef[menu])) { - console.log("Clicked inspect"); } else if (clicked(world, ObjectMenu.deserializeDrawingButtonRef[menu])) { console.log("Clicked deserialize drawing"); } else if (clicked(world, ObjectMenu.openLinkButtonRef[menu])) { @@ -217,6 +220,13 @@ function handleHeldEnter(world: HubsWorld, eid: EntityID, menuEid: EntityID) { ObjectMenu.flags[menuEid] &= ~ObjectMenuFlags.Visible; startScaling(world, menuEid, ObjectMenu.targetRef[menuEid]); break; + case ObjectMenu.inspectButtonRef[menuEid]: + if (!hasComponent(world, Inspected, ObjectMenu.targetRef[menuEid])) { + ObjectMenu.flags[menuEid] &= ~ObjectMenuFlags.Visible; + addComponent(world, Inspected, ObjectMenu.targetRef[menuEid]); + Inspectable.fireChangeEvent[ObjectMenu.targetRef[menuEid]] = 1; + } + break; } } @@ -230,6 +240,13 @@ function handleHeldExit(world: HubsWorld, eid: EntityID, menuEid: EntityID) { ObjectMenu.flags[menuEid] |= ObjectMenuFlags.Visible; stopScaling(world, menuEid); break; + case ObjectMenu.inspectButtonRef[menuEid]: + if (hasComponent(world, Inspected, ObjectMenu.targetRef[menuEid])) { + ObjectMenu.flags[menuEid] |= ObjectMenuFlags.Visible; + removeComponent(world, Inspected, ObjectMenu.targetRef[menuEid]); + Inspectable.fireChangeEvent[ObjectMenu.targetRef[menuEid]] = 1; + } + break; } } @@ -263,6 +280,8 @@ function updateVisibility(world: HubsWorld, menu: EntityID, frozen: boolean) { const media = MediaLoader.mediaRef[target]; const isVideoImagePdf = hasAnyComponent(world, [MediaVideo, MediaImage, MediaPDF], media); const isMirrored = hasComponent(world, MediaMirrored, target); + const isInspectable = hasComponent(world, Inspectable, target); + const isInspected = hasComponent(world, Inspected, target); // Parent visibility doesn't block raycasting, so we must set each button to be invisible // TODO: Ensure that children of invisible entities aren't raycastable @@ -278,6 +297,7 @@ function updateVisibility(world: HubsWorld, menu: EntityID, frozen: boolean) { world.eid2obj.get(ObjectMenu.dropButtonRef[menu])!.visible = !isVideoImagePdf && !isEntityPinned && !hasComponent(world, ObjectDropped, target); world.eid2obj.get(ObjectMenu.mirrorButtonRef[menu])!.visible = isVideoImagePdf && !isMirrored; + world.eid2obj.get(ObjectMenu.inspectButtonRef[menu])!.visible = isVideoImagePdf && isInspectable && !isInspected; // This is a hacky way of giving a chance to the object-menu-transform system to center the menu based on the // visible buttons without accounting for the background plane. @@ -289,7 +309,6 @@ function updateVisibility(world: HubsWorld, menu: EntityID, frozen: boolean) { world.eid2obj.get(ObjectMenu.cameraFocusButtonRef[menu])!.visible = false; world.eid2obj.get(ObjectMenu.cameraTrackButtonRef[menu])!.visible = false; world.eid2obj.get(ObjectMenu.deserializeDrawingButtonRef[menu])!.visible = false; - world.eid2obj.get(ObjectMenu.inspectButtonRef[menu])!.visible = false; world.eid2obj.get(ObjectMenu.refreshButtonRef[menu])!.visible = false; } diff --git a/src/prefabs/object-menu.tsx b/src/prefabs/object-menu.tsx index 4072043bfb..1004e02715 100644 --- a/src/prefabs/object-menu.tsx +++ b/src/prefabs/object-menu.tsx @@ -7,6 +7,7 @@ import scaleIconSrc from "../assets/scale-action.png"; import removeIconSrc from "../assets/remove-action.png"; import dropIconSrc from "../assets/drop-action.png"; import mirrorIconSrc from "../assets/mirror-action.png"; +import inspectIconSrc from "../assets/focus-action.png"; import { Plane } from "./plane"; import { FrontSide } from "three"; import { Layers } from "../camera-layers"; @@ -17,7 +18,8 @@ export async function loadObjectMenuButtonIcons() { loadTexture(scaleIconSrc, 1, "image/png"), loadTexture(removeIconSrc, 1, "image/png"), loadTexture(dropIconSrc, 1, "image/png"), - loadTexture(mirrorIconSrc, 1, "image/png") + loadTexture(mirrorIconSrc, 1, "image/png"), + loadTexture(inspectIconSrc, 1, "image/png") ]); } @@ -112,14 +114,16 @@ function DropButton(props: Attrs) { } function InspectButton(props: Attrs) { + const { texture, cacheKey } = loadTextureFromCache(inspectIconSrc, 1); return ( ); @@ -237,7 +241,7 @@ const position = { track: [ 0.25, 0.375, uiZ] as ArrayVec3, remove: [ 0, -0.275, uiZ] as ArrayVec3, drop: [ 0, -0.425, uiZ] as ArrayVec3, - inspect: [ -0.1, -0.625, uiZ] as ArrayVec3, + inspect: [ 0, -0.425, uiZ] as ArrayVec3, deserializeDrawing: [ -0.3, -0.625, uiZ] as ArrayVec3, openLink: [ 0.25, -0.275, uiZ] as ArrayVec3, refresh: [ 0.3, -0.625, uiZ] as ArrayVec3, diff --git a/src/react-components/room/hooks/useObjectList.js b/src/react-components/room/hooks/useObjectList.js index 79e9f7c77f..1a688774df 100644 --- a/src/react-components/room/hooks/useObjectList.js +++ b/src/react-components/room/hooks/useObjectList.js @@ -2,8 +2,8 @@ import React, { useState, useEffect, useContext, createContext, useCallback, Chi import PropTypes from "prop-types"; 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"; +import { addComponent, defineQuery, hasComponent, removeComponent } from "bitecs"; +import { Inspectable, Inspected, MediaInfo } from "../../../bit-components.js"; function getUrl(eid) { return hasComponent(APP.world, MediaInfo, eid) ? APP.getString(MediaInfo.accessibleUrl[eid]) : ""; @@ -56,14 +56,21 @@ function handleInspect(scene, object, callback) { callback(object); - const object3D = shouldUseNewLoader() ? APP.world.eid2obj.get(object.eid) : object.el.object3D; - - if (object3D !== cameraSystem.inspectable) { - if (cameraSystem.inspectable) { - cameraSystem.uninspect(false); + if (shouldUseNewLoader()) { + if (hasComponent(APP.world, Inspected, object.eid)) { + removeComponent(APP.world, Inspected, object.eid); + } else { + addComponent(APP.world, Inspected, object.eid); + Inspectable.fireChangeEvent[object.eid] = 0; } + } else { + if (object.el.object3D !== cameraSystem.inspectable) { + if (cameraSystem.inspectable) { + cameraSystem.uninspect(false); + } - cameraSystem.inspect(object3D, 1.5, false); + cameraSystem.inspect(object.el.object3D, 1.5, false); + } } } @@ -72,11 +79,18 @@ function handleDeselect(scene, object, callback) { callback(null); - cameraSystem.uninspect(false); + if (shouldUseNewLoader()) { + if (object) { + removeComponent(APP.world, Inspected, object.eid); + Inspectable.fireChangeEvent[object.eid] = 0; + } + } else { + cameraSystem.uninspect(false); - if (object) { - const object3D = shouldUseNewLoader() ? APP.world.eid2obj.get(object.eid) : object.el.object3D; - cameraSystem.inspect(object3D, 1.5, false); + if (object) { + const object3D = shouldUseNewLoader() ? APP.world.eid2obj.get(object.eid) : object.el.object3D; + cameraSystem.inspect(object3D, 1.5, false); + } } } diff --git a/src/systems/hubs-systems.ts b/src/systems/hubs-systems.ts index 47ec9c2d00..9ed0b166a1 100644 --- a/src/systems/hubs-systems.ts +++ b/src/systems/hubs-systems.ts @@ -90,6 +90,7 @@ import { followInFovSystem } from "../bit-systems/follow-in-fov-system"; import { linkedMediaSystem } from "../bit-systems/linked-media-system"; import { linkedVideoSystem } from "../bit-systems/linked-video-system"; import { linkedPDFSystem } from "../bit-systems/linked-pdf-system"; +import { inspectSystem } from "../bit-systems/inspect-system"; declare global { interface Window { @@ -283,6 +284,7 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene linkedMediaSystem(world); linkedVideoSystem(world); linkedPDFSystem(world); + inspectSystem(world, hubsSystems.cameraSystem); objectMenuTransformSystem(world);