/** * DEVKIT — In-world expanded HUD anchored to left controller (or fake left). * * Replaces the production XRDebugHud while devkit is on. Shows inputs, FPS, * model transform, plane count — readable inside the headset. */ import { useRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { Text } from '@react-three/drei'; import * as THREE from 'three'; import { sceneStats } from './sceneStatsStore'; import { fakeInput, isFakeActive } from './fakeInputStore'; const _m = new THREE.Matrix4(); const _p = new THREE.Vector3(); const _q = new THREE.Quaternion(); const _s = new THREE.Vector3(); const _off = new THREE.Vector3(0.10, 0.05, -0.05); // above wrist, away from face export function XRDevHud() { const groupRef = useRef(null); const textRef = useRef<{ text: string } & THREE.Object3D>(null); const { camera, gl } = useThree(); useFrame((_state, _dt, frame: XRFrame | undefined) => { if (!groupRef.current) return; let anchored = false; if (isFakeActive()) { _m.copy(fakeInput.left.gripWorld); anchored = true; } else if (frame) { const ref = gl.xr.getReferenceSpace(); if (ref) { for (const src of frame.session.inputSources) { if (src.handedness === 'left' && src.gripSpace) { const pose = frame.getPose(src.gripSpace, ref); if (pose) { _m.fromArray(pose.transform.matrix); anchored = true; break; } } } } } if (anchored) { _m.decompose(_p, _q, _s); const off = _off.clone().applyQuaternion(_q); groupRef.current.position.copy(_p).add(off); groupRef.current.quaternion.copy(_q); } else { // fallback: HUD floats in front of camera const off = new THREE.Vector3(-0.18, -0.10, -0.5).applyQuaternion(camera.quaternion); groupRef.current.position.copy(camera.position).add(off); groupRef.current.quaternion.copy(camera.quaternion); } if (textRef.current) { const lines = [ `FPS ${sceneStats.fps} XR ${sceneStats.xrMode ? 'on' : 'off'} fake:${isFakeActive() ? '1' : '0'}`, `inputs:${sceneStats.xrInputCount} planes:${sceneStats.xrPlanesCount}`, `meshes:${sceneStats.meshCount} tris:${sceneStats.triCount}`, `pos ${sceneStats.modelGroupPos.map((v) => v.toFixed(2)).join(' ')}`, `rot ${sceneStats.modelGroupRotDeg.map((v) => v.toFixed(0)).join(' ')}`, `bbox ${sceneStats.modelBboxSize.map((v) => v.toFixed(2)).join(' ')}`, `L g${fakeInput.left.gripValue.toFixed(2)} t${fakeInput.left.triggerValue.toFixed(2)}`, `R g${fakeInput.right.gripValue.toFixed(2)} t${fakeInput.right.triggerValue.toFixed(2)}`, ]; (textRef.current as unknown as { text: string }).text = lines.join('\n'); } }); return ( } fontSize={0.0075} color="#22c55e" anchorX="left" anchorY="top" position={[-0.018, 0.045, 0.001]} maxWidth={0.17} lineHeight={1.3} > {' '} ); }