From 3cbd8f5243a1815b90de8517688bd1de95ca1188 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 24 Jan 2024 14:23:38 +0100 Subject: [PATCH 1/3] BitECS snap object menu support --- src/assets/snap.png | Bin 0 -> 2769 bytes src/bit-components.js | 3 + .../object-menu-transform-system.ts | 6 +- src/bit-systems/pdf-menu-system.ts | 12 ++-- src/bit-systems/snap-media-system.ts | 64 ++++++++++++++++++ src/bit-systems/video-menu-system.ts | 6 +- src/hub.js | 3 +- src/inflators/pdf.ts | 1 + src/prefabs/pdf-menu.tsx | 40 +++++++++-- src/prefabs/video-menu.tsx | 40 +++++++++-- src/systems/hubs-systems.ts | 2 + src/utils/jsx-entity.ts | 2 + src/utils/load-pdf.tsx | 2 +- 13 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 src/assets/snap.png create mode 100644 src/bit-systems/snap-media-system.ts diff --git a/src/assets/snap.png b/src/assets/snap.png new file mode 100644 index 0000000000000000000000000000000000000000..71e48a87350c19fa310e8d29930599e37760761a GIT binary patch literal 2769 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 XGSA4eP7bo)00000NkvXXu0mjf-epRC literal 0 HcmV?d00001 diff --git a/src/bit-components.js b/src/bit-components.js index ad46cb8d7a..b2e11f3e57 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -278,6 +278,7 @@ export const VideoMenu = defineComponent({ headRef: Types.eid, playIndicatorRef: Types.eid, pauseIndicatorRef: Types.eid, + snapRef: Types.eid, clearTargetTimer: Types.f64 }); export const AudioEmitter = defineComponent({ @@ -352,12 +353,14 @@ export const PDFMenu = defineComponent({ prevButtonRef: Types.eid, nextButtonRef: Types.eid, pageLabelRef: Types.eid, + snapRef: Types.eid, targetRef: Types.eid, clearTargetTimer: Types.f64 }); export const ObjectMenuTarget = defineComponent({ flags: Types.ui8 }); +export const MediaSnapped = defineComponent(); export const NetworkDebug = defineComponent(); export const NetworkDebugRef = defineComponent({ ref: Types.eid diff --git a/src/bit-systems/object-menu-transform-system.ts b/src/bit-systems/object-menu-transform-system.ts index 907e1ba2a1..cfc20eadd7 100644 --- a/src/bit-systems/object-menu-transform-system.ts +++ b/src/bit-systems/object-menu-transform-system.ts @@ -17,6 +17,7 @@ const tmpMat42 = new Matrix4(); const aabb = new Box3(); const sphere = new Sphere(); const yVector = new Vector3(0, 1, 0); +const UNIT_V3 = new Vector3(1, 1, 1); // Calculate the AABB without accounting for the root object rotation function getAABB(obj: Object3D, box: Box3, onlyVisible: boolean = false) { @@ -83,8 +84,9 @@ function transformMenu(world: HubsWorld, menu: EntityID) { // For now we are defaulting to the current AFrame behavior. } else { targetObj.updateMatrices(true, true); - tmpMat4.copy(targetObj.matrixWorld); - tmpMat4.decompose(tmpVec1, tmpQuat1, tmpVec2); + targetObj.matrixWorld.decompose(tmpVec1, tmpQuat1, tmpVec2); + tmpMat42.compose(tmpVec1, tmpQuat1, UNIT_V3); + tmpMat4.copy(tmpMat42); const isFacing = isFacingCamera(targetObj); if (!isFacing) { diff --git a/src/bit-systems/pdf-menu-system.ts b/src/bit-systems/pdf-menu-system.ts index 4dcf7852fa..2197b93586 100644 --- a/src/bit-systems/pdf-menu-system.ts +++ b/src/bit-systems/pdf-menu-system.ts @@ -10,6 +10,7 @@ import { MediaLoader, MediaPDF, MediaPDFUpdated, + MediaSnapped, NetworkedPDF, ObjectMenuTransform, PDFMenu @@ -25,6 +26,7 @@ function setCursorRaycastable(world: HubsWorld, menu: EntityID, enable: boolean) change(world, CursorRaycastable, menu); change(world, CursorRaycastable, PDFMenu.prevButtonRef[menu]); change(world, CursorRaycastable, PDFMenu.nextButtonRef[menu]); + change(world, CursorRaycastable, PDFMenu.snapRef[menu]); } function clicked(world: HubsWorld, eid: EntityID) { @@ -83,6 +85,9 @@ function handleClicks(world: HubsWorld, menu: EntityID) { } else if (clicked(world, PDFMenu.prevButtonRef[menu])) { const pdf = PDFMenu.targetRef[menu]; setPage(world, pdf, MediaPDF.pageNumber[pdf] - 1); + } else if (clicked(world, PDFMenu.snapRef[menu])) { + const pdf = PDFMenu.targetRef[menu]; + addComponent(world, MediaSnapped, pdf); } } @@ -107,13 +112,6 @@ function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { 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 - // TODO: Ensure that children of invisible entities aren't raycastable - buttonObj.visible = visible; - }); - if (target) { const numPages = PDFResourcesMap.get(target)!.pdf.numPages; (world.eid2obj.get(PDFMenu.pageLabelRef[menu]) as Text).text = `${MediaPDF.pageNumber[target]} / ${numPages}`; diff --git a/src/bit-systems/snap-media-system.ts b/src/bit-systems/snap-media-system.ts new file mode 100644 index 0000000000..64b4dfce07 --- /dev/null +++ b/src/bit-systems/snap-media-system.ts @@ -0,0 +1,64 @@ +import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs"; +import { HubsWorld } from "../app"; +import { MediaPDF, MediaSnapped, MediaVideo, MediaVideoData } from "../bit-components"; +import { SOUND_CAMERA_TOOL_TOOK_SNAPSHOT } from "../systems/sound-effects-system"; +import { JobRunner } from "../utils/coroutine-utils"; +import { EntityID } from "../utils/networking-types"; +import { PDFResourcesMap } from "./pdf-system"; +import { spawnFromFileList } from "../load-media-on-paste-or-drop"; + +const TYPE_IMG_PNG = { type: "image/png" }; + +export function* snapMedia(world: HubsWorld, eid: EntityID) { + let canvas: HTMLCanvasElement | undefined; + if (hasComponent(world, MediaPDF, eid)) { + const res = PDFResourcesMap.get(eid); + canvas = res?.canvas; + } else if (hasComponent(world, MediaVideo, eid)) { + const video = MediaVideoData.get(eid)!; + if (video) { + canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext("2d")!.drawImage(video, 0, 0, canvas.width, canvas.height); + } + } + + if (canvas) { + const blob = new Promise((resolve, reject) => { + if (canvas) { + canvas.toBlob(resolve); + } else { + reject(); + } + }); + if (blob) { + const file = new File([yield blob], "snap.png", TYPE_IMG_PNG); + spawnFromFileList([file] as any); + } else { + console.error("Snapped image creation error"); + } + } + + if (entityExists(world, eid)) { + removeComponent(world, MediaSnapped, eid); + } +} + +const jobs = new JobRunner(); +const snappedMediaQuery = defineQuery([MediaSnapped]); +const snappedEnterQuery = enterQuery(snappedMediaQuery); +const snappedExitQuery = exitQuery(snappedMediaQuery); +export function snapMediaSystem(world: HubsWorld, sfxSystem: any) { + snappedExitQuery(world).forEach(eid => { + jobs.stop(eid); + }); + snappedEnterQuery(world).forEach(eid => { + sfxSystem.playSoundOneShot(SOUND_CAMERA_TOOL_TOOK_SNAPSHOT); + + if (!jobs.has(eid)) { + jobs.add(eid, () => snapMedia(world, eid)); + } + }); + jobs.tick(); +} diff --git a/src/bit-systems/video-menu-system.ts b/src/bit-systems/video-menu-system.ts index 4588cb4f82..12e8d2dbab 100644 --- a/src/bit-systems/video-menu-system.ts +++ b/src/bit-systems/video-menu-system.ts @@ -12,6 +12,7 @@ import { HoveredRemoteRight, Interacted, MediaLoader, + MediaSnapped, MediaVideo, MediaVideoData, MediaVideoUpdated, @@ -38,6 +39,7 @@ function setCursorRaycastable(world: HubsWorld, menu: number, enable: boolean) { change(world, CursorRaycastable, VideoMenu.trackRef[menu]); change(world, CursorRaycastable, VideoMenu.playIndicatorRef[menu]); change(world, CursorRaycastable, VideoMenu.pauseIndicatorRef[menu]); + change(world, CursorRaycastable, VideoMenu.snapRef[menu]); } const intersectInThePlaneOf = (() => { @@ -113,7 +115,6 @@ function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { } else { obj.removeFromParent(); setCursorRaycastable(world, menu, false); - ObjectMenuTransform.flags[menu] &= ~ObjectMenuTransformFlags.Enabled; } } @@ -142,6 +143,9 @@ function handleClicks(world: HubsWorld, menu: EntityID) { addComponent(world, EntityStateDirty, videoEid); } addComponent(world, MediaVideoUpdated, videoEid); + } else if (clicked(world, VideoMenu.snapRef[menu])) { + const video = VideoMenu.videoRef[menu]; + addComponent(world, MediaSnapped, video); } } diff --git a/src/hub.js b/src/hub.js index 8665309af7..fb609a6b7e 100644 --- a/src/hub.js +++ b/src/hub.js @@ -192,6 +192,7 @@ import { renderAsEntity } from "./utils/jsx-entity"; import { VideoMenuPrefab, loadVideoMenuButtonIcons } from "./prefabs/video-menu"; import { loadObjectMenuButtonIcons, ObjectMenuPrefab } from "./prefabs/object-menu"; import { loadMirrorMenuButtonIcons, MirrorMenuPrefab } from "./prefabs/mirror-menu"; +import { loadPDFMenuButtonIcons } from "./prefabs/pdf-menu"; import { LinkHoverMenuPrefab } from "./prefabs/link-hover-menu"; import { PDFMenuPrefab } from "./prefabs/pdf-menu"; import { loadWaypointPreviewModel, WaypointPreview } from "./prefabs/waypoint-preview"; @@ -206,7 +207,7 @@ function addToScene(entityDef, visible) { obj.visible = !!visible; }); } -preload(addToScene(PDFMenuPrefab(), false)); +preload(loadPDFMenuButtonIcons().then(() => addToScene(PDFMenuPrefab(), false))); preload(loadObjectMenuButtonIcons().then(() => addToScene(ObjectMenuPrefab(), false))); preload(loadMirrorMenuButtonIcons().then(() => addToScene(MirrorMenuPrefab(), false))); preload(addToScene(LinkHoverMenuPrefab(), false)); diff --git a/src/inflators/pdf.ts b/src/inflators/pdf.ts index bb5d3d219d..f4af9e0763 100644 --- a/src/inflators/pdf.ts +++ b/src/inflators/pdf.ts @@ -11,6 +11,7 @@ import { createPlaneBufferGeometry } from "../utils/three-utils"; export interface PDFResources { pdf: PDFDocumentProxy; material: MeshBasicMaterial; + canvas: HTMLCanvasElement; canvasContext: CanvasRenderingContext2D; } diff --git a/src/prefabs/pdf-menu.tsx b/src/prefabs/pdf-menu.tsx index 43e722691e..ee24504a2d 100644 --- a/src/prefabs/pdf-menu.tsx +++ b/src/prefabs/pdf-menu.tsx @@ -3,10 +3,17 @@ import { Color } from "three"; import { ArrayVec3, Attrs, createElementEntity, createRef } from "../utils/jsx-entity"; import { Button3D, BUTTON_TYPES } from "./button3D"; import { Label } from "./camera-tool"; +import { loadTexture, loadTextureFromCache } from "../utils/load-texture"; +import snapIconSrc from "../assets/spawn_message.png"; const BUTTON_HEIGHT = 0.2; -const BUTTON_SCALE: ArrayVec3 = [0.4, 0.4, 0.4]; -const BUTTON_WIDTH = 0.3; +const BUTTON_SCALE: ArrayVec3 = [0.6, 0.6, 0.6]; +const BIG_BUTTON_SCALE: ArrayVec3 = [0.8, 0.8, 0.8]; +const BUTTON_WIDTH = 0.2; + +export async function loadPDFMenuButtonIcons() { + return Promise.all([loadTexture(snapIconSrc, 1, "image/png")]); +} interface PDFPageButtonProps extends Attrs { text: string; @@ -16,7 +23,7 @@ function PDFPageButton(props: PDFPageButtonProps) { return ( + ); +} + const UI_Z = 0.001; -const POSITION_PREV: ArrayVec3 = [-0.45, 0.0, UI_Z]; -const POSITION_NEXT: ArrayVec3 = [0.45, 0.0, UI_Z]; -const POSITION_LABEL: ArrayVec3 = [0.0, -0.35, UI_Z]; +const POSITION_PREV: ArrayVec3 = [-0.35, 0.0, UI_Z]; +const POSITION_NEXT: ArrayVec3 = [0.35, 0.0, UI_Z]; +const POSITION_LABEL: ArrayVec3 = [0.0, -0.45, UI_Z]; +const POSITION_SNAP: ArrayVec3 = [0.0, 0.45, UI_Z]; const PAGE_LABEL_COLOR = new Color(0.1, 0.1, 0.1); export function PDFMenuPrefab() { const refPrev = createRef(); const refNext = createRef(); const refLabel = createRef(); + const refSnap = createRef(); return ( +