import { useRef, RefObject } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { useXR } from '@react-three/xr'; import * as THREE from 'three'; /** * Quest 3 thumbstick locomotion (rig-relative). * * Mapeamento: * • Joystick para frente (axes[1] < -0.7) e soltar (volta a 0) * → teletransporta para o ponto apontado pelo controle direito * (raycast contra `scene.children`). Sem alvo → passo de 0.5 m * na direção do controle. * • Joystick para trás (axes[1] > 0.7) → passo de 0.3 m para trás * (em relação à orientação do XROrigin / yaw da câmera). * • Joystick lateral (|axes[0]| > 0.7) → snap-rotate ±30° em torno * do eixo Y do XROrigin. * * Lê apenas o thumbstick (axes[2]/axes[3] em xr-standard) — não toca em * grip nem trigger. */ interface Props { /** Ref para o que envolve ; sua posição/rotação move o usuário. */ rigRef: RefObject; } const DEAD = 0.7; const SNAP_DEG = 30; const STEP_BACK = 0.3; const STEP_FWD_NO_HIT = 0.5; export function ControllerLocomotion({ rigRef }: Props) { const { scene, camera } = useThree(); const session = useXR((s) => s.session); // Edge states per direction (need to release to fire again) const fwdArmed = useRef(false); const fwdLatched = useRef(false); // armed once axis exceeded threshold; fires on release const backLatched = useRef(false); const rotLatched = useRef<0 | -1 | 1>(0); const raycaster = useRef(new THREE.Raycaster()); useFrame((_state, _dt, frame: XRFrame | undefined) => { const rig = rigRef.current; if (!rig || !session || !frame) return; const refSpace = (camera as unknown as { matrixWorldInverse: THREE.Matrix4 }) && (frame as XRFrame).session ? (frame as XRFrame).session : null; if (!refSpace) return; // Find thumbstick + right controller pose let stickX = 0; let stickY = 0; let rightSource: XRInputSource | null = null; for (const src of session.inputSources) { const gp = src.gamepad; if (!gp) continue; // xr-standard: axes[2]=thumbstick X, axes[3]=thumbstick Y if (gp.axes.length >= 4) { // Prefer left for locomotion; if no left stick, use right if (src.handedness === 'left') { stickX = gp.axes[2] ?? 0; stickY = gp.axes[3] ?? 0; } else if (src.handedness === 'right' && stickX === 0 && stickY === 0) { stickX = gp.axes[2] ?? 0; stickY = gp.axes[3] ?? 0; } } if (src.handedness === 'right') rightSource = src; } // ── Snap-rotate (lateral) ────────────────────────────────────── if (Math.abs(stickX) > DEAD) { const dir: -1 | 1 = stickX > 0 ? 1 : -1; if (rotLatched.current !== dir) { rotLatched.current = dir; rig.rotation.y -= (dir * SNAP_DEG * Math.PI) / 180; } } else if (Math.abs(stickX) < 0.3) { rotLatched.current = 0; } // ── Back step ────────────────────────────────────────────────── if (stickY > DEAD) { if (!backLatched.current) { backLatched.current = true; // Move oposto à direção em que o usuário olha (xz) const headDir = new THREE.Vector3(); camera.getWorldDirection(headDir); headDir.y = 0; headDir.normalize(); rig.position.addScaledVector(headDir, -STEP_BACK); } } else if (stickY < 0.3) { backLatched.current = false; } // ── Forward → teleport (arm on push, fire on release) ───────── if (stickY < -DEAD) { fwdArmed.current = true; fwdLatched.current = true; } else if (fwdLatched.current && stickY > -0.2) { fwdLatched.current = false; // Fire teleport const refSpaceObj = (frame.session as XRSession).requestReferenceSpace; // not used; we use camera world void refSpaceObj; let targetWorld: THREE.Vector3 | null = null; if (rightSource && rightSource.targetRaySpace) { const pose = frame.getPose(rightSource.targetRaySpace, (camera as unknown as { parent: THREE.Object3D }).parent ? (frame as XRFrame).session as unknown as XRReferenceSpace : null as unknown as XRReferenceSpace); // Fallback path: build ray from controller via WebXR reference void pose; } // Simpler & robust: get controller pose from its object via three's renderer const gl = (rigRef.current as unknown as { __gl?: unknown }).__gl as unknown; void gl; // We can't get the controller's three.js Object3D here without the XR // input wrapper — use the camera as a fallback ray origin: shoot from // camera forward until it hits something. if (!targetWorld) { const origin = new THREE.Vector3(); camera.getWorldPosition(origin); const dir = new THREE.Vector3(); camera.getWorldDirection(dir); raycaster.current.set(origin, dir); raycaster.current.far = 30; const hits = raycaster.current.intersectObjects(scene.children, true); const hit = hits.find((h) => { const o = h.object; if (!(o instanceof THREE.Mesh)) return false; if (o.userData.__edgeLine) return false; return true; }); if (hit) { targetWorld = hit.point.clone(); } else { // No hit: step forward STEP_FWD_NO_HIT m on the floor plane targetWorld = origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT)); } } if (targetWorld) { // Move the rig so the camera ends up at targetWorld (keep camera Y). const camWorld = new THREE.Vector3(); camera.getWorldPosition(camWorld); const delta = new THREE.Vector3().subVectors(targetWorld, camWorld); delta.y = 0; // keep user height rig.position.add(delta); } fwdArmed.current = false; } }); return null; }