/** * DEVKIT — Visual fake controllers + keyboard driver. * * Renders two spheres (L=blue, R=red) at the fake controller positions and * listens to global keyboard for grip/trigger/movement. Mounted only when * SimXR is active. * * Keys: * Q / W → hold grip Left / Right (analog = 1.0) * A / S → hold trigger Left / Right (analog = 1.0) * 1 / 2 → select Left / Right as the active hand * Arrows → move active hand on X (←→) / Y (↑↓) * Shift+Arrows→ move active hand on Z (↑=forward, ↓=back) * R → reset hand poses */ import { useEffect, useRef, useState } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { fakeInput, setFakeActive } from './fakeInputStore'; const STEP = 0.03; // m per keypress export function FakeControllers() { const leftMesh = useRef(null); const rightMesh = useRef(null); const [, force] = useState(0); const activeHand = useRef<'left' | 'right'>('right'); useEffect(() => { setFakeActive(true); return () => setFakeActive(false); }, []); useEffect(() => { const keys = new Set(); const apply = () => { const pos = new THREE.Vector3(); const quat = new THREE.Quaternion(); const scl = new THREE.Vector3(); const hand = fakeInput[activeHand.current]; hand.gripWorld.decompose(pos, quat, scl); const dx = (keys.has('ArrowRight') ? STEP : 0) - (keys.has('ArrowLeft') ? STEP : 0); const dyOrZ = (keys.has('ArrowUp') ? STEP : 0) - (keys.has('ArrowDown') ? STEP : 0); const shift = keys.has('Shift'); pos.x += dx; if (shift) pos.z -= dyOrZ; // up = forward (-Z) else pos.y += dyOrZ; hand.gripWorld.compose(pos, quat, scl); // grip / trigger fakeInput.left.gripValue = keys.has('q') ? 1 : 0; fakeInput.right.gripValue = keys.has('w') ? 1 : 0; fakeInput.left.triggerValue = keys.has('a') ? 1 : 0; fakeInput.right.triggerValue = keys.has('s') ? 1 : 0; force((n) => n + 1); }; const onDown = (e: KeyboardEvent) => { const k = e.key === 'Shift' ? 'Shift' : e.key; keys.add(k); if (k === '1') activeHand.current = 'left'; else if (k === '2') activeHand.current = 'right'; else if (k === 'r' || k === 'R') { fakeInput.left.gripWorld.makeTranslation(-0.25, 1.2, -0.5); fakeInput.right.gripWorld.makeTranslation(0.25, 1.2, -0.5); force((n) => n + 1); return; } apply(); }; const onUp = (e: KeyboardEvent) => { const k = e.key === 'Shift' ? 'Shift' : e.key; keys.delete(k); apply(); }; window.addEventListener('keydown', onDown); window.addEventListener('keyup', onUp); return () => { window.removeEventListener('keydown', onDown); window.removeEventListener('keyup', onUp); }; }, []); // Sync mesh visuals to fake state each frame useFrame(() => { if (leftMesh.current) { const p = new THREE.Vector3().setFromMatrixPosition(fakeInput.left.gripWorld); leftMesh.current.position.copy(p); const grabbing = fakeInput.left.gripValue > 0.7; const mat = leftMesh.current.material as THREE.MeshStandardMaterial; mat.emissiveIntensity = grabbing ? 1.5 : 0.4; } if (rightMesh.current) { const p = new THREE.Vector3().setFromMatrixPosition(fakeInput.right.gripWorld); rightMesh.current.position.copy(p); const grabbing = fakeInput.right.gripValue > 0.7; const mat = rightMesh.current.material as THREE.MeshStandardMaterial; mat.emissiveIntensity = grabbing ? 1.5 : 0.4; } }); return ( <> ); }