From ea184ba448cfd770fc366e998c86357ca1068628 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:23:06 +1000 Subject: [PATCH] More reffactors --- src/vr180player/vr180-player.ts | 6 +- src/vr180player/xr/input-mode.ts | 52 ++ src/vr180player/xr/input-rig.ts | 445 +++++++++++ src/vr180player/xr/pointer-overlays.ts | 328 ++++++++ .../xr/vr-controller-interactions.ts | 707 +----------------- 5 files changed, 828 insertions(+), 710 deletions(-) create mode 100644 src/vr180player/xr/input-mode.ts create mode 100644 src/vr180player/xr/input-rig.ts create mode 100644 src/vr180player/xr/pointer-overlays.ts diff --git a/src/vr180player/vr180-player.ts b/src/vr180player/vr180-player.ts index b30e645..eb2ce56 100644 --- a/src/vr180player/vr180-player.ts +++ b/src/vr180player/vr180-player.ts @@ -18,10 +18,8 @@ import { import { createMediaTexture as createMediaTextureCore } from './rendering/three-utils.js'; import { FallbackCameraControls } from './modes/fallback-camera-controls.js'; import { MediaController } from './media/media-controller.js'; -import { - createVrInputRig, - handleVrControllerSelect -} from './xr/vr-controller-interactions.js'; +import { createVrInputRig } from './xr/input-rig.js'; +import { handleVrControllerSelect } from './xr/vr-controller-interactions.js'; import { bindVideoEvents } from './media/video-events.js'; import { createVrControlPanel, diff --git a/src/vr180player/xr/input-mode.ts b/src/vr180player/xr/input-mode.ts new file mode 100644 index 0000000..e8d14b7 --- /dev/null +++ b/src/vr180player/xr/input-mode.ts @@ -0,0 +1,52 @@ +export type PointerInputMode = 'controller' | 'hand'; + +export type PointerInputModeCarrier = { + controller?: { + userData?: any; + }; + pointerInputMode?: PointerInputMode; +}; + +export function rememberPointerInputMode( + inputSource: PointerInputModeCarrier, + event: any, + fallbackMode: PointerInputMode +): void { + const eventInputSource = event?.data?.inputSource || event?.inputSource || event?.data; + const nextMode = getPointerInputMode(eventInputSource) || fallbackMode; + inputSource.pointerInputMode = nextMode; + + if (!inputSource.controller) { + return; + } + + inputSource.controller.userData = { + ...inputSource.controller.userData, + vrwpInputSource: inputSource + }; +} + +export function getPointerInputMode(eventInputSource: any): PointerInputMode | null { + if (!eventInputSource) { + return null; + } + + if (eventInputSource.hand) { + return 'hand'; + } + + if (Array.isArray(eventInputSource.profiles) && + eventInputSource.profiles.some((profile: string) => profile.toLowerCase().includes('hand'))) { + return 'hand'; + } + + if (eventInputSource.gamepad || eventInputSource.targetRayMode === 'tracked-pointer') { + return 'controller'; + } + + return null; +} + +export function shouldUseHandPointer(inputSource: PointerInputModeCarrier | undefined): boolean { + return inputSource?.pointerInputMode === 'hand'; +} diff --git a/src/vr180player/xr/input-rig.ts b/src/vr180player/xr/input-rig.ts new file mode 100644 index 0000000..f32c33b --- /dev/null +++ b/src/vr180player/xr/input-rig.ts @@ -0,0 +1,445 @@ +import * as THREE from 'https://unpkg.com/three/build/three.module.js'; +import { + getSeekProgressFromIntersection, + type VrControlPanel +} from './vr-control-panel.js'; +import { + beginPalmAimSelection, + computePalmAimRay, + createPalmAimLatch, + endPalmAimSelection, + getPalmAimSelectionRay, + recordStablePalmAimRay, + type PalmAimLatch, + type PalmAimRay, + type VectorLike +} from './hand-aim.js'; +import { + rememberPointerInputMode, + shouldUseHandPointer, + type PointerInputMode +} from './input-mode.js'; +import { + bindOverlayActivity, + createControllerOverlay, + createHandOverlay, + createPointerOverlay, + createWorldPointerOverlay, + POINTER_HIT_SURFACE_OFFSET, + POINTER_LENGTH, + POINTER_MIN_LENGTH, + setPointerOverlayLength, + VrOverlayVisibility +} from './pointer-overlays.js'; + +export type VrInputRig = { + beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => void; + hideOverlays: () => void; + raycaster: any; + showOverlays: (timestamp?: number) => void; + update: (timestamp: number, hoverTargets?: any[]) => boolean; +}; + +type AimRay = { + direction: any; + origin: any; +}; + +type ActiveSeekDrag = { + inputSource: VrInputSource; + onSeek: (progress: number) => void; + panel: VrControlPanel; +}; + +type HandPointerOverlay = { + fallbackPointerOverlay: any; + hand: any; + handAimLatch: PalmAimLatch; + inputSource: VrInputSource; + pointerOverlay: any; +}; + +type VrInputSource = { + controller: any; + controllerPointerOverlay: any; + hand?: any; + handAimLatch?: PalmAimLatch; + handPointerOverlay?: any; + pointerInputMode: PointerInputMode; +}; + +const DEFAULT_RAY_DIRECTION = new THREE.Vector3(0, 0, -1); +const tempMatrix = new THREE.Matrix4(); + +export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig { + const overlayVisibility = new VrOverlayVisibility(); + const handPointerOverlays: HandPointerOverlay[] = []; + const inputSources: VrInputSource[] = []; + const raycaster = createPointerRaycaster(); + const hoverRaycaster = createPointerRaycaster(); + const dragRaycaster = createPointerRaycaster(); + let activeSeekDrag: ActiveSeekDrag | null = null; + + for (let index = 0; index < 2; index += 1) { + const controller = renderer.xr.getController(index); + const controllerPointerOverlay = createPointerOverlay(index, overlayVisibility); + const inputSource: VrInputSource = { + controller, + controllerPointerOverlay, + pointerInputMode: 'controller' + }; + controller.userData = { + ...controller.userData, + vrwpInputSource: inputSource + }; + inputSources.push(inputSource); + + controller.addEventListener('connected', (event: any) => { + rememberPointerInputMode(inputSource, event, 'controller'); + }); + controller.addEventListener('selectstart', (event: any) => { + const timestamp = getEventTimestamp(event); + rememberPointerInputMode(inputSource, event, inputSource.pointerInputMode); + if (shouldUseHandPointer(inputSource)) { + beginPalmAimSelection(controller.userData?.vrwpHandAimLatch, timestamp); + } + overlayVisibility.show(timestamp); + onSelectStart(event); + }); + controller.addEventListener('selectend', () => { + endPalmAimSelection(controller.userData?.vrwpHandAimLatch); + if (activeSeekDrag?.inputSource.controller === controller) { + activeSeekDrag = null; + } + }); + controller.addEventListener('select', () => { + endPalmAimSelection(controller.userData?.vrwpHandAimLatch); + }); + bindOverlayActivity(controller, overlayVisibility); + controller.add(controllerPointerOverlay); + scene.add(controller); + + const grip = renderer.xr.getControllerGrip?.(index); + if (grip) { + grip.addEventListener('connected', (event: any) => { + rememberPointerInputMode(inputSource, event, 'controller'); + }); + bindOverlayActivity(grip, overlayVisibility); + grip.add(createControllerOverlay(index, overlayVisibility)); + scene.add(grip); + } + + const hand = renderer.xr.getHand?.(index); + if (hand) { + const handAimLatch = createPalmAimLatch(); + inputSource.hand = hand; + inputSource.handAimLatch = handAimLatch; + controller.userData = { + ...controller.userData, + vrwpHand: hand, + vrwpHandAimLatch: handAimLatch + }; + hand.userData = { + ...hand.userData, + vrwpAimLatch: handAimLatch + }; + bindOverlayActivity(hand, overlayVisibility); + rememberHandedness(hand, { data: hand.inputState }); + createHandOverlay(hand, index, overlayVisibility); + hand.addEventListener?.('connected', (event: any) => { + rememberPointerInputMode(inputSource, event, 'hand'); + rememberHandedness(hand, event); + createHandOverlay(hand, index, overlayVisibility); + overlayVisibility.show(); + }); + scene.add(hand); + + const handPointerOverlay = createWorldPointerOverlay(index, overlayVisibility); + inputSource.handPointerOverlay = handPointerOverlay; + scene.add(handPointerOverlay); + handPointerOverlays.push({ + fallbackPointerOverlay: controllerPointerOverlay, + hand, + handAimLatch, + inputSource, + pointerOverlay: handPointerOverlay + }); + } + } + + overlayVisibility.hideImmediately(); + + return { + beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => { + const inputSource = getInputSourceByController(inputSources, controller); + if (!inputSource || !panel?.seekBarHitAreaMesh) { + activeSeekDrag = null; + return; + } + + activeSeekDrag = { inputSource, onSeek, panel }; + }, + hideOverlays: () => overlayVisibility.hideImmediately(), + raycaster, + showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp), + update: (timestamp: number, hoverTargets: any[] = []) => { + updateHandPointerOverlays(handPointerOverlays, timestamp); + updateActiveSeekDrag(activeSeekDrag, dragRaycaster, timestamp); + const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster, timestamp); + if (isHovering) { + overlayVisibility.show(timestamp); + } + overlayVisibility.update(timestamp); + return isHovering; + } + }; +} + +function createPointerRaycaster(): any { + const raycaster = new THREE.Raycaster(); + raycaster.near = 0.1; + raycaster.far = POINTER_LENGTH; + return raycaster; +} + +function getInputSourceByController(inputSources: VrInputSource[], controller: any): VrInputSource | undefined { + return inputSources.find((inputSource) => inputSource.controller === controller); +} + +function updateInputPointerIntersections( + inputSources: VrInputSource[], + hoverTargets: any[], + hoverRaycaster: any, + timestamp: number +): boolean { + let isHoveringAnyTarget = false; + + inputSources.forEach((inputSource) => { + resetInputPointerLengths(inputSource); + const aimRay = getInputSourceAimRay(inputSource, timestamp); + const pointerOverlay = getActivePointerOverlay(inputSource); + if (!aimRay || !pointerOverlay || hoverTargets.length === 0) { + return; + } + + hoverRaycaster.ray.origin.copy(aimRay.origin); + hoverRaycaster.ray.direction.copy(aimRay.direction); + const intersections = hoverRaycaster.intersectObjects(hoverTargets, true); + if (intersections.length === 0) { + return; + } + + isHoveringAnyTarget = true; + setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance)); + }); + + return isHoveringAnyTarget; +} + +function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void { + if (!activeSeekDrag?.panel.seekBarHitAreaMesh) { + return; + } + + const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true }); + if (!aimRay) { + return; + } + + dragRaycaster.ray.origin.copy(aimRay.origin); + dragRaycaster.ray.direction.copy(aimRay.direction); + const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true); + if (intersections.length === 0) { + return; + } + + activeSeekDrag.onSeek(getSeekProgressFromIntersection(activeSeekDrag.panel, intersections[0].point)); +} + +function getInputSourceAimRay( + inputSource: VrInputSource, + timestamp: number, + { preferLiveHandAim = false }: { preferLiveHandAim?: boolean } = {} +): AimRay | null { + if (shouldUseHandPointer(inputSource) && inputSource.hand && inputSource.handAimLatch) { + if (preferLiveHandAim) { + const handRay = getHandAimRay(inputSource.hand); + if (handRay) { + return handRay; + } + } + + const latchedRay = getPalmAimSelectionRay(inputSource.handAimLatch, timestamp); + if (latchedRay) { + return toAimRay(latchedRay); + } + + const handRay = getHandAimRay(inputSource.hand); + if (handRay) { + return handRay; + } + } + + return getControllerAimRay(inputSource.controller); +} + +function getControllerAimRay(controller: any): AimRay | null { + if (!controller) { + return null; + } + + controller.updateMatrixWorld(); + tempMatrix.identity().extractRotation(controller.matrixWorld); + const origin = new THREE.Vector3().setFromMatrixPosition(controller.matrixWorld); + const direction = new THREE.Vector3(0, 0, -1).applyMatrix4(tempMatrix); + return { direction, origin }; +} + +function resetInputPointerLengths(inputSource: VrInputSource): void { + setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH); + if (inputSource.handPointerOverlay) { + setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH); + } +} + +function getActivePointerOverlay(inputSource: VrInputSource): any { + if (inputSource.handPointerOverlay && inputSource.handPointerOverlay.userData?.vrwpOverlayAvailable !== false) { + return inputSource.handPointerOverlay; + } + + if (inputSource.controllerPointerOverlay && inputSource.controllerPointerOverlay.userData?.vrwpOverlayAvailable !== false) { + return inputSource.controllerPointerOverlay; + } + + return null; +} + +function getPointerIntersectionLength(distance: number): number { + return Math.max( + POINTER_MIN_LENGTH, + Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET) + ); +} + +function updateHandPointerOverlays(handPointerOverlays: HandPointerOverlay[], timestamp: number): void { + handPointerOverlays.forEach(({ fallbackPointerOverlay, hand, handAimLatch, inputSource, pointerOverlay }) => { + if (!shouldUseHandPointer(inputSource)) { + pointerOverlay.userData = { + ...pointerOverlay.userData, + vrwpOverlayAvailable: false + }; + fallbackPointerOverlay.userData = { + ...fallbackPointerOverlay.userData, + vrwpOverlayAvailable: true + }; + return; + } + + const currentHandRay = getHandAimRay(hand); + if (currentHandRay) { + recordStablePalmAimRay(handAimLatch, toPalmAimRay(currentHandRay), timestamp); + } + + const selectionPalmRay = getPalmAimSelectionRay(handAimLatch, timestamp); + const displayHandRay = selectionPalmRay ? toAimRay(selectionPalmRay) : currentHandRay; + const hasHandRay = Boolean(displayHandRay); + pointerOverlay.userData = { + ...pointerOverlay.userData, + vrwpOverlayAvailable: hasHandRay + }; + fallbackPointerOverlay.userData = { + ...fallbackPointerOverlay.userData, + vrwpOverlayAvailable: !hasHandRay + }; + + if (!displayHandRay) { + return; + } + + pointerOverlay.position.copy(displayHandRay.origin); + pointerOverlay.quaternion.setFromUnitVectors(DEFAULT_RAY_DIRECTION, displayHandRay.direction); + }); +} + +function getHandAimRay(hand: any): AimRay | null { + const joints = hand?.joints; + if (!joints) { + return null; + } + + const palmAimRay = computePalmAimRay({ + handedness: getHandedness(hand), + indexMetacarpal: getJointWorldPosition(joints['index-finger-metacarpal']), + middleMetacarpal: getJointWorldPosition(joints['middle-finger-metacarpal']), + pinkyMetacarpal: getJointWorldPosition(joints['pinky-finger-metacarpal']), + ringMetacarpal: getJointWorldPosition(joints['ring-finger-metacarpal']), + wrist: getJointWorldPosition(joints.wrist) + }); + if (!palmAimRay) { + return null; + } + + const origin = toThreeVector(palmAimRay.origin); + const direction = toThreeVector(palmAimRay.direction); + return { direction, origin }; +} + +function toPalmAimRay(ray: AimRay): PalmAimRay { + return { + direction: fromThreeVector(ray.direction), + origin: fromThreeVector(ray.origin) + }; +} + +function toAimRay(ray: PalmAimRay): AimRay { + return { + direction: toThreeVector(ray.direction), + origin: toThreeVector(ray.origin) + }; +} + +function rememberHandedness(hand: any, event: any): void { + const handedness = event?.data?.handedness || + event?.data?.inputSource?.handedness || + hand?.inputState?.handedness; + + if (handedness !== 'left' && handedness !== 'right') { + return; + } + + hand.userData = { + ...hand.userData, + vrwpHandedness: handedness + }; +} + +function getHandedness(hand: any): string | undefined { + return hand?.userData?.vrwpHandedness || + hand?.inputState?.handedness || + hand?.userData?.inputSource?.handedness; +} + +function getJointWorldPosition(joint: any): VectorLike | null { + if (!joint?.getWorldPosition) { + return null; + } + + joint.updateMatrixWorld?.(true); + return joint.getWorldPosition(new THREE.Vector3()); +} + +function toThreeVector(vector: VectorLike): any { + return new THREE.Vector3(vector.x, vector.y, vector.z); +} + +function fromThreeVector(vector: any): VectorLike { + return { + x: vector.x, + y: vector.y, + z: vector.z + }; +} + +function getEventTimestamp(event: any): number { + return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now(); +} diff --git a/src/vr180player/xr/pointer-overlays.ts b/src/vr180player/xr/pointer-overlays.ts new file mode 100644 index 0000000..a9db375 --- /dev/null +++ b/src/vr180player/xr/pointer-overlays.ts @@ -0,0 +1,328 @@ +import * as THREE from 'https://unpkg.com/three/build/three.module.js'; + +export type PointerOverlayInputSource = { + controllerPointerOverlay: any; + handPointerOverlay?: any; +}; + +export type VrOverlayVisibilityOptions = { + fadeDurationMs?: number; + hideDelayMs?: number; +}; + +export const INPUT_OVERLAY_HIDE_DELAY_MS = 2500; +export const INPUT_OVERLAY_FADE_DURATION_MS = 200; +export const INPUT_OVERLAY_RENDER_ORDER = 10000; +export const POINTER_LENGTH = 5; +export const POINTER_MIN_LENGTH = 0.06; +export const POINTER_HIT_SURFACE_OFFSET = 0.015; + +const HAND_JOINT_NAMES = [ + 'wrist', + 'thumb-metacarpal', + 'thumb-phalanx-proximal', + 'thumb-phalanx-distal', + 'thumb-tip', + 'index-finger-metacarpal', + 'index-finger-phalanx-proximal', + 'index-finger-phalanx-intermediate', + 'index-finger-phalanx-distal', + 'index-finger-tip', + 'middle-finger-metacarpal', + 'middle-finger-phalanx-proximal', + 'middle-finger-phalanx-intermediate', + 'middle-finger-phalanx-distal', + 'middle-finger-tip', + 'ring-finger-metacarpal', + 'ring-finger-phalanx-proximal', + 'ring-finger-phalanx-intermediate', + 'ring-finger-phalanx-distal', + 'ring-finger-tip', + 'pinky-finger-metacarpal', + 'pinky-finger-phalanx-proximal', + 'pinky-finger-phalanx-intermediate', + 'pinky-finger-phalanx-distal', + 'pinky-finger-tip' +]; + +export class VrOverlayVisibility { + private readonly fadeDurationMs: number; + private readonly hideDelayMs: number; + private readonly objects: any[] = []; + private opacity = 0; + private targetOpacity = 0; + private visibleUntil = 0; + + constructor({ + fadeDurationMs = INPUT_OVERLAY_FADE_DURATION_MS, + hideDelayMs = INPUT_OVERLAY_HIDE_DELAY_MS + }: VrOverlayVisibilityOptions = {}) { + this.fadeDurationMs = fadeDurationMs; + this.hideDelayMs = hideDelayMs; + } + + register(object: any): void { + this.objects.push(object); + this.setObjectVisible(object, this.targetOpacity > 0 || this.opacity > 0.001); + this.setObjectOpacity(object, this.opacity); + } + + show(timestamp = performance.now()): void { + this.visibleUntil = timestamp + this.hideDelayMs; + this.targetOpacity = 1; + this.objects.forEach((object) => this.setObjectVisible(object, true)); + } + + hideImmediately(): void { + this.visibleUntil = 0; + this.opacity = 0; + this.targetOpacity = 0; + this.objects.forEach((object) => { + this.setObjectOpacity(object, 0); + this.setObjectVisible(object, false); + }); + } + + update(timestamp: number): void { + if (this.targetOpacity > 0 && timestamp >= this.visibleUntil) { + this.targetOpacity = 0; + } + + const shouldBeVisible = this.targetOpacity > 0 || this.opacity > 0.001; + this.objects.forEach((object) => this.setObjectVisible(object, shouldBeVisible)); + + if (this.opacity === this.targetOpacity) { + return; + } + + const fadeStep = this.fadeDurationMs <= 0 + ? 1 + : Math.min(1, 16.67 / this.fadeDurationMs); + const direction = this.opacity < this.targetOpacity ? 1 : -1; + this.opacity = Math.max(0, Math.min(1, this.opacity + fadeStep * direction)); + + if ((direction > 0 && this.opacity >= this.targetOpacity) || (direction < 0 && this.opacity <= this.targetOpacity)) { + this.opacity = this.targetOpacity; + } + + this.objects.forEach((object) => { + this.setObjectOpacity(object, this.opacity); + this.setObjectVisible(object, this.opacity > 0.001); + }); + } + + private setObjectVisible(object: any, isVisible: boolean): void { + const objectVisible = isVisible && object.userData?.vrwpOverlayAvailable !== false; + object.visible = objectVisible; + object.traverse?.((child: any) => { + child.visible = objectVisible; + }); + } + + private setObjectOpacity(object: any, opacity: number): void { + object.traverse?.((child: any) => { + const materials = Array.isArray(child.material) ? child.material : [child.material]; + materials.filter(Boolean).forEach((material: any) => { + material.opacity = opacity; + material.transparent = true; + material.depthTest = false; + material.depthWrite = false; + material.needsUpdate = true; + }); + }); + } +} + +export function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVisibility): void { + [ + 'connected', + 'disconnected', + 'select', + 'selectend', + 'squeezestart', + 'squeeze', + 'squeezeend', + 'pinchstart', + 'pinchend' + ].forEach((eventName) => { + target.addEventListener?.(eventName, () => overlayVisibility.show()); + }); +} + +export function createPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { + const group = new THREE.Group(); + group.name = `vrPointerOverlay${index}`; + + const pointerMaterial = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.75); + const pointerGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -POINTER_LENGTH) + ]); + const pointerLine = new THREE.Line(pointerGeometry, pointerMaterial); + pointerLine.name = `vrPointerRay${index}`; + pointerLine.renderOrder = INPUT_OVERLAY_RENDER_ORDER; + group.add(pointerLine); + + const tipMaterial = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.9); + const tipMesh = new THREE.Mesh(new THREE.SphereGeometry(0.018, 12, 8), tipMaterial); + tipMesh.name = `vrPointerTip${index}`; + tipMesh.position.z = -POINTER_LENGTH; + tipMesh.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1; + group.add(tipMesh); + + group.userData = { + ...group.userData, + vrwpPointerLength: POINTER_LENGTH, + vrwpPointerLine: pointerLine, + vrwpPointerTip: tipMesh + }; + + overlayVisibility.register(group); + return group; +} + +export function createWorldPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { + const group = createPointerOverlay(index, overlayVisibility); + group.name = `vrHandPointerOverlay${index}`; + group.userData = { + ...group.userData, + vrwpOverlayAvailable: false + }; + return group; +} + +export function createControllerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { + const group = new THREE.Group(); + group.name = `vrControllerOverlay${index}`; + + const material = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.8); + const outlineGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(-0.045, -0.025, -0.08), + new THREE.Vector3(0.045, -0.025, -0.08), + new THREE.Vector3(0.055, 0.025, -0.02), + new THREE.Vector3(0.025, 0.035, 0.05), + new THREE.Vector3(-0.025, 0.035, 0.05), + new THREE.Vector3(-0.055, 0.025, -0.02), + new THREE.Vector3(-0.045, -0.025, -0.08) + ]); + const outline = new THREE.Line(outlineGeometry, material); + outline.name = `vrControllerOutline${index}`; + outline.renderOrder = INPUT_OVERLAY_RENDER_ORDER; + group.add(outline); + + const originMaterial = createOverlayMeshMaterial(0xffffff, 0.75); + const origin = new THREE.Mesh(new THREE.SphereGeometry(0.012, 10, 6), originMaterial); + origin.name = `vrControllerOrigin${index}`; + origin.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1; + group.add(origin); + + overlayVisibility.register(group); + return group; +} + +export function createHandOverlay(hand: any, index: number, overlayVisibility: VrOverlayVisibility): void { + const joints = getHandJoints(hand); + if (joints.length === 0) { + return; + } + + const material = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.85); + joints.forEach(({ joint, name }) => { + if (!joint || joint.userData?.vrwpHandOverlayMarker) { + return; + } + + const isTip = name.endsWith('tip'); + const isWrist = name === 'wrist'; + const radius = isWrist ? 0.014 : isTip ? 0.011 : 0.008; + const marker = new THREE.Mesh(new THREE.SphereGeometry(radius, 10, 6), material); + marker.name = `vrHandJointOverlay${index}-${name}`; + marker.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 2; + marker.frustumCulled = false; + joint.add(marker); + joint.userData = { + ...joint.userData, + vrwpHandOverlayMarker: marker + }; + overlayVisibility.register(marker); + }); +} + +export function resetInputPointerLengths(inputSource: PointerOverlayInputSource): void { + setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH); + if (inputSource.handPointerOverlay) { + setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH); + } +} + +export function getPointerIntersectionLength(distance: number): number { + return Math.max( + POINTER_MIN_LENGTH, + Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET) + ); +} + +export function setPointerOverlayLength(pointerOverlay: any, length: number): void { + if (!pointerOverlay || pointerOverlay.userData?.vrwpPointerLength === length) { + return; + } + + const pointerLine = pointerOverlay.userData?.vrwpPointerLine; + const pointerTip = pointerOverlay.userData?.vrwpPointerTip; + const positionAttribute = pointerLine?.geometry?.getAttribute?.('position') || + pointerLine?.geometry?.attributes?.position; + + if (positionAttribute?.setXYZ) { + positionAttribute.setXYZ(1, 0, 0, -length); + positionAttribute.needsUpdate = true; + pointerLine.geometry.computeBoundingSphere?.(); + } + + if (pointerTip) { + pointerTip.position.z = -length; + } + + pointerOverlay.userData = { + ...pointerOverlay.userData, + vrwpPointerLength: length + }; +} + +export function createOverlayLineMaterial(color: number, opacity: number): any { + return new THREE.LineBasicMaterial({ + color, + depthTest: false, + depthWrite: false, + opacity, + transparent: true + }); +} + +export function createOverlayMeshMaterial(color: number, opacity: number): any { + return new THREE.MeshBasicMaterial({ + color, + depthTest: false, + depthWrite: false, + opacity, + transparent: true + }); +} + +function getHandJoints(hand: any): Array<{ joint: any; name: string }> { + const joints = hand?.joints; + if (!joints) { + return []; + } + + const namedJoints = HAND_JOINT_NAMES + .map((name) => ({ joint: joints[name], name })) + .filter(({ joint }) => Boolean(joint)); + + if (namedJoints.length > 0) { + return namedJoints; + } + + return Object.entries(joints) + .map(([name, joint]) => ({ joint, name })) + .filter(({ joint }) => Boolean(joint)); +} diff --git a/src/vr180player/xr/vr-controller-interactions.ts b/src/vr180player/xr/vr-controller-interactions.ts index 9db7f1b..92609e1 100644 --- a/src/vr180player/xr/vr-controller-interactions.ts +++ b/src/vr180player/xr/vr-controller-interactions.ts @@ -4,16 +4,12 @@ import { type VrControlPanel } from './vr-control-panel.js'; import { - beginPalmAimSelection, computePalmAimRay, - createPalmAimLatch, - endPalmAimSelection, getPalmAimSelectionRay, - recordStablePalmAimRay, - type PalmAimLatch, type PalmAimRay, type VectorLike } from './hand-aim.js'; +import { shouldUseHandPointer } from './input-mode.js'; type VrControllerSelectionOptions = { beginSeekDrag?: (controller: any) => void; @@ -32,214 +28,13 @@ type VrControllerSelectionOptions = { vrPanel: VrControlPanel | undefined; }; -type VrInputRig = { - beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => void; - hideOverlays: () => void; - raycaster: any; - showOverlays: (timestamp?: number) => void; - update: (timestamp: number, hoverTargets?: any[]) => boolean; -}; - type AimRay = { direction: any; origin: any; }; -type ActiveSeekDrag = { - inputSource: VrInputSource; - onSeek: (progress: number) => void; - panel: VrControlPanel; -}; - -type HandPointerOverlay = { - fallbackPointerOverlay: any; - hand: any; - handAimLatch: PalmAimLatch; - inputSource: VrInputSource; - pointerOverlay: any; -}; - -type PointerInputMode = 'controller' | 'hand'; - -type VrInputSource = { - controller: any; - controllerPointerOverlay: any; - hand?: any; - handAimLatch?: PalmAimLatch; - handPointerOverlay?: any; - pointerInputMode: PointerInputMode; -}; - -type VrOverlayVisibilityOptions = { - fadeDurationMs?: number; - hideDelayMs?: number; -}; - -const INPUT_OVERLAY_HIDE_DELAY_MS = 2500; -const INPUT_OVERLAY_FADE_DURATION_MS = 200; -const INPUT_OVERLAY_RENDER_ORDER = 10000; -const POINTER_LENGTH = 5; -const POINTER_MIN_LENGTH = 0.06; -const POINTER_HIT_SURFACE_OFFSET = 0.015; -const HAND_JOINT_NAMES = [ - 'wrist', - 'thumb-metacarpal', - 'thumb-phalanx-proximal', - 'thumb-phalanx-distal', - 'thumb-tip', - 'index-finger-metacarpal', - 'index-finger-phalanx-proximal', - 'index-finger-phalanx-intermediate', - 'index-finger-phalanx-distal', - 'index-finger-tip', - 'middle-finger-metacarpal', - 'middle-finger-phalanx-proximal', - 'middle-finger-phalanx-intermediate', - 'middle-finger-phalanx-distal', - 'middle-finger-tip', - 'ring-finger-metacarpal', - 'ring-finger-phalanx-proximal', - 'ring-finger-phalanx-intermediate', - 'ring-finger-phalanx-distal', - 'ring-finger-tip', - 'pinky-finger-metacarpal', - 'pinky-finger-phalanx-proximal', - 'pinky-finger-phalanx-intermediate', - 'pinky-finger-phalanx-distal', - 'pinky-finger-tip' -]; -const DEFAULT_RAY_DIRECTION = new THREE.Vector3(0, 0, -1); const tempMatrix = new THREE.Matrix4(); -export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig { - const overlayVisibility = new VrOverlayVisibility(); - const handPointerOverlays: HandPointerOverlay[] = []; - const inputSources: VrInputSource[] = []; - const raycaster = new THREE.Raycaster(); - raycaster.near = 0.1; - raycaster.far = POINTER_LENGTH; - const hoverRaycaster = new THREE.Raycaster(); - hoverRaycaster.near = 0.1; - hoverRaycaster.far = POINTER_LENGTH; - const dragRaycaster = new THREE.Raycaster(); - dragRaycaster.near = 0.1; - dragRaycaster.far = POINTER_LENGTH; - let activeSeekDrag: ActiveSeekDrag | null = null; - - for (let index = 0; index < 2; index += 1) { - const controller = renderer.xr.getController(index); - const controllerPointerOverlay = createPointerOverlay(index, overlayVisibility); - const inputSource: VrInputSource = { - controller, - controllerPointerOverlay, - pointerInputMode: 'controller' - }; - controller.userData = { - ...controller.userData, - vrwpInputSource: inputSource - }; - inputSources.push(inputSource); - controller.addEventListener('connected', (event: any) => { - rememberPointerInputMode(inputSource, event, 'controller'); - }); - controller.addEventListener('selectstart', (event: any) => { - const timestamp = getEventTimestamp(event); - rememberPointerInputMode(inputSource, event, inputSource.pointerInputMode); - if (shouldUseHandPointer(inputSource)) { - beginPalmAimSelection(controller.userData?.vrwpHandAimLatch, timestamp); - } - overlayVisibility.show(timestamp); - onSelectStart(event); - }); - controller.addEventListener('selectend', () => { - endPalmAimSelection(controller.userData?.vrwpHandAimLatch); - if (activeSeekDrag?.inputSource.controller === controller) { - activeSeekDrag = null; - } - }); - controller.addEventListener('select', () => { - endPalmAimSelection(controller.userData?.vrwpHandAimLatch); - }); - bindOverlayActivity(controller, overlayVisibility); - controller.add(controllerPointerOverlay); - scene.add(controller); - - const grip = renderer.xr.getControllerGrip?.(index); - if (grip) { - grip.addEventListener('connected', (event: any) => { - rememberPointerInputMode(inputSource, event, 'controller'); - }); - bindOverlayActivity(grip, overlayVisibility); - grip.add(createControllerOverlay(index, overlayVisibility)); - scene.add(grip); - } - - const hand = renderer.xr.getHand?.(index); - if (hand) { - const handAimLatch = createPalmAimLatch(); - inputSource.hand = hand; - inputSource.handAimLatch = handAimLatch; - controller.userData = { - ...controller.userData, - vrwpHand: hand, - vrwpHandAimLatch: handAimLatch - }; - hand.userData = { - ...hand.userData, - vrwpAimLatch: handAimLatch - }; - bindOverlayActivity(hand, overlayVisibility); - rememberHandedness(hand, { data: hand.inputState }); - createHandOverlay(hand, index, overlayVisibility); - hand.addEventListener?.('connected', (event: any) => { - rememberPointerInputMode(inputSource, event, 'hand'); - rememberHandedness(hand, event); - createHandOverlay(hand, index, overlayVisibility); - overlayVisibility.show(); - }); - scene.add(hand); - - const handPointerOverlay = createWorldPointerOverlay(index, overlayVisibility); - inputSource.handPointerOverlay = handPointerOverlay; - scene.add(handPointerOverlay); - handPointerOverlays.push({ - fallbackPointerOverlay: controllerPointerOverlay, - hand, - handAimLatch, - inputSource, - pointerOverlay: handPointerOverlay - }); - } - } - - overlayVisibility.hideImmediately(); - - return { - beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => { - const inputSource = getInputSourceByController(inputSources, controller); - if (!inputSource || !panel?.seekBarHitAreaMesh) { - activeSeekDrag = null; - return; - } - - activeSeekDrag = { inputSource, onSeek, panel }; - }, - hideOverlays: () => overlayVisibility.hideImmediately(), - raycaster, - showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp), - update: (timestamp: number, hoverTargets: any[] = []) => { - updateHandPointerOverlays(handPointerOverlays, timestamp); - updateActiveSeekDrag(activeSeekDrag, dragRaycaster, timestamp); - const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster, timestamp); - if (isHovering) { - overlayVisibility.show(timestamp); - } - overlayVisibility.update(timestamp); - return isHovering; - } - }; -} - export function handleVrControllerSelect(event: any, options: VrControllerSelectionOptions): void { const controller = event.target; if (!options.raycaster) return; @@ -325,230 +120,6 @@ function applySelectionRay(controller: any, raycaster: any): void { raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); } -function getInputSourceByController(inputSources: VrInputSource[], controller: any): VrInputSource | undefined { - return inputSources.find((inputSource) => inputSource.controller === controller); -} - -function updateInputPointerIntersections( - inputSources: VrInputSource[], - hoverTargets: any[], - hoverRaycaster: any, - timestamp: number -): boolean { - let isHoveringAnyTarget = false; - - inputSources.forEach((inputSource) => { - resetInputPointerLengths(inputSource); - const aimRay = getInputSourceAimRay(inputSource, timestamp); - const pointerOverlay = getActivePointerOverlay(inputSource); - if (!aimRay || !pointerOverlay || hoverTargets.length === 0) { - return; - } - - hoverRaycaster.ray.origin.copy(aimRay.origin); - hoverRaycaster.ray.direction.copy(aimRay.direction); - const intersections = hoverRaycaster.intersectObjects(hoverTargets, true); - if (intersections.length === 0) { - return; - } - - isHoveringAnyTarget = true; - setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance)); - }); - - return isHoveringAnyTarget; -} - -function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void { - if (!activeSeekDrag?.panel.seekBarHitAreaMesh) { - return; - } - - const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true }); - if (!aimRay) { - return; - } - - dragRaycaster.ray.origin.copy(aimRay.origin); - dragRaycaster.ray.direction.copy(aimRay.direction); - const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true); - if (intersections.length === 0) { - return; - } - - activeSeekDrag.onSeek(getSeekProgressFromIntersection(activeSeekDrag.panel, intersections[0].point)); -} - -function getInputSourceAimRay( - inputSource: VrInputSource, - timestamp: number, - { preferLiveHandAim = false }: { preferLiveHandAim?: boolean } = {} -): AimRay | null { - if (shouldUseHandPointer(inputSource) && inputSource.hand && inputSource.handAimLatch) { - if (preferLiveHandAim) { - const handRay = getHandAimRay(inputSource.hand); - if (handRay) { - return handRay; - } - } - - const latchedRay = getPalmAimSelectionRay(inputSource.handAimLatch, timestamp); - if (latchedRay) { - return toAimRay(latchedRay); - } - - const handRay = getHandAimRay(inputSource.hand); - if (handRay) { - return handRay; - } - } - - return getControllerAimRay(inputSource.controller); -} - -function getControllerAimRay(controller: any): AimRay | null { - if (!controller) { - return null; - } - - controller.updateMatrixWorld(); - tempMatrix.identity().extractRotation(controller.matrixWorld); - const origin = new THREE.Vector3().setFromMatrixPosition(controller.matrixWorld); - const direction = new THREE.Vector3(0, 0, -1).applyMatrix4(tempMatrix); - return { direction, origin }; -} - -function resetInputPointerLengths(inputSource: VrInputSource): void { - setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH); - if (inputSource.handPointerOverlay) { - setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH); - } -} - -function getActivePointerOverlay(inputSource: VrInputSource): any { - if (inputSource.handPointerOverlay && inputSource.handPointerOverlay.userData?.vrwpOverlayAvailable !== false) { - return inputSource.handPointerOverlay; - } - - if (inputSource.controllerPointerOverlay && inputSource.controllerPointerOverlay.userData?.vrwpOverlayAvailable !== false) { - return inputSource.controllerPointerOverlay; - } - - return null; -} - -function getPointerIntersectionLength(distance: number): number { - return Math.max( - POINTER_MIN_LENGTH, - Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET) - ); -} - -function setPointerOverlayLength(pointerOverlay: any, length: number): void { - if (!pointerOverlay || pointerOverlay.userData?.vrwpPointerLength === length) { - return; - } - - const pointerLine = pointerOverlay.userData?.vrwpPointerLine; - const pointerTip = pointerOverlay.userData?.vrwpPointerTip; - const positionAttribute = pointerLine?.geometry?.getAttribute?.('position') || - pointerLine?.geometry?.attributes?.position; - - if (positionAttribute?.setXYZ) { - positionAttribute.setXYZ(1, 0, 0, -length); - positionAttribute.needsUpdate = true; - pointerLine.geometry.computeBoundingSphere?.(); - } - - if (pointerTip) { - pointerTip.position.z = -length; - } - - pointerOverlay.userData = { - ...pointerOverlay.userData, - vrwpPointerLength: length - }; -} - -function updateHandPointerOverlays(handPointerOverlays: HandPointerOverlay[], timestamp: number): void { - handPointerOverlays.forEach(({ fallbackPointerOverlay, hand, handAimLatch, inputSource, pointerOverlay }) => { - if (!shouldUseHandPointer(inputSource)) { - pointerOverlay.userData = { - ...pointerOverlay.userData, - vrwpOverlayAvailable: false - }; - fallbackPointerOverlay.userData = { - ...fallbackPointerOverlay.userData, - vrwpOverlayAvailable: true - }; - return; - } - - const currentHandRay = getHandAimRay(hand); - if (currentHandRay) { - recordStablePalmAimRay(handAimLatch, toPalmAimRay(currentHandRay), timestamp); - } - - const selectionPalmRay = getPalmAimSelectionRay(handAimLatch, timestamp); - const displayHandRay = selectionPalmRay ? toAimRay(selectionPalmRay) : currentHandRay; - const hasHandRay = Boolean(displayHandRay); - pointerOverlay.userData = { - ...pointerOverlay.userData, - vrwpOverlayAvailable: hasHandRay - }; - fallbackPointerOverlay.userData = { - ...fallbackPointerOverlay.userData, - vrwpOverlayAvailable: !hasHandRay - }; - - if (!displayHandRay) { - return; - } - - pointerOverlay.position.copy(displayHandRay.origin); - pointerOverlay.quaternion.setFromUnitVectors(DEFAULT_RAY_DIRECTION, displayHandRay.direction); - }); -} - -function rememberPointerInputMode( - inputSource: VrInputSource, - event: any, - fallbackMode: PointerInputMode -): void { - const eventInputSource = event?.data?.inputSource || event?.inputSource || event?.data; - const nextMode = getPointerInputMode(eventInputSource) || fallbackMode; - inputSource.pointerInputMode = nextMode; - inputSource.controller.userData = { - ...inputSource.controller.userData, - vrwpInputSource: inputSource - }; -} - -function getPointerInputMode(eventInputSource: any): PointerInputMode | null { - if (!eventInputSource) { - return null; - } - - if (eventInputSource.hand) { - return 'hand'; - } - - if (Array.isArray(eventInputSource.profiles) && - eventInputSource.profiles.some((profile: string) => profile.toLowerCase().includes('hand'))) { - return 'hand'; - } - - if (eventInputSource.gamepad || eventInputSource.targetRayMode === 'tracked-pointer') { - return 'controller'; - } - - return null; -} - -function shouldUseHandPointer(inputSource: VrInputSource | undefined): boolean { - return inputSource?.pointerInputMode === 'hand'; -} - function getSelectionHandAimRay(controller: any): AimRay | null { const latch = controller.userData?.vrwpHandAimLatch || controller.userData?.vrwpHand?.userData?.vrwpAimLatch; @@ -583,13 +154,6 @@ function getHandAimRay(hand: any): AimRay | null { return { direction, origin }; } -function toPalmAimRay(ray: AimRay): PalmAimRay { - return { - direction: fromThreeVector(ray.direction), - origin: fromThreeVector(ray.origin) - }; -} - function toAimRay(ray: PalmAimRay): AimRay { return { direction: toThreeVector(ray.direction), @@ -597,21 +161,6 @@ function toAimRay(ray: PalmAimRay): AimRay { }; } -function rememberHandedness(hand: any, event: any): void { - const handedness = event?.data?.handedness || - event?.data?.inputSource?.handedness || - hand?.inputState?.handedness; - - if (handedness !== 'left' && handedness !== 'right') { - return; - } - - hand.userData = { - ...hand.userData, - vrwpHandedness: handedness - }; -} - function getHandedness(hand: any): string | undefined { return hand?.userData?.vrwpHandedness || hand?.inputState?.handedness || @@ -630,257 +179,3 @@ function getJointWorldPosition(joint: any): VectorLike | null { function toThreeVector(vector: VectorLike): any { return new THREE.Vector3(vector.x, vector.y, vector.z); } - -function fromThreeVector(vector: any): VectorLike { - return { - x: vector.x, - y: vector.y, - z: vector.z - }; -} - -function getEventTimestamp(event: any): number { - return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now(); -} - -class VrOverlayVisibility { - private readonly fadeDurationMs: number; - private readonly hideDelayMs: number; - private readonly objects: any[] = []; - private opacity = 0; - private targetOpacity = 0; - private visibleUntil = 0; - - constructor({ - fadeDurationMs = INPUT_OVERLAY_FADE_DURATION_MS, - hideDelayMs = INPUT_OVERLAY_HIDE_DELAY_MS - }: VrOverlayVisibilityOptions = {}) { - this.fadeDurationMs = fadeDurationMs; - this.hideDelayMs = hideDelayMs; - } - - register(object: any): void { - this.objects.push(object); - this.setObjectVisible(object, this.targetOpacity > 0 || this.opacity > 0.001); - this.setObjectOpacity(object, this.opacity); - } - - show(timestamp = performance.now()): void { - this.visibleUntil = timestamp + this.hideDelayMs; - this.targetOpacity = 1; - this.objects.forEach((object) => this.setObjectVisible(object, true)); - } - - hideImmediately(): void { - this.visibleUntil = 0; - this.opacity = 0; - this.targetOpacity = 0; - this.objects.forEach((object) => { - this.setObjectOpacity(object, 0); - this.setObjectVisible(object, false); - }); - } - - update(timestamp: number): void { - if (this.targetOpacity > 0 && timestamp >= this.visibleUntil) { - this.targetOpacity = 0; - } - - const shouldBeVisible = this.targetOpacity > 0 || this.opacity > 0.001; - this.objects.forEach((object) => this.setObjectVisible(object, shouldBeVisible)); - - if (this.opacity === this.targetOpacity) { - return; - } - - const fadeStep = this.fadeDurationMs <= 0 - ? 1 - : Math.min(1, 16.67 / this.fadeDurationMs); - const direction = this.opacity < this.targetOpacity ? 1 : -1; - this.opacity = Math.max(0, Math.min(1, this.opacity + fadeStep * direction)); - - if ((direction > 0 && this.opacity >= this.targetOpacity) || (direction < 0 && this.opacity <= this.targetOpacity)) { - this.opacity = this.targetOpacity; - } - - this.objects.forEach((object) => { - this.setObjectOpacity(object, this.opacity); - this.setObjectVisible(object, this.opacity > 0.001); - }); - } - - private setObjectVisible(object: any, isVisible: boolean): void { - const objectVisible = isVisible && object.userData?.vrwpOverlayAvailable !== false; - object.visible = objectVisible; - object.traverse?.((child: any) => { - child.visible = objectVisible; - }); - } - - private setObjectOpacity(object: any, opacity: number): void { - object.traverse?.((child: any) => { - const materials = Array.isArray(child.material) ? child.material : [child.material]; - materials.filter(Boolean).forEach((material: any) => { - material.opacity = opacity; - material.transparent = true; - material.depthTest = false; - material.depthWrite = false; - material.needsUpdate = true; - }); - }); - } -} - -function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVisibility): void { - [ - 'connected', - 'disconnected', - 'select', - 'selectend', - 'squeezestart', - 'squeeze', - 'squeezeend', - 'pinchstart', - 'pinchend' - ].forEach((eventName) => { - target.addEventListener?.(eventName, () => overlayVisibility.show()); - }); -} - -function createPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { - const group = new THREE.Group(); - group.name = `vrPointerOverlay${index}`; - - const pointerMaterial = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.75); - const pointerGeometry = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(0, 0, 0), - new THREE.Vector3(0, 0, -POINTER_LENGTH) - ]); - const pointerLine = new THREE.Line(pointerGeometry, pointerMaterial); - pointerLine.name = `vrPointerRay${index}`; - pointerLine.renderOrder = INPUT_OVERLAY_RENDER_ORDER; - group.add(pointerLine); - - const tipMaterial = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.9); - const tipMesh = new THREE.Mesh(new THREE.SphereGeometry(0.018, 12, 8), tipMaterial); - tipMesh.name = `vrPointerTip${index}`; - tipMesh.position.z = -POINTER_LENGTH; - tipMesh.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1; - group.add(tipMesh); - - group.userData = { - ...group.userData, - vrwpPointerLength: POINTER_LENGTH, - vrwpPointerLine: pointerLine, - vrwpPointerTip: tipMesh - }; - - overlayVisibility.register(group); - return group; -} - -function createWorldPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { - const group = createPointerOverlay(index, overlayVisibility); - group.name = `vrHandPointerOverlay${index}`; - group.userData = { - ...group.userData, - vrwpOverlayAvailable: false - }; - return group; -} - -function createControllerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any { - const group = new THREE.Group(); - group.name = `vrControllerOverlay${index}`; - - const material = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.8); - const outlineGeometry = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(-0.045, -0.025, -0.08), - new THREE.Vector3(0.045, -0.025, -0.08), - new THREE.Vector3(0.055, 0.025, -0.02), - new THREE.Vector3(0.025, 0.035, 0.05), - new THREE.Vector3(-0.025, 0.035, 0.05), - new THREE.Vector3(-0.055, 0.025, -0.02), - new THREE.Vector3(-0.045, -0.025, -0.08) - ]); - const outline = new THREE.Line(outlineGeometry, material); - outline.name = `vrControllerOutline${index}`; - outline.renderOrder = INPUT_OVERLAY_RENDER_ORDER; - group.add(outline); - - const originMaterial = createOverlayMeshMaterial(0xffffff, 0.75); - const origin = new THREE.Mesh(new THREE.SphereGeometry(0.012, 10, 6), originMaterial); - origin.name = `vrControllerOrigin${index}`; - origin.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1; - group.add(origin); - - overlayVisibility.register(group); - return group; -} - -function createHandOverlay(hand: any, index: number, overlayVisibility: VrOverlayVisibility): void { - const joints = getHandJoints(hand); - if (joints.length === 0) { - return; - } - - const material = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.85); - joints.forEach(({ joint, name }) => { - if (!joint || joint.userData?.vrwpHandOverlayMarker) { - return; - } - - const isTip = name.endsWith('tip'); - const isWrist = name === 'wrist'; - const radius = isWrist ? 0.014 : isTip ? 0.011 : 0.008; - const marker = new THREE.Mesh(new THREE.SphereGeometry(radius, 10, 6), material); - marker.name = `vrHandJointOverlay${index}-${name}`; - marker.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 2; - marker.frustumCulled = false; - joint.add(marker); - joint.userData = { - ...joint.userData, - vrwpHandOverlayMarker: marker - }; - overlayVisibility.register(marker); - }); -} - -function getHandJoints(hand: any): Array<{ joint: any; name: string }> { - const joints = hand?.joints; - if (!joints) { - return []; - } - - const namedJoints = HAND_JOINT_NAMES - .map((name) => ({ joint: joints[name], name })) - .filter(({ joint }) => Boolean(joint)); - - if (namedJoints.length > 0) { - return namedJoints; - } - - return Object.entries(joints) - .map(([name, joint]) => ({ joint, name })) - .filter(({ joint }) => Boolean(joint)); -} - -function createOverlayLineMaterial(color: number, opacity: number): any { - return new THREE.LineBasicMaterial({ - color, - depthTest: false, - depthWrite: false, - opacity, - transparent: true - }); -} - -function createOverlayMeshMaterial(color: number, opacity: number): any { - return new THREE.MeshBasicMaterial({ - color, - depthTest: false, - depthWrite: false, - opacity, - transparent: true - }); -}