diff --git a/src/bit-components.js b/src/bit-components.js index e81d24954a1..d6a4119e8e5 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -251,7 +251,8 @@ export const VideoMenu = defineComponent({ trackRef: Types.eid, headRef: Types.eid, playIndicatorRef: Types.eid, - pauseIndicatorRef: Types.eid + pauseIndicatorRef: Types.eid, + clearTargetTimer: Types.f64 }); export const AudioEmitter = defineComponent({ flags: Types.ui8 @@ -280,7 +281,9 @@ export const ObjectMenu = defineComponent({ rotateButtonRef: Types.eid, mirrorButtonRef: Types.eid, scaleButtonRef: Types.eid, - targetRef: Types.eid + targetRef: Types.eid, + handlingTargetRef: Types.eid, + flags: Types.ui8 }); // TODO: Store this data elsewhere, since only one or two will ever exist. export const LinkHoverMenu = defineComponent({ @@ -303,7 +306,9 @@ export const PDFMenu = defineComponent({ targetRef: Types.eid, clearTargetTimer: Types.f64 }); -export const ObjectMenuTarget = defineComponent(); +export const ObjectMenuTarget = defineComponent({ + flags: Types.ui8 +}); export const NetworkDebug = defineComponent(); export const NetworkDebugRef = defineComponent({ ref: Types.eid @@ -381,3 +386,8 @@ export const LinearScale = defineComponent({ export const Quack = defineComponent(); export const TrimeshTag = defineComponent(); export const HeightFieldTag = defineComponent(); +export const ObjectMenuTransform = defineComponent({ + targetObjectRef: Types.eid, + prevObjectRef: Types.eid, + flags: Types.ui8 +}); diff --git a/src/bit-systems/link-hover-menu.ts b/src/bit-systems/link-hover-menu.ts index 5c7ace3d75f..87b1fc28c33 100644 --- a/src/bit-systems/link-hover-menu.ts +++ b/src/bit-systems/link-hover-menu.ts @@ -1,5 +1,4 @@ -import { Not, defineQuery, entityExists } from "bitecs"; -import { Matrix4, Vector3 } from "three"; +import { Not, addComponent, defineQuery, entityExists, removeComponent } from "bitecs"; import type { HubsWorld } from "../app"; import { Link, @@ -8,7 +7,8 @@ import { TextTag, Interacted, LinkHoverMenuItem, - LinkInitializing + LinkInitializing, + ObjectMenuTransform } from "../bit-components"; import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils"; import { hubIdFromUrl } from "../utils/media-url-utils"; @@ -16,8 +16,8 @@ import { Text as TroikaText } from "troika-three-text"; import { handleExitTo2DInterstitial } from "../utils/vr-interstitial"; import { changeHub } from "../change-hub"; import { EntityID } from "../utils/networking-types"; -import { setMatrixWorld } from "../utils/three-utils"; import { LinkType } from "../inflators/link"; +import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; const menuQuery = defineQuery([LinkHoverMenu]); const hoveredLinksQuery = defineQuery([HoveredRemoteRight, Link, Not(LinkInitializing)]); @@ -89,43 +89,6 @@ async function handleLinkClick(world: HubsWorld, button: EntityID) { } } -const _moveTargetPos = new Vector3(); -const _lookAtTargetPos = new Vector3(); -const _objectPos = new Vector3(); -const _mat4 = new Matrix4(); - -// Move the menu object to target object position but a little bit closer -// to the camera and make the menu object look at the camera. -// TODO: Similar code in object-menu system. Expose as util and reuse? -function moveToTarget(world: HubsWorld, menu: EntityID) { - const menuObj = world.eid2obj.get(menu)!; - - const targetObj = world.eid2obj.get(LinkHoverMenu.targetObjectRef[menu])!; - targetObj.updateMatrices(); - - // TODO: Remove the dependency with AFRAME - const camera = AFRAME.scenes[0].systems["hubs-systems"].cameraSystem.viewingCamera; - camera.updateMatrices(); - - _moveTargetPos.setFromMatrixPosition(targetObj.matrixWorld); - _lookAtTargetPos.setFromMatrixPosition(camera.matrixWorld); - - // Place the menu object a little bit closer to the camera in the scene - _objectPos - .copy(_lookAtTargetPos) - .sub(_moveTargetPos) - .normalize() - // TODO: 0.5 is an arbitrary number. 0.5 might be too small for - // huge target object. Using bounding box may be safer? - // TODO: What if camera is between the menu and the target object? - .multiplyScalar(0.5) - .add(_moveTargetPos); - - _mat4.copy(camera.matrixWorld).setPosition(_objectPos); - setMatrixWorld(menuObj, _mat4); - menuObj.lookAt(_lookAtTargetPos); -} - function updateButtonText(world: HubsWorld, menu: EntityID, button: EntityID) { const text = findChildWithComponent(world, TextTag, button)!; const textObj = world.eid2obj.get(text)! as TroikaText; @@ -157,6 +120,15 @@ function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean, for const target = LinkHoverMenu.targetObjectRef[menu]; const visible = !!target && !frozen; + // TODO We are handling menus visibility in a similar way for all the object menus, we + // should probably refactor this to a common object-menu-visibility-system + if (visible) { + ObjectMenuTransform.targetObjectRef[menu] = target; + ObjectMenuTransform.flags[menu] |= ObjectMenuTransformFlags.Enabled; + } else { + ObjectMenuTransform.flags[menu] &= ~ObjectMenuTransformFlags.Enabled; + } + const obj = world.eid2obj.get(menu)!; obj.visible = visible; @@ -182,7 +154,6 @@ export function linkHoverMenuSystem(world: HubsWorld, sceneIsFrozen: boolean) { updateLinkMenuTarget(world, menu, sceneIsFrozen); const currTarget = LinkHoverMenu.targetObjectRef[menu]; if (currTarget) { - moveToTarget(world, menu); clickedMenuItemQuery(world).forEach(eid => handleLinkClick(world, eid)); } flushToObject3Ds(world, menu, sceneIsFrozen, prevTarget !== currTarget); diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index 9195112acb8..c809358af63 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -33,8 +33,7 @@ import { EntityID } from "../utils/networking-types"; const getBox = (() => { const rotation = new Euler(); - return (world: HubsWorld, eid: EntityID, rootEid: EntityID, worldSpace?: boolean) => { - const box = new Box3(); + return (world: HubsWorld, eid: EntityID, rootEid: EntityID, box: Box3, worldSpace?: boolean) => { const obj = world.eid2obj.get(eid)!; const rootObj = world.eid2obj.get(rootEid)!; @@ -57,8 +56,6 @@ const getBox = (() => { rootObj.matrixWorldNeedsUpdate = true; rootObj.updateMatrices(); - - return box; }; })(); @@ -182,6 +179,7 @@ function* loadByMediaType( case MediaType.IMAGE: return yield* loadImage( world, + eid, accessibleUrl, contentType, MediaImageLoaderData.has(eid) ? MediaImageLoaderData.get(eid)! : {} @@ -189,6 +187,7 @@ function* loadByMediaType( case MediaType.VIDEO: return yield* loadVideo( world, + eid, accessibleUrl, contentType, MediaVideoLoaderData.has(eid) ? MediaVideoLoaderData.get(eid)! : {} @@ -196,15 +195,16 @@ function* loadByMediaType( case MediaType.MODEL: return yield* loadModel(world, accessibleUrl, contentType, true); case MediaType.PDF: - return yield* loadPDF(world, accessibleUrl); + return yield* loadPDF(world, eid, accessibleUrl); case MediaType.AUDIO: return yield* loadAudio( world, + eid, accessibleUrl, MediaVideoLoaderData.has(eid) ? MediaVideoLoaderData.get(eid)! : {} ); case MediaType.HTML: - return yield* loadHtml(world, canonicalUrl, thumbnail); + return yield* loadHtml(world, eid, canonicalUrl, thumbnail); default: throw new UnsupportedMediaTypeError(eid, mediaType); } @@ -234,6 +234,7 @@ function* loadMedia(world: HubsWorld, eid: EntityID) { } const tmpVector = new Vector3(); +const box = new Box3(); function* loadAndAnimateMedia(world: HubsWorld, eid: EntityID, clearRollbacks: ClearFunction) { if (MediaLoader.flags[eid] & MEDIA_LOADER_FLAGS.IS_OBJECT_MENU_TARGET) { addComponent(world, ObjectMenuTarget, eid); @@ -251,7 +252,7 @@ function* loadAndAnimateMedia(world: HubsWorld, eid: EntityID, clearRollbacks: C if (media) { if (hasComponent(world, MediaLoaded, media)) { - const box = getBox(world, eid, media); + getBox(world, eid, media, box); addComponent(world, MediaContentBounds, eid); box.getSize(tmpVector); MediaContentBounds.bounds[eid].set(tmpVector.toArray()); diff --git a/src/bit-systems/object-menu-transform-system.ts b/src/bit-systems/object-menu-transform-system.ts new file mode 100644 index 00000000000..5419f8e0565 --- /dev/null +++ b/src/bit-systems/object-menu-transform-system.ts @@ -0,0 +1,113 @@ +import { defineQuery } from "bitecs"; +import { HubsWorld } from "../app"; +import { ObjectMenuTarget, ObjectMenuTransform } from "../bit-components"; +import { EntityID } from "../utils/networking-types"; +import { Box3, Matrix4, Object3D, Quaternion, Sphere, Vector3 } from "three"; +import { isFacingCamera, setFromObject, setMatrixWorld } from "../utils/three-utils"; +import { ObjectMenuTargetFlags } from "../inflators/object-menu-target"; +import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; + +const offset = new Vector3(); +const tmpVec1 = new Vector3(); +const tmpVec2 = new Vector3(); +const tmpQuat1 = new Quaternion(); +const tmpQuat2 = new Quaternion(); +const tmpMat4 = new Matrix4(); +const tmpMat42 = new Matrix4(); +const aabb = new Box3(); +const sphere = new Sphere(); +const yVector = new Vector3(0, 1, 0); + +// Calculate the AABB without accounting for the root object rotation +function getAABB(obj: Object3D, box: Box3, onlyVisible: boolean = false) { + const parent = obj.parent; + obj.removeFromParent(); + obj.updateMatrices(true, true); + tmpMat4.copy(obj.matrixWorld); + tmpMat4.decompose(tmpVec1, tmpQuat1, tmpVec2); + tmpQuat2.copy(tmpQuat1); + obj.quaternion.identity(); + obj.updateMatrix(); + obj.updateMatrixWorld(); + setFromObject(box, obj, onlyVisible); + parent?.add(obj); + obj.quaternion.copy(tmpQuat2); + obj.updateMatrix(); + obj.updateMatrixWorld(); +} + +// Check https://github.com/mozilla/hubs/pull/6289#issuecomment-1739003555 for implementation details. +function transformMenu(world: HubsWorld, menu: EntityID) { + const targetEid = ObjectMenuTransform.targetObjectRef[menu]; + const targetObj = world.eid2obj.get(targetEid); + const enabled = (ObjectMenuTransform.flags[menu] & ObjectMenuTransformFlags.Enabled) !== 0 ? true : false; + if (!targetObj || !enabled) return; + + const menuObj = world.eid2obj.get(menu)!; + + // Calculate the menu offset based on visible elements + const center = (ObjectMenuTransform.flags[menu] & ObjectMenuTransformFlags.Center) !== 0 ? true : false; + if (center && ObjectMenuTransform.targetObjectRef[menu] !== ObjectMenuTransform.prevObjectRef[menu]) { + getAABB(menuObj, aabb); + aabb.getCenter(tmpVec1); + getAABB(menuObj, aabb, true); + aabb.getCenter(tmpVec2); + offset.subVectors(tmpVec1, tmpVec2); + offset.z = 0; + } + + const camera = APP.scene?.systems["hubs-systems"].cameraSystem.viewingCamera; + camera.updateMatrices(); + + const isFlat = (ObjectMenuTarget.flags[targetEid] & ObjectMenuTargetFlags.Flat) !== 0 ? true : false; + if (!isFlat) { + getAABB(targetObj, aabb); + aabb.getBoundingSphere(sphere); + + tmpMat4.copy(targetObj.matrixWorld); + tmpMat4.decompose(tmpVec1, tmpQuat1, tmpVec2); + tmpVec2.set(1.0, 1.0, 1.0); + tmpMat4.compose(sphere.center, tmpQuat1, tmpVec2); + + setMatrixWorld(menuObj, tmpMat4); + + menuObj.lookAt(tmpVec2.setFromMatrixPosition(camera.matrixWorld)); + menuObj.translateZ(sphere.radius); + + if (center) { + menuObj.position.add(offset); + menuObj.matrixNeedsUpdate = true; + } + + // TODO We need to handle the menu positioning when the player is inside the bounding sphere. + // For now we are defaulting to the current AFrame behavior. + } else { + targetObj.updateMatrices(true, true); + tmpMat4.copy(targetObj.matrixWorld); + tmpMat4.decompose(tmpVec1, tmpQuat1, tmpVec2); + + const isFacing = isFacingCamera(targetObj); + if (!isFacing) { + tmpQuat1.setFromAxisAngle(yVector, Math.PI); + tmpMat42.makeRotationFromQuaternion(tmpQuat1); + tmpMat4.multiply(tmpMat42); + } + + if (center) { + tmpMat42.makeTranslation(offset.x, offset.y, offset.z); + tmpMat4.multiply(tmpMat42); + } + + setMatrixWorld(menuObj, tmpMat4); + } + + ObjectMenuTransform.prevObjectRef[menu] = ObjectMenuTransform.targetObjectRef[menu]; +} + +const menuQuery = defineQuery([ObjectMenuTransform]); + +export function objectMenuTransformSystem(world: HubsWorld) { + menuQuery(world).forEach(menu => { + transformMenu(world, menu); + }); +} diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts index 31bfb5d6db4..35743e45780 100644 --- a/src/bit-systems/object-menu.ts +++ b/src/bit-systems/object-menu.ts @@ -1,8 +1,9 @@ import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent } from "bitecs"; -import { Matrix4, Quaternion, Vector3 } from "three"; +import { Matrix4, Vector3 } from "three"; import type { HubsWorld } from "../app"; import { EntityStateDirty, + ObjectMenuTransform, HeldRemoteRight, HoveredRemoteRight, Interacted, @@ -10,9 +11,11 @@ import { ObjectMenu, ObjectMenuTarget, RemoteRight, - Rigidbody + Rigidbody, + MediaContentBounds, + Deleting } from "../bit-components"; -import { anyEntityWith, findAncestorWithComponent } from "../utils/bit-utils"; +import { anyEntityWith, findAncestorWithAnyComponent } from "../utils/bit-utils"; import { createNetworkedEntity } from "../utils/create-networked-entity"; import HubChannel from "../utils/hub-channel"; import type { EntityID } from "../utils/networking-types"; @@ -23,21 +26,18 @@ import { createMessageDatas, isPinned } from "./networking"; import { TRANSFORM_MODE } from "../components/transform-object-button"; import { ScalingHandler } from "../components/scale-button"; import { canPin, setPinned } from "../utils/bit-pinning-helper"; +import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; // Working variables. const _vec3_1 = new Vector3(); const _vec3_2 = new Vector3(); -const _quat = new Quaternion(); const _mat4 = new Matrix4(); let scalingHandler: ScalingHandler | null = null; -// Needs to remember rotating/scaling entity id because -// rotation/scaling are operated by dragging so that -// rotation/scaling operation can continue even if the cursor -// hover off the target entity. -// TODO: Should this data stored in Component? -let handlingTargetEid: EntityID = 0; +export const enum ObjectMenuFlags { + Visible = 1 << 0 +} function clicked(world: HubsWorld, eid: EntityID) { return hasComponent(world, Interacted, eid); @@ -48,8 +48,22 @@ function objectMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boole return 0; } - const target = hoveredQuery(world).map(eid => findAncestorWithComponent(world, ObjectMenuTarget, eid))[0]; - if (target) return target; + // We can have more than one object menu target in the object hierarchy so we need to explicity look + // for the media loader entity here. ie. The object menu target in the media loader entity and the + // video menu in the video entity + // TODO We should use something more meaningful than MediaContentBounds for the media loader root entity + // or rename that to something like MediaRoot + const target = hoveredQuery(world).map(eid => + findAncestorWithAnyComponent(world, [MediaContentBounds, ObjectMenuTarget], eid) + )[0]; + if (target) { + if (hasComponent(world, Deleting, target)) { + return 0; + } else { + ObjectMenu.flags[menu] |= ObjectMenuFlags.Visible; + return target; + } + } if (entityExists(world, ObjectMenu.targetRef[menu])) { return ObjectMenu.targetRef[menu]; @@ -58,39 +72,10 @@ function objectMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boole return 0; } -function moveToTarget(world: HubsWorld, menu: EntityID) { - const targetObj = world.eid2obj.get(ObjectMenu.targetRef[menu])!; - targetObj.updateMatrices(); - - // TODO: position the menu more carefully... - // For example, if a menu object is just placed at a target - // object's position the menu object can be hidden by a large - // target object or the menu object looks too small for a far - // target object. - _mat4.copy(targetObj.matrixWorld); - - // Keeps world scale (1, 1, 1) because - // a menu object is a child of a target object - // and the target object's scale can be changed. - // Another option may be making the menu object - // a sibling of the target object. - _mat4.decompose(_vec3_1, _quat, _vec3_2); - _vec3_2.set(1.0, 1.0, 1.0); - _mat4.compose(_vec3_1, _quat, _vec3_2); - - const menuObj = world.eid2obj.get(menu)!; - setMatrixWorld(menuObj, _mat4); - - // TODO: Remove the dependency with AFRAME - const camera = AFRAME.scenes[0].systems["hubs-systems"].cameraSystem.viewingCamera; - camera.updateMatrices(); - menuObj.lookAt(_vec3_1.setFromMatrixPosition(camera.matrixWorld)); -} - // TODO: startRotation/Scaling() and stopRotation/Scaling() are // temporary implementation that rely on the old systems. // They should be rewritten more elegantly with bitecs. -function startRotation(world: HubsWorld, targetEid: EntityID) { +function startRotation(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) { if (hasComponent(world, Networked, targetEid)) { takeOwnership(world, targetEid); } @@ -101,27 +86,29 @@ function startRotation(world: HubsWorld, targetEid: EntityID) { transformSystem.startTransform(world.eid2obj.get(targetEid)!, world.eid2obj.get(rightCursorEid)!, { mode: TRANSFORM_MODE.CURSOR }); - handlingTargetEid = targetEid; + ObjectMenu.handlingTargetRef[menuEid] = targetEid; } -function stopRotation(world: HubsWorld) { +function stopRotation(world: HubsWorld, menuEid: EntityID) { // TODO: More proper handling in case the target entity is already removed. // In the worst scenario the entity has been already recycled at this moment // and this code doesn't handled such a case correctly. // We may refactor when we will reimplement the object menu system // by removing A-Frame dependency. + const handlingTargetEid = ObjectMenu.handlingTargetRef[menuEid]; if (entityExists(world, handlingTargetEid) && hasComponent(world, Networked, handlingTargetEid)) { addComponent(world, EntityStateDirty, handlingTargetEid); } const transformSystem = APP.scene!.systems["transform-selected-object"]; transformSystem.stopTransform(); - handlingTargetEid = 0; + ObjectMenu.handlingTargetRef[menuEid] = 0; } -function startScaling(world: HubsWorld, targetEid: EntityID) { +function startScaling(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) { if (hasComponent(world, Networked, targetEid)) { takeOwnership(world, targetEid); } + // TODO: Don't use any // TODO: Remove the dependency with AFRAME const transformSystem = (AFRAME as any).scenes[0].systems["transform-selected-object"]; @@ -131,17 +118,18 @@ function startScaling(world: HubsWorld, targetEid: EntityID) { scalingHandler = new ScalingHandler(world.eid2obj.get(targetEid), transformSystem); scalingHandler!.objectToScale = world.eid2obj.get(targetEid); scalingHandler!.startScaling(world.eid2obj.get(rightCursorEid)); - handlingTargetEid = targetEid; + ObjectMenu.handlingTargetRef[menuEid] = targetEid; } -function stopScaling(world: HubsWorld) { +function stopScaling(world: HubsWorld, menuEid: EntityID) { + const handlingTargetEid = ObjectMenu.handlingTargetRef[menuEid]; if (entityExists(world, handlingTargetEid) && hasComponent(world, Networked, handlingTargetEid)) { addComponent(world, EntityStateDirty, handlingTargetEid); } const rightCursorEid = anyEntityWith(world, RemoteRight)!; scalingHandler!.endScaling(world.eid2obj.get(rightCursorEid)); scalingHandler = null; - handlingTargetEid = 0; + ObjectMenu.handlingTargetRef[menuEid] = 0; } function openLink(world: HubsWorld, eid: EntityID) { @@ -187,6 +175,7 @@ function handleClicks(world: HubsWorld, menu: EntityID, hubChannel: HubChannel) } else if (clicked(world, ObjectMenu.cameraTrackButtonRef[menu])) { console.log("Clicked track"); } else if (clicked(world, ObjectMenu.removeButtonRef[menu])) { + ObjectMenu.flags[menu] &= ~ObjectMenuFlags.Visible; deleteTheDeletableAncestor(world, ObjectMenu.targetRef[menu]); } else if (clicked(world, ObjectMenu.dropButtonRef[menu])) { console.log("Clicked drop"); @@ -208,10 +197,12 @@ function handleClicks(world: HubsWorld, menu: EntityID, hubChannel: HubChannel) function handleHeldEnter(world: HubsWorld, eid: EntityID, menuEid: EntityID) { switch (eid) { case ObjectMenu.rotateButtonRef[menuEid]: - startRotation(world, ObjectMenu.targetRef[menuEid]); + ObjectMenu.flags[menuEid] &= ~ObjectMenuFlags.Visible; + startRotation(world, menuEid, ObjectMenu.targetRef[menuEid]); break; case ObjectMenu.scaleButtonRef[menuEid]: - startScaling(world, ObjectMenu.targetRef[menuEid]); + ObjectMenu.flags[menuEid] &= ~ObjectMenuFlags.Visible; + startScaling(world, menuEid, ObjectMenu.targetRef[menuEid]); break; } } @@ -219,17 +210,28 @@ function handleHeldEnter(world: HubsWorld, eid: EntityID, menuEid: EntityID) { function handleHeldExit(world: HubsWorld, eid: EntityID, menuEid: EntityID) { switch (eid) { case ObjectMenu.rotateButtonRef[menuEid]: - stopRotation(world); + ObjectMenu.flags[menuEid] |= ObjectMenuFlags.Visible; + stopRotation(world, menuEid); break; case ObjectMenu.scaleButtonRef[menuEid]: - stopScaling(world); + ObjectMenu.flags[menuEid] |= ObjectMenuFlags.Visible; + stopScaling(world, menuEid); break; } } function updateVisibility(world: HubsWorld, menu: EntityID, frozen: boolean) { const target = ObjectMenu.targetRef[menu]; - const visible = !!(target && frozen); + const visible = !!(target && frozen) && (ObjectMenu.flags[menu] & ObjectMenuFlags.Visible) !== 0; + + // TODO We are handling menus visibility in a similar way for all the object menus, we + // should probably refactor this to a common object-menu-visibility-system + if (visible) { + ObjectMenuTransform.targetObjectRef[menu] = target; + ObjectMenuTransform.flags[menu] |= ObjectMenuTransformFlags.Enabled; + } else { + ObjectMenuTransform.flags[menu] &= ~ObjectMenuTransformFlags.Enabled; + } const obj = world.eid2obj.get(menu)!; obj.visible = visible; @@ -273,7 +275,8 @@ export function objectMenuSystem(world: HubsWorld, sceneIsFrozen: boolean, hubCh handleHeldExit(world, eid, menu); }); - ObjectMenu.targetRef[menu] = objectMenuTarget(world, menu, sceneIsFrozen); + const targetEid = objectMenuTarget(world, menu, sceneIsFrozen); + ObjectMenu.targetRef[menu] = targetEid; if (ObjectMenu.targetRef[menu]) { handleClicks(world, menu, hubChannel); @@ -285,8 +288,6 @@ export function objectMenuSystem(world: HubsWorld, sceneIsFrozen: boolean, hubCh if (scalingHandler !== null) { scalingHandler.tick(); } - - moveToTarget(world, menu); } updateVisibility(world, menu, sceneIsFrozen); } diff --git a/src/bit-systems/pdf-menu-system.ts b/src/bit-systems/pdf-menu-system.ts index e8a2b7a8374..6e4bf140602 100644 --- a/src/bit-systems/pdf-menu-system.ts +++ b/src/bit-systems/pdf-menu-system.ts @@ -1,12 +1,20 @@ import { addComponent, defineQuery, entityExists, hasComponent } from "bitecs"; import { Text } from "troika-three-text"; import type { HubsWorld } from "../app"; -import { EntityStateDirty, HoveredRemoteRight, Interacted, MediaPDF, NetworkedPDF, PDFMenu } from "../bit-components"; +import { + EntityStateDirty, + HoveredRemoteRight, + Interacted, + MediaPDF, + NetworkedPDF, + ObjectMenuTransform, + PDFMenu +} from "../bit-components"; import { anyEntityWith, findAncestorWithComponent } from "../utils/bit-utils"; import type { EntityID } from "../utils/networking-types"; import { takeOwnership } from "../utils/take-ownership"; -import { setMatrixWorld } from "../utils/three-utils"; import { PDFResourcesMap } from "./pdf-system"; +import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; function clicked(world: HubsWorld, eid: EntityID) { return hasComponent(world, Interacted, eid); @@ -42,13 +50,6 @@ function findPDFMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: bool } } -function moveToTarget(world: HubsWorld, menu: EntityID) { - const targetObj = world.eid2obj.get(PDFMenu.targetRef[menu])!; - targetObj.updateMatrices(); - const menuObj = world.eid2obj.get(menu)!; - setMatrixWorld(menuObj, targetObj.matrixWorld); -} - function wrapAround(n: number, min: number, max: number) { // Wrap around [min, max] inclusively // Assumes that n is only 1 more than max or 1 less than min @@ -78,6 +79,15 @@ function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { const obj = world.eid2obj.get(menu)!; obj.visible = visible; + // TODO We are handling menus visibility in a similar way for all the object menus, we + // should probably refactor this to a common object-menu-visibility-system + if (visible) { + ObjectMenuTransform.targetObjectRef[menu] = target; + ObjectMenuTransform.flags[menu] |= ObjectMenuTransformFlags.Enabled; + } else { + ObjectMenuTransform.flags[menu] &= ~ObjectMenuTransformFlags.Enabled; + } + [PDFMenu.prevButtonRef[menu], PDFMenu.nextButtonRef[menu]].forEach(buttonRef => { const buttonObj = world.eid2obj.get(buttonRef)!; // Parent visibility doesn't block raycasting, so we must set each button to be invisible @@ -96,7 +106,6 @@ export function pdfMenuSystem(world: HubsWorld, sceneIsFrozen: boolean) { const menu = anyEntityWith(world, PDFMenu)!; findPDFMenuTarget(world, menu, sceneIsFrozen); if (PDFMenu.targetRef[menu]) { - moveToTarget(world, menu); handleClicks(world, menu); } flushToObject3Ds(world, menu, sceneIsFrozen); diff --git a/src/bit-systems/video-menu-system.ts b/src/bit-systems/video-menu-system.ts index 4f9681106d4..ea10ee90b2d 100644 --- a/src/bit-systems/video-menu-system.ts +++ b/src/bit-systems/video-menu-system.ts @@ -12,8 +12,8 @@ import { MediaVideo, MediaVideoData, NetworkedVideo, - VideoMenu, - VideoMenuItem + ObjectMenuTransform, + VideoMenu } from "../bit-components"; import { timeFmt } from "../components/media-video"; import { takeOwnership } from "../utils/take-ownership"; @@ -23,12 +23,12 @@ import { coroutine } from "../utils/coroutine"; import { easeOutQuadratic } from "../utils/easing"; import { isFacingCamera } from "../utils/three-utils"; import { Emitter2Audio } from "./audio-emitter-system"; -import { VIDEO_FLAGS } from "../inflators/video"; +import { EntityID } from "../utils/networking-types"; +import { findAncestorWithComponent, hasAnyComponent } from "../utils/bit-utils"; +import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; const videoMenuQuery = defineQuery([VideoMenu]); -const hoverRightVideoQuery = defineQuery([HoveredRemoteRight, MediaVideo]); -const hoverRightVideoEnterQuery = enterQuery(hoverRightVideoQuery); -const hoverRightMenuItemQuery = defineQuery([HoveredRemoteRight, VideoMenuItem]); +const hoveredQuery = defineQuery([HoveredRemoteRight]); const sliderHalfWidth = 0.475; function setCursorRaycastable(world: HubsWorld, menu: number, enable: boolean) { @@ -45,7 +45,6 @@ const intersectInThePlaneOf = (() => { ray.set(position, direction); plane.normal.set(0, 0, 1); plane.constant = 0; - obj.updateMatrices(); plane.applyMatrix4(obj.matrixWorld); ray.intersectPlane(plane, intersection); }; @@ -54,33 +53,68 @@ const intersectInThePlaneOf = (() => { type Job = () => IteratorResult; let rightMenuIndicatorCoroutine: Job | null = null; -let intersectionPoint = new Vector3(); -export function videoMenuSystem(world: HubsWorld, userinput: any) { - const rightVideoMenu = videoMenuQuery(world)[0]; - const shouldHideVideoMenu = - VideoMenu.videoRef[rightVideoMenu] && - (!entityExists(world, VideoMenu.videoRef[rightVideoMenu]) || - (!hoverRightVideoQuery(world).length && - !hoverRightMenuItemQuery(world).length && - !hasComponent(world, Held, VideoMenu.trackRef[rightVideoMenu]))); - if (shouldHideVideoMenu) { - const menu = rightVideoMenu; - const menuObj = world.eid2obj.get(menu)!; - menuObj.removeFromParent(); - setCursorRaycastable(world, menu, false); +function findVideoMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boolean) { + if (VideoMenu.videoRef[menu] && !entityExists(world, VideoMenu.videoRef[menu])) { + // Clear the invalid entity reference. (The pdf entity was removed). VideoMenu.videoRef[menu] = 0; } - hoverRightVideoEnterQuery(world).forEach(function (eid) { - if (MediaVideo.flags[eid] & VIDEO_FLAGS.CONTROLS) { - const menu = rightVideoMenu; - VideoMenu.videoRef[menu] = eid; - const menuObj = world.eid2obj.get(menu)!; - const videoObj = world.eid2obj.get(eid)!; - videoObj.add(menuObj); - setCursorRaycastable(world, menu, true); - } - }); + if (sceneIsFrozen) { + VideoMenu.videoRef[menu] = 0; + return; + } + + const isTrackHoveredOrHeld = hasAnyComponent(world, [Held, HoveredRemoteRight], VideoMenu.trackRef[menu]); + if (isTrackHoveredOrHeld) { + VideoMenu.clearTargetTimer[menu] = world.time.elapsed + 1000; + return; + } + + const hovered = hoveredQuery(world); + const target = hovered.map(eid => findAncestorWithComponent(world, MediaVideo, eid))[0] || 0; + if (target) { + VideoMenu.videoRef[menu] = target; + VideoMenu.clearTargetTimer[menu] = world.time.elapsed + 1000; + return; + } + + if (hovered.some(eid => findAncestorWithComponent(world, VideoMenu, eid))) { + VideoMenu.clearTargetTimer[menu] = world.time.elapsed + 1000; + return; + } + + if (world.time.elapsed > VideoMenu.clearTargetTimer[menu]) { + VideoMenu.videoRef[menu] = 0; + return; + } +} + +function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { + const target = VideoMenu.videoRef[menu]; + const visible = !!(target && !frozen); + + const obj = world.eid2obj.get(menu)!; + obj.visible = visible; + + // TODO We are handling menus visibility in a similar way for all the object menus, we + // should probably refactor this to a common object-menu-visibility-system + if (visible) { + setCursorRaycastable(world, menu, true); + APP.world.scene.add(obj); + ObjectMenuTransform.targetObjectRef[menu] = target; + ObjectMenuTransform.flags[menu] |= ObjectMenuTransformFlags.Enabled; + } else { + obj.removeFromParent(); + setCursorRaycastable(world, menu, false); + + ObjectMenuTransform.flags[menu] &= ~ObjectMenuTransformFlags.Enabled; + } +} + +let intersectionPoint = new Vector3(); +export function videoMenuSystem(world: HubsWorld, userinput: any, sceneIsFrozen: boolean) { + const rightVideoMenu = videoMenuQuery(world)[0]; + findVideoMenuTarget(world, rightVideoMenu, sceneIsFrozen); videoMenuQuery(world).forEach(function (eid) { const videoEid = VideoMenu.videoRef[eid]; @@ -153,6 +187,8 @@ export function videoMenuSystem(world: HubsWorld, userinput: any) { rightMenuIndicatorCoroutine = null; } }); + + flushToObject3Ds(world, rightVideoMenu, sceneIsFrozen); } const START_SCALE = new Vector3().setScalar(0.05); diff --git a/src/inflators/object-menu-target.ts b/src/inflators/object-menu-target.ts new file mode 100644 index 00000000000..73a9eefda68 --- /dev/null +++ b/src/inflators/object-menu-target.ts @@ -0,0 +1,24 @@ +import { addComponent } from "bitecs"; +import { HubsWorld } from "../app"; +import { EntityID } from "../utils/networking-types"; +import { ObjectMenuTarget } from "../bit-components"; + +export const ObjectMenuTargetFlags = { + Flat: 1 << 0 +}; + +export type ObjectMenuTargetParams = { + isFlat?: boolean; +}; + +const DEFAULTS = { + isFlat: true +}; + +export function inflateObjectMenuTarget(world: HubsWorld, eid: EntityID, params: ObjectMenuTargetParams) { + params = Object.assign({}, DEFAULTS, params); + addComponent(world, ObjectMenuTarget, eid); + if (params.isFlat === true) { + ObjectMenuTarget.flags[eid] |= ObjectMenuTargetFlags.Flat; + } +} diff --git a/src/inflators/object-menu-transform.ts b/src/inflators/object-menu-transform.ts new file mode 100644 index 00000000000..301ae648d11 --- /dev/null +++ b/src/inflators/object-menu-transform.ts @@ -0,0 +1,25 @@ +import { addComponent } from "bitecs"; +import { HubsWorld } from "../app"; +import { EntityID } from "../utils/networking-types"; +import { ObjectMenuTransform } from "../bit-components"; + +export const ObjectMenuTransformFlags = { + Enabled: 1 << 0, + Center: 1 << 1 +}; + +export type ObjectMenuTransformParams = { + center?: boolean; +}; + +const DEFAULTS = { + center: true +}; + +export function inflateObjectMenuTransform(world: HubsWorld, eid: EntityID, params: ObjectMenuTransformParams) { + params = Object.assign({}, DEFAULTS, params); + addComponent(world, ObjectMenuTransform, eid); + if (params.center === true) { + ObjectMenuTransform.flags[eid] |= ObjectMenuTransformFlags.Center; + } +} diff --git a/src/prefabs/duck.tsx b/src/prefabs/duck.tsx index 99c86771281..2bf14fc315e 100644 --- a/src/prefabs/duck.tsx +++ b/src/prefabs/duck.tsx @@ -44,6 +44,7 @@ export function DuckPrefab(): EntityDef { halfExtents: [0.25, 0.5, 0.45] }} scale={[1, 1, 1]} + deletable /> ); } diff --git a/src/prefabs/link-hover-menu.tsx b/src/prefabs/link-hover-menu.tsx index 0a1f5059757..d97ebbdee78 100644 --- a/src/prefabs/link-hover-menu.tsx +++ b/src/prefabs/link-hover-menu.tsx @@ -32,6 +32,7 @@ export function LinkHoverMenuPrefab() { return (