-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
185 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,98 +1,150 @@ | ||
import { defineQuery, enterQuery, entityExists } from "bitecs"; | ||
import { defineQuery, entityExists } from "bitecs"; | ||
import type { HubsWorld } from "../app"; | ||
import { Interacted, Link, LinkHoverMenu, LinkHoverMenuItem, HoveredRemoteRight, TextTag } from "../bit-components"; | ||
import { anyEntityWith, findChildWithComponent } from "../utils/bit-utils"; | ||
import { isHubsRoomUrl, isLocalHubsAvatarUrl, isLocalHubsSceneUrl, isLocalHubsUrl } from "../utils/media-url-utils"; | ||
import { Link, LinkHoverMenu, HoveredRemoteRight, TextTag, Interacted, LinkHoverMenuItem } from "../bit-components"; | ||
import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils"; | ||
import { hubIdFromUrl } from "../utils/media-url-utils"; | ||
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"; | ||
|
||
const NULL_EID = 0; | ||
|
||
const hoveredQuery = defineQuery([HoveredRemoteRight, Link]); | ||
const hoveredEnterQuery = enterQuery(hoveredQuery); | ||
const hoveredMenuItemQuery = defineQuery([HoveredRemoteRight, LinkHoverMenuItem]); | ||
const menuQuery = defineQuery([LinkHoverMenu]); | ||
const hoveredQuery = defineQuery([HoveredRemoteRight]); | ||
const clickedMenuItemQuery = defineQuery([Interacted, LinkHoverMenuItem]); | ||
|
||
export function linkHoverMenuSystem(world: HubsWorld) { | ||
// Assumes always only single LinkHoverMenu entity exists for now. | ||
// TODO: Take into account for more than one for VR | ||
const menuEid = anyEntityWith(world, LinkHoverMenu)!; | ||
const menuObject = world.eid2obj.get(menuEid)!; | ||
function updateLinkMenuTarget(world: HubsWorld, menu: EntityID, sceneIsFrozen: boolean) { | ||
if (LinkHoverMenu.targetObjectRef[menu] && !entityExists(world, LinkHoverMenu.targetObjectRef[menu])) { | ||
// Clear the invalid entity reference. (The link entity was removed). | ||
LinkHoverMenu.targetObjectRef[menu] = 0; | ||
return; | ||
} | ||
|
||
if (sceneIsFrozen) { | ||
LinkHoverMenu.targetObjectRef[menu] = 0; | ||
return; | ||
} | ||
|
||
const hovered = hoveredQuery(world); | ||
const target = hovered.map(eid => findAncestorWithComponent(world, Link, eid))[0] || 0; | ||
if (target) { | ||
LinkHoverMenu.targetObjectRef[menu] = target; | ||
LinkHoverMenu.clearTargetTimer[menu] = world.time.elapsed + 1000; | ||
return; | ||
} | ||
|
||
if (hovered.some(eid => findAncestorWithComponent(world, LinkHoverMenu, eid))) { | ||
LinkHoverMenu.clearTargetTimer[menu] = world.time.elapsed + 1000; | ||
return; | ||
} | ||
|
||
if (world.time.elapsed > LinkHoverMenu.clearTargetTimer[menu]) { | ||
LinkHoverMenu.targetObjectRef[menu] = 0; | ||
return; | ||
} | ||
} | ||
|
||
async function handleLinkClick(world: HubsWorld, button: EntityID) { | ||
const exitImmersive = async () => await handleExitTo2DInterstitial(false, () => {}, true); | ||
|
||
const menu = findAncestorWithComponent(world, LinkHoverMenu, button)!; | ||
const linkEid = LinkHoverMenu.targetObjectRef[menu]; | ||
const src = APP.getString(Link.url[linkEid])!; | ||
const url = new URL(src); | ||
const linkType = Link.type[linkEid]; | ||
switch (linkType) { | ||
case LinkType.LINK: | ||
await exitImmersive(); | ||
window.open(src); | ||
break; | ||
case LinkType.AVATAR: | ||
const avatarId = new URL(src).pathname.split("/").pop(); | ||
window.APP.store.update({ profile: { avatarId } }); | ||
APP.scene!.emit("avatar_updated"); | ||
break; | ||
case LinkType.SCENE: | ||
APP.scene!.emit("scene_media_selected", src); | ||
break; | ||
case LinkType.WAYPOINT: | ||
// move to waypoint w/o writing to history | ||
window.history.replaceState(null, "", window.location.href.split("#")[0] + url.hash); | ||
break; | ||
case LinkType.LOCAL_ROOM: | ||
const waypoint = url.hash && url.hash.substring(1); | ||
// move to new room without page load or entry flow | ||
const hubId = hubIdFromUrl(url); | ||
changeHub(hubId, true, waypoint); | ||
break; | ||
case LinkType.EXTERNAL_ROOM: | ||
await exitImmersive(); | ||
location.href = src; | ||
break; | ||
} | ||
} | ||
|
||
// Save Link object eid in LinkHoverMenu when hovered. | ||
hoveredEnterQuery(world).forEach(async (eid: number) => { | ||
LinkHoverMenu.targetObjectRef[menuEid] = eid; | ||
const targetObject = world.eid2obj.get(eid)!; | ||
targetObject.add(menuObject); | ||
function moveToTarget(world: HubsWorld, menu: EntityID) { | ||
const linkEid = LinkHoverMenu.targetObjectRef[menu]; | ||
const targetObject = world.eid2obj.get(linkEid)!; | ||
targetObject.updateMatrices(); | ||
const menuObject = world.eid2obj.get(menu)!; | ||
setMatrixWorld(menuObject, targetObject.matrixWorld); | ||
} | ||
|
||
const buttonRef = LinkHoverMenu.linkButtonRef[menuEid]; | ||
let text = findChildWithComponent(world, TextTag, buttonRef)!; | ||
let textObj = world.eid2obj.get(text)! as TroikaText; | ||
const mayChangeScene = (APP.scene?.systems as any).permissions.canOrWillIfCreator("update_hub"); | ||
const src = APP.getString(Link.url[eid])!; | ||
let hubId; | ||
let label = "open link"; | ||
if (await isLocalHubsAvatarUrl(src)) { | ||
function updateButtonText(world: HubsWorld, menu: EntityID, button: EntityID) { | ||
const text = findChildWithComponent(world, TextTag, button)!; | ||
const textObj = world.eid2obj.get(text)! as TroikaText; | ||
const linkEid = LinkHoverMenu.targetObjectRef[menu]; | ||
const linkType = Link.type[linkEid]; | ||
let label = "open link"; | ||
switch (linkType) { | ||
case LinkType.AVATAR: | ||
label = "use avatar"; | ||
} else if ((await isLocalHubsSceneUrl(src)) && mayChangeScene) { | ||
break; | ||
case LinkType.SCENE: | ||
label = "use scene"; | ||
} else if ((hubId = await isHubsRoomUrl(src))) { | ||
const url = new URL(src); | ||
if (url.hash && APP.hub!.hub_id === hubId) { | ||
label = "go to"; | ||
} else { | ||
label = "visit room"; | ||
} | ||
} | ||
textObj.text = label; | ||
}); | ||
break; | ||
case LinkType.WAYPOINT: | ||
label = "go to"; | ||
break; | ||
case LinkType.LOCAL_ROOM: | ||
case LinkType.EXTERNAL_ROOM: | ||
label = "visit room"; | ||
break; | ||
} | ||
textObj.text = label; | ||
} | ||
|
||
// Check if the cursor it hovered on Link object or Link menu button object. | ||
const hovered = hoveredQuery(world).length > 0 || hoveredMenuItemQuery(world).length > 0; | ||
function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { | ||
const target = LinkHoverMenu.targetObjectRef[menu]; | ||
const visible = !!(target && !frozen); | ||
|
||
const linkEid = LinkHoverMenu.targetObjectRef[menuEid]; | ||
if (hovered && entityExists(world, linkEid)) { | ||
// Hovered then make the menu visible and handle clicks if needed. | ||
menuObject.visible = true; | ||
clickedMenuItemQuery(world).forEach(async eid => { | ||
const linkEid = LinkHoverMenu.targetObjectRef[menuEid]; | ||
const src = APP.getString(Link.url[linkEid])!; | ||
const exitImmersive = async () => await handleExitTo2DInterstitial(false, () => {}, true); | ||
const obj = world.eid2obj.get(menu)!; | ||
obj.visible = visible; | ||
|
||
const mayChangeScene = (APP.scene?.systems as any).permissions.canOrWillIfCreator("update_hub"); | ||
let hubId; | ||
if (await isLocalHubsAvatarUrl(src)) { | ||
const avatarId = new URL(src).pathname.split("/").pop(); | ||
window.APP.store.update({ profile: { avatarId } }); | ||
APP.scene!.emit("avatar_updated"); | ||
} else if ((await isLocalHubsSceneUrl(src)) && mayChangeScene) { | ||
APP.scene!.emit("scene_media_selected", src); | ||
} else if ((hubId = await isHubsRoomUrl(src))) { | ||
const url = new URL(src); | ||
if (url.hash && APP.hub!.hub_id === hubId) { | ||
// move to waypoint w/o writing to history | ||
window.history.replaceState(null, "", window.location.href.split("#")[0] + url.hash); | ||
} else if (await isLocalHubsUrl(src)) { | ||
const waypoint = url.hash && url.hash.substring(1); | ||
// move to new room without page load or entry flow | ||
changeHub(hubId, true, waypoint); | ||
} else { | ||
await exitImmersive(); | ||
location.href = src; | ||
} | ||
} else { | ||
await exitImmersive(); | ||
window.open(src); | ||
} | ||
}); | ||
} else { | ||
// Not hovered or target object has already been deleted | ||
// then make the menu invisible and forget the Link object. | ||
menuObject.visible = false; | ||
if (menuObject.parent !== null) { | ||
menuObject.parent.remove(menuObject); | ||
const linkButtonRef = LinkHoverMenu.linkButtonRef[menu]; | ||
const buttonObj = world.eid2obj.get(linkButtonRef)!; | ||
// 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 | ||
if (visible) { | ||
if (visible !== buttonObj.visible) { | ||
updateButtonText(world, menu, linkButtonRef); | ||
buttonObj.visible = true; | ||
} | ||
LinkHoverMenu.targetObjectRef[menuEid] = NULL_EID; | ||
} else { | ||
buttonObj.visible = false; | ||
} | ||
} | ||
|
||
export function linkHoverMenuSystem(world: HubsWorld, sceneIsFrozen: boolean) { | ||
// Assumes always only single LinkHoverMenu entity exists for now. | ||
// TODO: Take into account for more than one for VR | ||
menuQuery(world).forEach(menu => { | ||
updateLinkMenuTarget(world, menu, sceneIsFrozen); | ||
if (LinkHoverMenu.targetObjectRef[menu]) { | ||
moveToTarget(world, menu); | ||
clickedMenuItemQuery(world).forEach(eid => handleLinkClick(world, eid)); | ||
} | ||
flushToObject3Ds(world, menu, sceneIsFrozen); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { defineQuery, enterQuery } from "bitecs"; | ||
import type { HubsWorld } from "../app"; | ||
import { Link } from "../bit-components"; | ||
import { isHubsRoomUrl, isLocalHubsAvatarUrl, isLocalHubsSceneUrl, isLocalHubsUrl } from "../utils/media-url-utils"; | ||
import { LinkType } from "../inflators/link"; | ||
|
||
const linkQuery = defineQuery([Link]); | ||
const linkEnterQuery = enterQuery(linkQuery); | ||
|
||
export function linkSystem(world: HubsWorld) { | ||
linkEnterQuery(world).forEach(async eid => { | ||
const mayChangeScene = (APP.scene?.systems as any).permissions.canOrWillIfCreator("update_hub"); | ||
const src = APP.getString(Link.url[eid])!; | ||
let hubId; | ||
if (await isLocalHubsAvatarUrl(src)) { | ||
Link.type[eid] = LinkType.AVATAR; | ||
} else if ((await isLocalHubsSceneUrl(src)) && mayChangeScene) { | ||
Link.type[eid] = LinkType.SCENE; | ||
} else if ((hubId = await isHubsRoomUrl(src))) { | ||
const url = new URL(src); | ||
if (url.hash && APP.hub!.hub_id === hubId) { | ||
Link.type[eid] = LinkType.WAYPOINT; | ||
} else if (await isLocalHubsUrl(this.src)) { | ||
Link.type[eid] = LinkType.LOCAL_ROOM; | ||
} else { | ||
Link.type[eid] = LinkType.EXTERNAL_ROOM; | ||
} | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters