From fbfdc1c575ff30090748a7e2752d2903acfbcc92 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:51:42 +1000 Subject: [PATCH] removed hand specific tracking --- .gitignore | 1 - README.md | 2 +- package.json | 4 +- scripts/clean-build-output.mjs | 7 + src/vr180player/vr180-player.ts | 3 +- src/vr180player/xr/hand-aim-three.ts | 90 ------- src/vr180player/xr/hand-aim.ts | 255 ------------------ src/vr180player/xr/input-mode.ts | 15 +- src/vr180player/xr/input-rig.ts | 204 ++------------ src/vr180player/xr/pointer-overlays.ts | 73 +---- .../xr/vr-controller-interactions.ts | 27 -- tests/hand-aim.test.mjs | 101 ------- tests/input-mode.test.mjs | 32 +-- 13 files changed, 43 insertions(+), 771 deletions(-) create mode 100644 scripts/clean-build-output.mjs delete mode 100644 src/vr180player/xr/hand-aim-three.ts delete mode 100644 src/vr180player/xr/hand-aim.ts delete mode 100644 tests/hand-aim.test.mjs diff --git a/.gitignore b/.gitignore index cf84e96..2509858 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ dist/ vr180player/*.css vr180player/*.js vr180player/**/*.js -/media diff --git a/README.md b/README.md index 47c4fbd..b3fc1d0 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ When the page loads, the script binds every `[data-vr-web-launcher]` on the page - Video controls include a loop toggle for indefinite replay. - Static images show only applicable controls; playback, seek, and mute controls are video-only. - Image carousels replace skip buttons with previous/next image controls, so you can change stills without leaving the immersive session. -- Controller pointers and lightweight hand/controller overlays appear after controller interaction, then fade away after a short idle period so they do not distract from viewing. +- Controller pointers and lightweight controller overlays appear after controller interaction, then fade away after a short idle period so they do not distract from viewing. - Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required. ## Demo diff --git a/package.json b/package.json index 9a3d99b..371cc67 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "type": "module", "scripts": { "dev": "npm run build && vite --host 0.0.0.0", - "build": "tsc && node scripts/copy-styles.mjs", + "build": "node scripts/clean-build-output.mjs && tsc && node scripts/copy-styles.mjs", "build:test-app": "npm run build && node scripts/build-test-app.mjs", "check": "tsc --noEmit", "deploy:r2": "npm run build && npm run upload:r2", - "test": "npm run build && node --test tests/time.test.mjs tests/projection.test.mjs tests/media-controller.test.mjs tests/texture-manager.test.mjs tests/media-adapter.test.mjs tests/hand-aim.test.mjs tests/input-mode.test.mjs tests/icons.test.mjs tests/control-panel-timing.test.mjs tests/launcher.test.mjs", + "test": "npm run build && node --test tests/time.test.mjs tests/projection.test.mjs tests/media-controller.test.mjs tests/texture-manager.test.mjs tests/media-adapter.test.mjs tests/input-mode.test.mjs tests/icons.test.mjs tests/control-panel-timing.test.mjs tests/launcher.test.mjs", "preview": "npm run build && vite preview --host 127.0.0.1", "upload:r2": "node scripts/upload-r2.mjs", "upload:r2:dry-run": "node scripts/upload-r2.mjs --dry-run" diff --git a/scripts/clean-build-output.mjs b/scripts/clean-build-output.mjs new file mode 100644 index 0000000..f23109e --- /dev/null +++ b/scripts/clean-build-output.mjs @@ -0,0 +1,7 @@ +import { rm } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); + +await rm(join(rootDir, 'vr180player'), { force: true, recursive: true }); diff --git a/src/vr180player/vr180-player.ts b/src/vr180player/vr180-player.ts index d66fd37..850b08f 100644 --- a/src/vr180player/vr180-player.ts +++ b/src/vr180player/vr180-player.ts @@ -563,8 +563,7 @@ export class PlayerSession { try { const session = await navigator.xr.requestSession('immersive-vr', { - requiredFeatures: ['local-floor'], - optionalFeatures: ['hand-tracking'] + requiredFeatures: ['local-floor'] }); if (!session) { throw new Error('requestSession returned no session.'); } diff --git a/src/vr180player/xr/hand-aim-three.ts b/src/vr180player/xr/hand-aim-three.ts deleted file mode 100644 index 4f108b9..0000000 --- a/src/vr180player/xr/hand-aim-three.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as THREE from 'https://unpkg.com/three/build/three.module.js'; -import { - computePalmAimRay, - type PalmAimRay, - type VectorLike -} from './hand-aim.js'; - -export type AimRay = { - direction: any; - origin: any; -}; - -export const DEFAULT_RAY_DIRECTION = new THREE.Vector3(0, 0, -1); - -export 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; - } - - return toAimRay(palmAimRay); -} - -export function toPalmAimRay(ray: AimRay): PalmAimRay { - return { - direction: fromThreeVector(ray.direction), - origin: fromThreeVector(ray.origin) - }; -} - -export function toAimRay(ray: PalmAimRay): AimRay { - return { - direction: toThreeVector(ray.direction), - origin: toThreeVector(ray.origin) - }; -} - -export 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 - }; -} - -export function getHandedness(hand: any): string | undefined { - return hand?.userData?.vrwpHandedness || - hand?.inputState?.handedness || - hand?.userData?.inputSource?.handedness; -} - -export function getJointWorldPosition(joint: any): VectorLike | null { - if (!joint?.getWorldPosition) { - return null; - } - - joint.updateMatrixWorld?.(true); - return joint.getWorldPosition(new THREE.Vector3()); -} - -export function toThreeVector(vector: VectorLike): any { - return new THREE.Vector3(vector.x, vector.y, vector.z); -} - -export function fromThreeVector(vector: any): VectorLike { - return { - x: vector.x, - y: vector.y, - z: vector.z - }; -} diff --git a/src/vr180player/xr/hand-aim.ts b/src/vr180player/xr/hand-aim.ts deleted file mode 100644 index 05ae006..0000000 --- a/src/vr180player/xr/hand-aim.ts +++ /dev/null @@ -1,255 +0,0 @@ -export type VectorLike = { - x: number; - y: number; - z: number; -}; - -export type PalmAimInput = { - handedness?: string | null; - indexMetacarpal?: VectorLike | null; - middleMetacarpal?: VectorLike | null; - pinkyMetacarpal?: VectorLike | null; - ringMetacarpal?: VectorLike | null; - wrist?: VectorLike | null; -}; - -export type PalmAimRay = { - direction: VectorLike; - origin: VectorLike; -}; - -export type TimedPalmAimRay = PalmAimRay & { - timestamp: number; -}; - -export type PalmAimLatch = { - isSelecting: boolean; - selectedRay: TimedPalmAimRay | null; - stableRay: TimedPalmAimRay | null; -}; - -const MIN_AXIS_LENGTH_SQ = 0.000001; -const PALM_AIM_FORWARD_TILT_DEGREES = 40; -const PALM_SURFACE_OFFSET_METERS = 0.035; -export const DEFAULT_PALM_AIM_LATCH_MAX_AGE_MS = 300; - -export function computePalmAimRay(input: PalmAimInput): PalmAimRay | null { - const { indexMetacarpal, pinkyMetacarpal, wrist } = input; - if (!indexMetacarpal || !pinkyMetacarpal || !wrist) { - return null; - } - - const knuckleCenter = averageVectors([ - indexMetacarpal, - input.middleMetacarpal, - input.ringMetacarpal, - pinkyMetacarpal - ]); - if (!knuckleCenter) { - return null; - } - - const fingerAxis = normalize(subtract(knuckleCenter, wrist)); - const acrossPalmAxis = normalize(subtract(pinkyMetacarpal, indexMetacarpal)); - if (!fingerAxis || !acrossPalmAxis) { - return null; - } - - const direction = normalize(getTiltedPalmDirection( - getPalmNormal(fingerAxis, acrossPalmAxis, input.handedness), - fingerAxis - )); - if (!direction) { - return null; - } - - const palmCenter = lerp(wrist, knuckleCenter, 0.62); - const origin = add(palmCenter, scale(direction, PALM_SURFACE_OFFSET_METERS)); - return { direction, origin }; -} - -export function createPalmAimLatch(): PalmAimLatch { - return { - isSelecting: false, - selectedRay: null, - stableRay: null - }; -} - -export function recordStablePalmAimRay( - latch: PalmAimLatch | null | undefined, - ray: PalmAimRay, - timestamp: number -): void { - if (!latch) { - return; - } - - if (latch.isSelecting) { - return; - } - - latch.stableRay = withTimestamp(ray, timestamp); -} - -export function beginPalmAimSelection( - latch: PalmAimLatch | null | undefined, - timestamp: number, - maxAgeMs = DEFAULT_PALM_AIM_LATCH_MAX_AGE_MS -): PalmAimRay | null { - if (!latch) { - return null; - } - - latch.isSelecting = true; - const stableRay = getFreshTimedRay(latch.stableRay, timestamp, maxAgeMs); - latch.selectedRay = stableRay ? cloneTimedRay(stableRay) : null; - return latch.selectedRay ? clonePalmAimRay(latch.selectedRay) : null; -} - -export function endPalmAimSelection(latch: PalmAimLatch | null | undefined): void { - if (!latch) { - return; - } - - latch.isSelecting = false; - latch.selectedRay = null; -} - -export function getPalmAimSelectionRay( - latch: PalmAimLatch | null | undefined, - timestamp: number, - maxAgeMs = DEFAULT_PALM_AIM_LATCH_MAX_AGE_MS -): PalmAimRay | null { - if (!latch) { - return null; - } - - if (latch.isSelecting && latch.selectedRay) { - return clonePalmAimRay(latch.selectedRay); - } - - const stableRay = getFreshTimedRay(latch.stableRay, timestamp, maxAgeMs); - return stableRay ? clonePalmAimRay(stableRay) : null; -} - -function getPalmNormal(fingerAxis: VectorLike, acrossPalmAxis: VectorLike, handedness?: string | null): VectorLike { - if (handedness === 'left') { - return cross(acrossPalmAxis, fingerAxis); - } - - return cross(fingerAxis, acrossPalmAxis); -} - -function getTiltedPalmDirection(palmNormal: VectorLike, fingerAxis: VectorLike): VectorLike { - const tiltRadians = (PALM_AIM_FORWARD_TILT_DEGREES * Math.PI) / 180; - return add( - scale(palmNormal, Math.cos(tiltRadians)), - scale(fingerAxis, Math.sin(tiltRadians)) - ); -} - -function withTimestamp(ray: PalmAimRay, timestamp: number): TimedPalmAimRay { - return { - ...clonePalmAimRay(ray), - timestamp - }; -} - -function cloneTimedRay(ray: TimedPalmAimRay): TimedPalmAimRay { - return { - ...clonePalmAimRay(ray), - timestamp: ray.timestamp - }; -} - -function clonePalmAimRay(ray: PalmAimRay): PalmAimRay { - return { - direction: cloneVector(ray.direction), - origin: cloneVector(ray.origin) - }; -} - -function cloneVector(vector: VectorLike): VectorLike { - return { - x: vector.x, - y: vector.y, - z: vector.z - }; -} - -function getFreshTimedRay( - ray: TimedPalmAimRay | null, - timestamp: number, - maxAgeMs: number -): TimedPalmAimRay | null { - if (!ray) { - return null; - } - - const ageMs = Math.max(0, timestamp - ray.timestamp); - return ageMs <= maxAgeMs ? ray : null; -} - -function averageVectors(vectors: Array): VectorLike | null { - const usableVectors = vectors.filter(Boolean) as VectorLike[]; - if (usableVectors.length === 0) { - return null; - } - - const total = usableVectors.reduce( - (sum, vector) => add(sum, vector), - { x: 0, y: 0, z: 0 } - ); - return scale(total, 1 / usableVectors.length); -} - -function normalize(vector: VectorLike): VectorLike | null { - const lengthSq = vector.x * vector.x + vector.y * vector.y + vector.z * vector.z; - if (lengthSq < MIN_AXIS_LENGTH_SQ) { - return null; - } - - const length = Math.sqrt(lengthSq); - return scale(vector, 1 / length); -} - -function add(a: VectorLike, b: VectorLike): VectorLike { - return { - x: a.x + b.x, - y: a.y + b.y, - z: a.z + b.z - }; -} - -function subtract(a: VectorLike, b: VectorLike): VectorLike { - return { - x: a.x - b.x, - y: a.y - b.y, - z: a.z - b.z - }; -} - -function scale(vector: VectorLike, scalar: number): VectorLike { - return { - x: vector.x * scalar, - y: vector.y * scalar, - z: vector.z * scalar - }; -} - -function lerp(a: VectorLike, b: VectorLike, amount: number): VectorLike { - return { - x: a.x + (b.x - a.x) * amount, - y: a.y + (b.y - a.y) * amount, - z: a.z + (b.z - a.z) * amount - }; -} - -function cross(a: VectorLike, b: VectorLike): VectorLike { - return { - x: a.y * b.z - a.z * b.y, - y: a.z * b.x - a.x * b.z, - z: a.x * b.y - a.y * b.x - }; -} diff --git a/src/vr180player/xr/input-mode.ts b/src/vr180player/xr/input-mode.ts index e8d14b7..5a4b6cf 100644 --- a/src/vr180player/xr/input-mode.ts +++ b/src/vr180player/xr/input-mode.ts @@ -1,4 +1,4 @@ -export type PointerInputMode = 'controller' | 'hand'; +export type PointerInputMode = 'controller'; export type PointerInputModeCarrier = { controller?: { @@ -31,22 +31,9 @@ export function getPointerInputMode(eventInputSource: any): PointerInputMode | n 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 index a8e7c9c..b11b1ee 100644 --- a/src/vr180player/xr/input-rig.ts +++ b/src/vr180player/xr/input-rig.ts @@ -3,36 +3,14 @@ import { getSeekProgressFromIntersection, type VrControlPanel } from './vr-control-panel.js'; -import { - beginPalmAimSelection, - createPalmAimLatch, - endPalmAimSelection, - getPalmAimSelectionRay, - recordStablePalmAimRay, - type PalmAimLatch -} from './hand-aim.js'; -import { - DEFAULT_RAY_DIRECTION, - getHandAimRay, - rememberHandedness, - toAimRay, - toPalmAimRay, - type AimRay -} from './hand-aim-three.js'; -import { - rememberPointerInputMode, - shouldUseHandPointer, - type PointerInputMode -} from './input-mode.js'; +import { rememberPointerInputMode } from './input-mode.js'; import { bindOverlayActivity, createControllerOverlay, - createHandOverlay, createPointerOverlay, - createWorldPointerOverlay, - POINTER_HIT_SURFACE_OFFSET, + getPointerIntersectionLength, POINTER_LENGTH, - POINTER_MIN_LENGTH, + resetInputPointerLengths, setPointerOverlayLength, VrOverlayVisibility } from './pointer-overlays.js'; @@ -51,28 +29,21 @@ type ActiveSeekDrag = { panel: VrControlPanel; }; -type HandPointerOverlay = { - fallbackPointerOverlay: any; - hand: any; - handAimLatch: PalmAimLatch; - inputSource: VrInputSource; - pointerOverlay: any; +type AimRay = { + direction: any; + origin: any; }; type VrInputSource = { controller: any; controllerPointerOverlay: any; - hand?: any; - handAimLatch?: PalmAimLatch; - handPointerOverlay?: any; - pointerInputMode: PointerInputMode; + pointerInputMode: 'controller'; }; 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(); @@ -97,23 +68,15 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even 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); + rememberPointerInputMode(inputSource, event, 'controller'); + overlayVisibility.show(getEventTimestamp(event)); 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); @@ -127,43 +90,6 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even 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(); @@ -182,9 +108,8 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even 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); + updateActiveSeekDrag(activeSeekDrag, dragRaycaster); + const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster); if (isHovering) { overlayVisibility.show(timestamp); } @@ -208,15 +133,14 @@ function getInputSourceByController(inputSources: VrInputSource[], controller: a function updateInputPointerIntersections( inputSources: VrInputSource[], hoverTargets: any[], - hoverRaycaster: any, - timestamp: number + hoverRaycaster: any ): boolean { let isHoveringAnyTarget = false; inputSources.forEach((inputSource) => { resetInputPointerLengths(inputSource); - const aimRay = getInputSourceAimRay(inputSource, timestamp); - const pointerOverlay = getActivePointerOverlay(inputSource); + const aimRay = getControllerAimRay(inputSource.controller); + const pointerOverlay = inputSource.controllerPointerOverlay; if (!aimRay || !pointerOverlay || hoverTargets.length === 0) { return; } @@ -228,25 +152,26 @@ function updateInputPointerIntersections( return; } - isHoveringAnyTarget = true; setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance)); + isHoveringAnyTarget = true; }); return isHoveringAnyTarget; } -function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void { +function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any): void { if (!activeSeekDrag?.panel.seekBarHitAreaMesh) { return; } - const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true }); + const aimRay = getControllerAimRay(activeSeekDrag.inputSource.controller); 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; @@ -255,33 +180,6 @@ function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycast 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; @@ -294,72 +192,6 @@ function getControllerAimRay(controller: any): AimRay | null { 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 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 index 5bd7f7b..cd687a3 100644 --- a/src/vr180player/xr/pointer-overlays.ts +++ b/src/vr180player/xr/pointer-overlays.ts @@ -2,7 +2,6 @@ import * as THREE from 'https://unpkg.com/three/build/three.module.js'; export type PointerOverlayInputSource = { controllerPointerOverlay: any; - handPointerOverlay?: any; }; export type VrOverlayVisibilityOptions = { @@ -17,14 +16,6 @@ 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-tip', - 'index-finger-tip', - 'middle-finger-tip', - 'pinky-finger-tip' -]; - export class VrOverlayVisibility { private readonly fadeDurationMs: number; private readonly hideDelayMs: number; @@ -121,9 +112,7 @@ export function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVis 'selectend', 'squeezestart', 'squeeze', - 'squeezeend', - 'pinchstart', - 'pinchend' + 'squeezeend' ].forEach((eventName) => { target.addEventListener?.(eventName, () => overlayVisibility.show()); }); @@ -161,16 +150,6 @@ export function createPointerOverlay(index: number, overlayVisibility: VrOverlay 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}`; @@ -200,39 +179,8 @@ export function createControllerOverlay(index: number, overlayVisibility: VrOver 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 ? 0xb9e8ff : 0xffd99a, 0.26); - joints.forEach(({ joint, name }) => { - if (!joint || joint.userData?.vrwpHandOverlayMarker) { - return; - } - - const isTip = name.endsWith('tip'); - const isWrist = name === 'wrist'; - const radius = isWrist ? 0.008 : isTip ? 0.006 : 0.005; - const marker = new THREE.Mesh(new THREE.SphereGeometry(radius, 8, 5), 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 { @@ -302,22 +250,3 @@ function getOverlayMaterialMaxOpacity(material: any): number { const maxOpacity = material.userData?.vrwpOverlayMaxOpacity; return Number.isFinite(maxOpacity) ? maxOpacity : 1; } - -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 50e86d8..76f8a0b 100644 --- a/src/vr180player/xr/vr-controller-interactions.ts +++ b/src/vr180player/xr/vr-controller-interactions.ts @@ -3,13 +3,6 @@ import { getSeekProgressFromIntersection, type VrControlPanel } from './vr-control-panel.js'; -import { getPalmAimSelectionRay } from './hand-aim.js'; -import { - getHandAimRay, - toAimRay, - type AimRay -} from './hand-aim-three.js'; -import { shouldUseHandPointer } from './input-mode.js'; type VrControllerSelectionOptions = { beginSeekDrag?: (controller: any) => void; @@ -100,28 +93,8 @@ function togglePanel(options: VrControllerSelectionOptions): void { } function applySelectionRay(controller: any, raycaster: any): void { - const handRay = shouldUseHandPointer(controller.userData?.vrwpInputSource) - ? getSelectionHandAimRay(controller) || getHandAimRay(controller.userData?.vrwpHand) - : null; - if (handRay) { - raycaster.ray.origin.copy(handRay.origin); - raycaster.ray.direction.copy(handRay.direction); - return; - } - controller.updateMatrixWorld(); tempMatrix.identity().extractRotation(controller.matrixWorld); raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); } - -function getSelectionHandAimRay(controller: any): AimRay | null { - const latch = controller.userData?.vrwpHandAimLatch || - controller.userData?.vrwpHand?.userData?.vrwpAimLatch; - if (!latch) { - return null; - } - - const palmAimRay = getPalmAimSelectionRay(latch, performance.now()); - return palmAimRay ? toAimRay(palmAimRay) : null; -} diff --git a/tests/hand-aim.test.mjs b/tests/hand-aim.test.mjs deleted file mode 100644 index 2489a37..0000000 --- a/tests/hand-aim.test.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - beginPalmAimSelection, - computePalmAimRay, - createPalmAimLatch, - endPalmAimSelection, - getPalmAimSelectionRay, - recordStablePalmAimRay -} from '../vr180player/xr/hand-aim.js'; - -const wrist = { x: 0, y: 0, z: 0 }; -const middleMetacarpal = { x: 0, y: 0.1, z: 0 }; -const ringMetacarpal = { x: 0.025, y: 0.1, z: 0 }; -const straightAheadRay = { - direction: { x: 0, y: 0, z: -1 }, - origin: { x: 0, y: 1.4, z: -0.04 } -}; -const pinchedRay = { - direction: { x: 0.4, y: -0.2, z: -0.8 }, - origin: { x: 0.04, y: 1.32, z: -0.03 } -}; - -test('computePalmAimRay tilts a right palm ray toward the finger-forward axis', () => { - const ray = computePalmAimRay({ - handedness: 'right', - indexMetacarpal: { x: -0.045, y: 0.1, z: 0 }, - middleMetacarpal: { x: -0.015, y: 0.1, z: 0 }, - pinkyMetacarpal: { x: 0.045, y: 0.1, z: 0 }, - ringMetacarpal: { x: 0.015, y: 0.1, z: 0 }, - wrist - }); - - assert.ok(ray); - assert.equal(Math.round(ray.direction.x * 1000) / 1000, 0); - assert.equal(Math.round(ray.direction.y * 1000) / 1000, 0.643); - assert.equal(Math.round(ray.direction.z * 1000) / 1000, -0.766); - assert.equal(Math.round(ray.origin.y * 1000) / 1000, 0.084); - assert.equal(Math.round(ray.origin.z * 1000) / 1000, -0.027); -}); - -test('computePalmAimRay flips the palm normal for a left hand while keeping forward tilt', () => { - const ray = computePalmAimRay({ - handedness: 'left', - indexMetacarpal: { x: 0.045, y: 0.1, z: 0 }, - middleMetacarpal: { x: 0.015, y: 0.1, z: 0 }, - pinkyMetacarpal: { x: -0.045, y: 0.1, z: 0 }, - ringMetacarpal: { x: -0.015, y: 0.1, z: 0 }, - wrist - }); - - assert.ok(ray); - assert.equal(Math.round(ray.direction.y * 1000) / 1000, 0.643); - assert.equal(Math.round(ray.direction.z * 1000) / 1000, -0.766); -}); - -test('computePalmAimRay returns null when palm joints cannot define a stable ray', () => { - assert.equal(computePalmAimRay({ handedness: 'right', wrist }), null); - assert.equal(computePalmAimRay({ - handedness: 'right', - indexMetacarpal: { x: 0, y: 0.1, z: 0 }, - pinkyMetacarpal: { x: 0, y: 0.1, z: 0 }, - wrist - }), null); -}); - -test('palm aim latch returns the latest fresh stable ray', () => { - const latch = createPalmAimLatch(); - - recordStablePalmAimRay(latch, straightAheadRay, 100); - - assert.deepEqual(getPalmAimSelectionRay(latch, 120), straightAheadRay); - assert.equal(getPalmAimSelectionRay(latch, 500), null); -}); - -test('palm aim latch freezes the pre-selection ray while selecting', () => { - const latch = createPalmAimLatch(); - - recordStablePalmAimRay(latch, straightAheadRay, 100); - assert.deepEqual(beginPalmAimSelection(latch, 120), straightAheadRay); - - recordStablePalmAimRay(latch, pinchedRay, 130); - - assert.deepEqual(getPalmAimSelectionRay(latch, 140), straightAheadRay); - assert.deepEqual(getPalmAimSelectionRay(latch, 1000), straightAheadRay); - - endPalmAimSelection(latch); - recordStablePalmAimRay(latch, pinchedRay, 150); - - assert.deepEqual(getPalmAimSelectionRay(latch, 160), pinchedRay); -}); - -test('palm aim latch ignores stale rays when selection starts too late', () => { - const latch = createPalmAimLatch(); - - recordStablePalmAimRay(latch, straightAheadRay, 100); - - assert.equal(beginPalmAimSelection(latch, 450), null); - assert.equal(getPalmAimSelectionRay(latch, 450), null); -}); diff --git a/tests/input-mode.test.mjs b/tests/input-mode.test.mjs index 246036c..339e666 100644 --- a/tests/input-mode.test.mjs +++ b/tests/input-mode.test.mjs @@ -3,14 +3,13 @@ import assert from 'node:assert/strict'; import { getPointerInputMode, - rememberPointerInputMode, - shouldUseHandPointer + rememberPointerInputMode } from '../vr180player/xr/input-mode.js'; -test('getPointerInputMode detects WebXR hand sources', () => { - assert.equal(getPointerInputMode({ hand: {} }), 'hand'); - assert.equal(getPointerInputMode({ profiles: ['generic-hand-tracking'] }), 'hand'); - assert.equal(getPointerInputMode({ profiles: ['Oculus-Hand'] }), 'hand'); +test('getPointerInputMode ignores WebXR hand sources', () => { + assert.equal(getPointerInputMode({ hand: {} }), null); + assert.equal(getPointerInputMode({ profiles: ['generic-hand-tracking'] }), null); + assert.equal(getPointerInputMode({ profiles: ['Oculus-Hand'] }), null); }); test('getPointerInputMode detects controller sources', () => { @@ -27,20 +26,20 @@ test('getPointerInputMode returns null for unknown or gaze-like sources', () => test('rememberPointerInputMode reads input sources from supported event shapes', () => { const fromNestedInputSource = {}; - rememberPointerInputMode(fromNestedInputSource, { data: { inputSource: { hand: {} } } }, 'controller'); - assert.equal(fromNestedInputSource.pointerInputMode, 'hand'); + rememberPointerInputMode(fromNestedInputSource, { data: { inputSource: { gamepad: {} } } }, 'controller'); + assert.equal(fromNestedInputSource.pointerInputMode, 'controller'); const fromDirectInputSource = {}; - rememberPointerInputMode(fromDirectInputSource, { inputSource: { gamepad: {} } }, 'hand'); + rememberPointerInputMode(fromDirectInputSource, { inputSource: { gamepad: {} } }, 'controller'); assert.equal(fromDirectInputSource.pointerInputMode, 'controller'); const fromDataSource = {}; - rememberPointerInputMode(fromDataSource, { data: { targetRayMode: 'tracked-pointer' } }, 'hand'); + rememberPointerInputMode(fromDataSource, { data: { targetRayMode: 'tracked-pointer' } }, 'controller'); assert.equal(fromDataSource.pointerInputMode, 'controller'); }); test('rememberPointerInputMode keeps fallback mode when the event is ambiguous', () => { - const inputSource = { pointerInputMode: 'hand' }; + const inputSource = { pointerInputMode: 'controller' }; rememberPointerInputMode(inputSource, { data: { targetRayMode: 'gaze' } }, 'controller'); @@ -56,16 +55,9 @@ test('rememberPointerInputMode stores the input source on controller userData', } }; - rememberPointerInputMode(inputSource, { data: { inputSource: { hand: {} } } }, 'controller'); + rememberPointerInputMode(inputSource, { data: { inputSource: { gamepad: {} } } }, 'controller'); - assert.equal(inputSource.pointerInputMode, 'hand'); + assert.equal(inputSource.pointerInputMode, 'controller'); assert.equal(inputSource.controller.userData.existing, true); assert.equal(inputSource.controller.userData.vrwpInputSource, inputSource); }); - -test('shouldUseHandPointer only enables the hand ray for remembered hand mode', () => { - assert.equal(shouldUseHandPointer({ pointerInputMode: 'hand' }), true); - assert.equal(shouldUseHandPointer({ pointerInputMode: 'controller' }), false); - assert.equal(shouldUseHandPointer({}), false); - assert.equal(shouldUseHandPointer(undefined), false); -});