forked from EXT/VR180-Web-Player
removed hand specific tracking
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,4 +6,3 @@ dist/
|
|||||||
vr180player/*.css
|
vr180player/*.css
|
||||||
vr180player/*.js
|
vr180player/*.js
|
||||||
vr180player/**/*.js
|
vr180player/**/*.js
|
||||||
/media
|
|
||||||
|
|||||||
@@ -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.
|
- Video controls include a loop toggle for indefinite replay.
|
||||||
- Static images show only applicable controls; playback, seek, and mute controls are video-only.
|
- 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.
|
- 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.
|
- Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run build && vite --host 0.0.0.0",
|
"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",
|
"build:test-app": "npm run build && node scripts/build-test-app.mjs",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"deploy:r2": "npm run build && npm run upload:r2",
|
"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",
|
"preview": "npm run build && vite preview --host 127.0.0.1",
|
||||||
"upload:r2": "node scripts/upload-r2.mjs",
|
"upload:r2": "node scripts/upload-r2.mjs",
|
||||||
"upload:r2:dry-run": "node scripts/upload-r2.mjs --dry-run"
|
"upload:r2:dry-run": "node scripts/upload-r2.mjs --dry-run"
|
||||||
|
|||||||
7
scripts/clean-build-output.mjs
Normal file
7
scripts/clean-build-output.mjs
Normal file
@@ -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 });
|
||||||
@@ -563,8 +563,7 @@ export class PlayerSession {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await navigator.xr.requestSession('immersive-vr', {
|
const session = await navigator.xr.requestSession('immersive-vr', {
|
||||||
requiredFeatures: ['local-floor'],
|
requiredFeatures: ['local-floor']
|
||||||
optionalFeatures: ['hand-tracking']
|
|
||||||
});
|
});
|
||||||
if (!session) { throw new Error('requestSession returned no session.'); }
|
if (!session) { throw new Error('requestSession returned no session.'); }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 | undefined>): 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type PointerInputMode = 'controller' | 'hand';
|
export type PointerInputMode = 'controller';
|
||||||
|
|
||||||
export type PointerInputModeCarrier = {
|
export type PointerInputModeCarrier = {
|
||||||
controller?: {
|
controller?: {
|
||||||
@@ -31,22 +31,9 @@ export function getPointerInputMode(eventInputSource: any): PointerInputMode | n
|
|||||||
return null;
|
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') {
|
if (eventInputSource.gamepad || eventInputSource.targetRayMode === 'tracked-pointer') {
|
||||||
return 'controller';
|
return 'controller';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldUseHandPointer(inputSource: PointerInputModeCarrier | undefined): boolean {
|
|
||||||
return inputSource?.pointerInputMode === 'hand';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,36 +3,14 @@ import {
|
|||||||
getSeekProgressFromIntersection,
|
getSeekProgressFromIntersection,
|
||||||
type VrControlPanel
|
type VrControlPanel
|
||||||
} from './vr-control-panel.js';
|
} from './vr-control-panel.js';
|
||||||
import {
|
import { rememberPointerInputMode } from './input-mode.js';
|
||||||
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 {
|
import {
|
||||||
bindOverlayActivity,
|
bindOverlayActivity,
|
||||||
createControllerOverlay,
|
createControllerOverlay,
|
||||||
createHandOverlay,
|
|
||||||
createPointerOverlay,
|
createPointerOverlay,
|
||||||
createWorldPointerOverlay,
|
getPointerIntersectionLength,
|
||||||
POINTER_HIT_SURFACE_OFFSET,
|
|
||||||
POINTER_LENGTH,
|
POINTER_LENGTH,
|
||||||
POINTER_MIN_LENGTH,
|
resetInputPointerLengths,
|
||||||
setPointerOverlayLength,
|
setPointerOverlayLength,
|
||||||
VrOverlayVisibility
|
VrOverlayVisibility
|
||||||
} from './pointer-overlays.js';
|
} from './pointer-overlays.js';
|
||||||
@@ -51,28 +29,21 @@ type ActiveSeekDrag = {
|
|||||||
panel: VrControlPanel;
|
panel: VrControlPanel;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HandPointerOverlay = {
|
type AimRay = {
|
||||||
fallbackPointerOverlay: any;
|
direction: any;
|
||||||
hand: any;
|
origin: any;
|
||||||
handAimLatch: PalmAimLatch;
|
|
||||||
inputSource: VrInputSource;
|
|
||||||
pointerOverlay: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type VrInputSource = {
|
type VrInputSource = {
|
||||||
controller: any;
|
controller: any;
|
||||||
controllerPointerOverlay: any;
|
controllerPointerOverlay: any;
|
||||||
hand?: any;
|
pointerInputMode: 'controller';
|
||||||
handAimLatch?: PalmAimLatch;
|
|
||||||
handPointerOverlay?: any;
|
|
||||||
pointerInputMode: PointerInputMode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tempMatrix = new THREE.Matrix4();
|
const tempMatrix = new THREE.Matrix4();
|
||||||
|
|
||||||
export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig {
|
export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig {
|
||||||
const overlayVisibility = new VrOverlayVisibility();
|
const overlayVisibility = new VrOverlayVisibility();
|
||||||
const handPointerOverlays: HandPointerOverlay[] = [];
|
|
||||||
const inputSources: VrInputSource[] = [];
|
const inputSources: VrInputSource[] = [];
|
||||||
const raycaster = createPointerRaycaster();
|
const raycaster = createPointerRaycaster();
|
||||||
const hoverRaycaster = createPointerRaycaster();
|
const hoverRaycaster = createPointerRaycaster();
|
||||||
@@ -97,23 +68,15 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even
|
|||||||
rememberPointerInputMode(inputSource, event, 'controller');
|
rememberPointerInputMode(inputSource, event, 'controller');
|
||||||
});
|
});
|
||||||
controller.addEventListener('selectstart', (event: any) => {
|
controller.addEventListener('selectstart', (event: any) => {
|
||||||
const timestamp = getEventTimestamp(event);
|
rememberPointerInputMode(inputSource, event, 'controller');
|
||||||
rememberPointerInputMode(inputSource, event, inputSource.pointerInputMode);
|
overlayVisibility.show(getEventTimestamp(event));
|
||||||
if (shouldUseHandPointer(inputSource)) {
|
|
||||||
beginPalmAimSelection(controller.userData?.vrwpHandAimLatch, timestamp);
|
|
||||||
}
|
|
||||||
overlayVisibility.show(timestamp);
|
|
||||||
onSelectStart(event);
|
onSelectStart(event);
|
||||||
});
|
});
|
||||||
controller.addEventListener('selectend', () => {
|
controller.addEventListener('selectend', () => {
|
||||||
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
|
|
||||||
if (activeSeekDrag?.inputSource.controller === controller) {
|
if (activeSeekDrag?.inputSource.controller === controller) {
|
||||||
activeSeekDrag = null;
|
activeSeekDrag = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
controller.addEventListener('select', () => {
|
|
||||||
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
|
|
||||||
});
|
|
||||||
bindOverlayActivity(controller, overlayVisibility);
|
bindOverlayActivity(controller, overlayVisibility);
|
||||||
controller.add(controllerPointerOverlay);
|
controller.add(controllerPointerOverlay);
|
||||||
scene.add(controller);
|
scene.add(controller);
|
||||||
@@ -127,43 +90,6 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even
|
|||||||
grip.add(createControllerOverlay(index, overlayVisibility));
|
grip.add(createControllerOverlay(index, overlayVisibility));
|
||||||
scene.add(grip);
|
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();
|
overlayVisibility.hideImmediately();
|
||||||
@@ -182,9 +108,8 @@ export function createVrInputRig(scene: any, renderer: any, onSelectStart: (even
|
|||||||
raycaster,
|
raycaster,
|
||||||
showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp),
|
showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp),
|
||||||
update: (timestamp: number, hoverTargets: any[] = []) => {
|
update: (timestamp: number, hoverTargets: any[] = []) => {
|
||||||
updateHandPointerOverlays(handPointerOverlays, timestamp);
|
updateActiveSeekDrag(activeSeekDrag, dragRaycaster);
|
||||||
updateActiveSeekDrag(activeSeekDrag, dragRaycaster, timestamp);
|
const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster);
|
||||||
const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster, timestamp);
|
|
||||||
if (isHovering) {
|
if (isHovering) {
|
||||||
overlayVisibility.show(timestamp);
|
overlayVisibility.show(timestamp);
|
||||||
}
|
}
|
||||||
@@ -208,15 +133,14 @@ function getInputSourceByController(inputSources: VrInputSource[], controller: a
|
|||||||
function updateInputPointerIntersections(
|
function updateInputPointerIntersections(
|
||||||
inputSources: VrInputSource[],
|
inputSources: VrInputSource[],
|
||||||
hoverTargets: any[],
|
hoverTargets: any[],
|
||||||
hoverRaycaster: any,
|
hoverRaycaster: any
|
||||||
timestamp: number
|
|
||||||
): boolean {
|
): boolean {
|
||||||
let isHoveringAnyTarget = false;
|
let isHoveringAnyTarget = false;
|
||||||
|
|
||||||
inputSources.forEach((inputSource) => {
|
inputSources.forEach((inputSource) => {
|
||||||
resetInputPointerLengths(inputSource);
|
resetInputPointerLengths(inputSource);
|
||||||
const aimRay = getInputSourceAimRay(inputSource, timestamp);
|
const aimRay = getControllerAimRay(inputSource.controller);
|
||||||
const pointerOverlay = getActivePointerOverlay(inputSource);
|
const pointerOverlay = inputSource.controllerPointerOverlay;
|
||||||
if (!aimRay || !pointerOverlay || hoverTargets.length === 0) {
|
if (!aimRay || !pointerOverlay || hoverTargets.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -228,25 +152,26 @@ function updateInputPointerIntersections(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isHoveringAnyTarget = true;
|
|
||||||
setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance));
|
setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance));
|
||||||
|
isHoveringAnyTarget = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return isHoveringAnyTarget;
|
return isHoveringAnyTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void {
|
function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any): void {
|
||||||
if (!activeSeekDrag?.panel.seekBarHitAreaMesh) {
|
if (!activeSeekDrag?.panel.seekBarHitAreaMesh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true });
|
const aimRay = getControllerAimRay(activeSeekDrag.inputSource.controller);
|
||||||
if (!aimRay) {
|
if (!aimRay) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dragRaycaster.ray.origin.copy(aimRay.origin);
|
dragRaycaster.ray.origin.copy(aimRay.origin);
|
||||||
dragRaycaster.ray.direction.copy(aimRay.direction);
|
dragRaycaster.ray.direction.copy(aimRay.direction);
|
||||||
|
|
||||||
const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true);
|
const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true);
|
||||||
if (intersections.length === 0) {
|
if (intersections.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -255,33 +180,6 @@ function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycast
|
|||||||
activeSeekDrag.onSeek(getSeekProgressFromIntersection(activeSeekDrag.panel, intersections[0].point));
|
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 {
|
function getControllerAimRay(controller: any): AimRay | null {
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
return null;
|
return null;
|
||||||
@@ -294,72 +192,6 @@ function getControllerAimRay(controller: any): AimRay | null {
|
|||||||
return { direction, origin };
|
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 {
|
function getEventTimestamp(event: any): number {
|
||||||
return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now();
|
return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
|||||||
|
|
||||||
export type PointerOverlayInputSource = {
|
export type PointerOverlayInputSource = {
|
||||||
controllerPointerOverlay: any;
|
controllerPointerOverlay: any;
|
||||||
handPointerOverlay?: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VrOverlayVisibilityOptions = {
|
export type VrOverlayVisibilityOptions = {
|
||||||
@@ -17,14 +16,6 @@ export const POINTER_LENGTH = 5;
|
|||||||
export const POINTER_MIN_LENGTH = 0.06;
|
export const POINTER_MIN_LENGTH = 0.06;
|
||||||
export const POINTER_HIT_SURFACE_OFFSET = 0.015;
|
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 {
|
export class VrOverlayVisibility {
|
||||||
private readonly fadeDurationMs: number;
|
private readonly fadeDurationMs: number;
|
||||||
private readonly hideDelayMs: number;
|
private readonly hideDelayMs: number;
|
||||||
@@ -121,9 +112,7 @@ export function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVis
|
|||||||
'selectend',
|
'selectend',
|
||||||
'squeezestart',
|
'squeezestart',
|
||||||
'squeeze',
|
'squeeze',
|
||||||
'squeezeend',
|
'squeezeend'
|
||||||
'pinchstart',
|
|
||||||
'pinchend'
|
|
||||||
].forEach((eventName) => {
|
].forEach((eventName) => {
|
||||||
target.addEventListener?.(eventName, () => overlayVisibility.show());
|
target.addEventListener?.(eventName, () => overlayVisibility.show());
|
||||||
});
|
});
|
||||||
@@ -161,16 +150,6 @@ export function createPointerOverlay(index: number, overlayVisibility: VrOverlay
|
|||||||
return 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 {
|
export function createControllerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
group.name = `vrControllerOverlay${index}`;
|
group.name = `vrControllerOverlay${index}`;
|
||||||
@@ -200,39 +179,8 @@ export function createControllerOverlay(index: number, overlayVisibility: VrOver
|
|||||||
return 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 ? 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 {
|
export function resetInputPointerLengths(inputSource: PointerOverlayInputSource): void {
|
||||||
setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH);
|
setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH);
|
||||||
if (inputSource.handPointerOverlay) {
|
|
||||||
setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPointerIntersectionLength(distance: number): number {
|
export function getPointerIntersectionLength(distance: number): number {
|
||||||
@@ -302,22 +250,3 @@ function getOverlayMaterialMaxOpacity(material: any): number {
|
|||||||
const maxOpacity = material.userData?.vrwpOverlayMaxOpacity;
|
const maxOpacity = material.userData?.vrwpOverlayMaxOpacity;
|
||||||
return Number.isFinite(maxOpacity) ? maxOpacity : 1;
|
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));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ import {
|
|||||||
getSeekProgressFromIntersection,
|
getSeekProgressFromIntersection,
|
||||||
type VrControlPanel
|
type VrControlPanel
|
||||||
} from './vr-control-panel.js';
|
} 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 = {
|
type VrControllerSelectionOptions = {
|
||||||
beginSeekDrag?: (controller: any) => void;
|
beginSeekDrag?: (controller: any) => void;
|
||||||
@@ -100,28 +93,8 @@ function togglePanel(options: VrControllerSelectionOptions): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySelectionRay(controller: any, raycaster: any): 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();
|
controller.updateMatrixWorld();
|
||||||
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
||||||
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
||||||
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -3,14 +3,13 @@ import assert from 'node:assert/strict';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getPointerInputMode,
|
getPointerInputMode,
|
||||||
rememberPointerInputMode,
|
rememberPointerInputMode
|
||||||
shouldUseHandPointer
|
|
||||||
} from '../vr180player/xr/input-mode.js';
|
} from '../vr180player/xr/input-mode.js';
|
||||||
|
|
||||||
test('getPointerInputMode detects WebXR hand sources', () => {
|
test('getPointerInputMode ignores WebXR hand sources', () => {
|
||||||
assert.equal(getPointerInputMode({ hand: {} }), 'hand');
|
assert.equal(getPointerInputMode({ hand: {} }), null);
|
||||||
assert.equal(getPointerInputMode({ profiles: ['generic-hand-tracking'] }), 'hand');
|
assert.equal(getPointerInputMode({ profiles: ['generic-hand-tracking'] }), null);
|
||||||
assert.equal(getPointerInputMode({ profiles: ['Oculus-Hand'] }), 'hand');
|
assert.equal(getPointerInputMode({ profiles: ['Oculus-Hand'] }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPointerInputMode detects controller sources', () => {
|
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', () => {
|
test('rememberPointerInputMode reads input sources from supported event shapes', () => {
|
||||||
const fromNestedInputSource = {};
|
const fromNestedInputSource = {};
|
||||||
rememberPointerInputMode(fromNestedInputSource, { data: { inputSource: { hand: {} } } }, 'controller');
|
rememberPointerInputMode(fromNestedInputSource, { data: { inputSource: { gamepad: {} } } }, 'controller');
|
||||||
assert.equal(fromNestedInputSource.pointerInputMode, 'hand');
|
assert.equal(fromNestedInputSource.pointerInputMode, 'controller');
|
||||||
|
|
||||||
const fromDirectInputSource = {};
|
const fromDirectInputSource = {};
|
||||||
rememberPointerInputMode(fromDirectInputSource, { inputSource: { gamepad: {} } }, 'hand');
|
rememberPointerInputMode(fromDirectInputSource, { inputSource: { gamepad: {} } }, 'controller');
|
||||||
assert.equal(fromDirectInputSource.pointerInputMode, 'controller');
|
assert.equal(fromDirectInputSource.pointerInputMode, 'controller');
|
||||||
|
|
||||||
const fromDataSource = {};
|
const fromDataSource = {};
|
||||||
rememberPointerInputMode(fromDataSource, { data: { targetRayMode: 'tracked-pointer' } }, 'hand');
|
rememberPointerInputMode(fromDataSource, { data: { targetRayMode: 'tracked-pointer' } }, 'controller');
|
||||||
assert.equal(fromDataSource.pointerInputMode, 'controller');
|
assert.equal(fromDataSource.pointerInputMode, 'controller');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rememberPointerInputMode keeps fallback mode when the event is ambiguous', () => {
|
test('rememberPointerInputMode keeps fallback mode when the event is ambiguous', () => {
|
||||||
const inputSource = { pointerInputMode: 'hand' };
|
const inputSource = { pointerInputMode: 'controller' };
|
||||||
|
|
||||||
rememberPointerInputMode(inputSource, { data: { targetRayMode: 'gaze' } }, '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.existing, true);
|
||||||
assert.equal(inputSource.controller.userData.vrwpInputSource, inputSource);
|
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);
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user