9a353795f2
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import { useRef } from 'react';
|
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
import * as THREE from 'three';
|
|
// DEVKIT: remove this import + the fake-input block in useFrame to strip devkit
|
|
import { fakeInput, isFakeActive } from '@/devkit/fakeInputStore';
|
|
|
|
export interface GrabState {
|
|
/** Analog grip pressure (0..1) */
|
|
gripValue: number;
|
|
/** Analog trigger pressure (0..1) */
|
|
triggerValue: number;
|
|
/** True when grip pressure crosses grab threshold (with hysteresis) */
|
|
isGrabbing: boolean;
|
|
/** World-space matrix of the gripSpace (Touch Plus controller pose) */
|
|
gripWorld: THREE.Matrix4;
|
|
/** True when controller pose is valid this frame */
|
|
hasPose: boolean;
|
|
}
|
|
|
|
export interface ControllerGrabSnapshot {
|
|
left: GrabState;
|
|
right: GrabState;
|
|
}
|
|
|
|
const GRAB_ON = 0.7;
|
|
const GRAB_OFF = 0.3;
|
|
|
|
function makeState(): GrabState {
|
|
return {
|
|
gripValue: 0,
|
|
triggerValue: 0,
|
|
isGrabbing: false,
|
|
gripWorld: new THREE.Matrix4(),
|
|
hasPose: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Polls WebXR input sources every frame and writes Touch Plus controller
|
|
* state (grip analog, trigger analog, grip world pose, isGrabbing with
|
|
* hysteresis) into a stable ref object.
|
|
*
|
|
* The returned ref is mutated in place — callers should also call useFrame()
|
|
* with a *later* registration to read it. Pass a higher `renderPriority` to
|
|
* the consumer's useFrame to ensure ordering.
|
|
*/
|
|
export function useControllerGrab() {
|
|
const ref = useRef<ControllerGrabSnapshot>({
|
|
left: makeState(),
|
|
right: makeState(),
|
|
});
|
|
const { gl } = useThree();
|
|
|
|
useFrame((_state, _delta, frame: XRFrame | undefined) => {
|
|
// DEVKIT: fake-input branch — bypasses real WebXR when SimXR is active.
|
|
if (isFakeActive()) {
|
|
for (const hand of ['left', 'right'] as const) {
|
|
const fake = fakeInput[hand];
|
|
const slot = ref.current[hand];
|
|
slot.gripValue = fake.gripValue;
|
|
slot.triggerValue = fake.triggerValue;
|
|
slot.hasPose = fake.hasPose;
|
|
slot.gripWorld.copy(fake.gripWorld);
|
|
if (!slot.isGrabbing && fake.gripValue > GRAB_ON) {
|
|
slot.isGrabbing = true;
|
|
console.log(`[XR][grab] ▶ ${hand} START (fake v=${fake.gripValue.toFixed(2)})`);
|
|
} else if (slot.isGrabbing && fake.gripValue < GRAB_OFF) {
|
|
slot.isGrabbing = false;
|
|
console.log(`[XR][grab] ◼ ${hand} END (fake)`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!frame) return;
|
|
const session = frame.session;
|
|
const referenceSpace = gl.xr.getReferenceSpace();
|
|
if (!session || !referenceSpace) return;
|
|
|
|
// Reset hasPose flags
|
|
ref.current.left.hasPose = false;
|
|
ref.current.right.hasPose = false;
|
|
|
|
for (const source of session.inputSources) {
|
|
if (source.handedness !== 'left' && source.handedness !== 'right') continue;
|
|
const slot = ref.current[source.handedness];
|
|
const gp = source.gamepad;
|
|
|
|
if (gp) {
|
|
// xr-standard mapping: [0]=trigger, [1]=squeeze/grip, [2]=touchpad, [3]=thumbstick
|
|
const trigBtn = gp.buttons[0];
|
|
const gripBtn = gp.buttons[1];
|
|
const gripValue = gripBtn ? (gripBtn.value || (gripBtn.pressed ? 1 : 0)) : 0;
|
|
const trigValue = trigBtn ? (trigBtn.value || (trigBtn.pressed ? 1 : 0)) : 0;
|
|
slot.gripValue = gripValue;
|
|
slot.triggerValue = trigValue;
|
|
// Hysteresis
|
|
if (!slot.isGrabbing && gripValue > GRAB_ON) {
|
|
slot.isGrabbing = true;
|
|
console.log(`[XR][grab] ▶ ${source.handedness} START (v=${gripValue.toFixed(2)})`);
|
|
} else if (slot.isGrabbing && gripValue < GRAB_OFF) {
|
|
slot.isGrabbing = false;
|
|
console.log(`[XR][grab] ◼ ${source.handedness} END`);
|
|
}
|
|
}
|
|
|
|
if (source.gripSpace) {
|
|
const pose = frame.getPose(source.gripSpace, referenceSpace);
|
|
if (pose) {
|
|
slot.gripWorld.fromArray(pose.transform.matrix);
|
|
slot.hasPose = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return ref;
|
|
}
|