From 705d7b171bd8874e0c53259f004f0813561569d6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:07:52 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ControllerLocomotion.tsx | 149 +++++++----------- 1 file changed, 54 insertions(+), 95 deletions(-) diff --git a/src/components/three/ControllerLocomotion.tsx b/src/components/three/ControllerLocomotion.tsx index 2a9c1cf..bc8f9a1 100644 --- a/src/components/three/ControllerLocomotion.tsx +++ b/src/components/three/ControllerLocomotion.tsx @@ -6,25 +6,23 @@ 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. + * Mapeamento (thumbstick = axes[2]/axes[3] em xr-standard): + * • Para frente (axes[1] < -0.7), libera (axis volta a ~0) → teletransporta + * para o ponto onde o usuário está olhando (raycast da câmera). Se não + * houver geometria atingida, dá um passo de 0.5 m na direção do olhar. + * • Para trás (axes[1] > 0.7) → passo de 0.3 m oposto ao olhar. + * • Lateral (|axes[0]| > 0.7) → snap-rotate ±30° no eixo Y do rig. * - * Lê apenas o thumbstick (axes[2]/axes[3] em xr-standard) — não toca em - * grip nem trigger. + * NÃO lê grip ou trigger — esses são responsabilidade exclusiva de + * XRGrabbable e XRControllerMeasure, respectivamente. */ interface Props { - /** Ref para o que envolve ; sua posição/rotação move o usuário. */ + /** Ref para o que envolve ; sua pose move o usuário. */ rigRef: RefObject; } const DEAD = 0.7; +const RELEASE = 0.3; const SNAP_DEG = 30; const STEP_BACK = 0.3; const STEP_FWD_NO_HIT = 0.5; @@ -33,42 +31,30 @@ 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 fwdPushed = useRef(false); const backLatched = useRef(false); const rotLatched = useRef<0 | -1 | 1>(0); - const raycaster = useRef(new THREE.Raycaster()); - useFrame((_state, _dt, frame: XRFrame | undefined) => { + useFrame(() => { const rig = rigRef.current; - if (!rig || !session || !frame) return; + if (!rig || !session) 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 (!gp || gp.axes.length < 4) continue; + // Prefer left stick for locomotion + if (src.handedness === 'left') { + stickX = gp.axes[2] ?? 0; + stickY = gp.axes[3] ?? 0; + break; + } + 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) ────────────────────────────────────── @@ -78,7 +64,7 @@ export function ControllerLocomotion({ rigRef }: Props) { rotLatched.current = dir; rig.rotation.y -= (dir * SNAP_DEG * Math.PI) / 180; } - } else if (Math.abs(stickX) < 0.3) { + } else if (Math.abs(stickX) < RELEASE) { rotLatched.current = 0; } @@ -86,70 +72,43 @@ export function ControllerLocomotion({ rigRef }: Props) { 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); + headDir.y = 0; + if (headDir.lengthSq() > 1e-6) { + headDir.normalize(); + rig.position.addScaledVector(headDir, -STEP_BACK); + } } - } else if (stickY < 0.3) { + } else if (stickY < RELEASE) { backLatched.current = false; } - // ── Forward → teleport (arm on push, fire on release) ───────── + // ── Forward → teleport 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; + fwdPushed.current = true; + } else if (fwdPushed.current && stickY > -RELEASE) { + fwdPushed.current = false; + // Cast from camera forward to find target on real/virtual geometry + 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; + }); + const target = hit ? hit.point.clone() : origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT)); + const camWorld = new THREE.Vector3(); + camera.getWorldPosition(camWorld); + const delta = new THREE.Vector3().subVectors(target, camWorld); + delta.y = 0; // preserva altura do usuário + rig.position.add(delta); } });