diff --git a/src/lab/QuestCubeLab.tsx b/src/lab/QuestCubeLab.tsx index 5eb5bdf..22b646a 100644 --- a/src/lab/QuestCubeLab.tsx +++ b/src/lab/QuestCubeLab.tsx @@ -24,12 +24,15 @@ import { Suspense } from "react"; /** * ============================================================ * QUEST CUBE LAB — Fase 2.5 (PHYSICS HLA-LIKE) - * - Física real com Rapier3D (gravidade, colisões, impulso) - * - Cubo é um RigidBody dinâmico (cai, bate, joga) - * - Mesa é RigidBody estático (cubo pousa em cima) - * - Pega o cubo com a mão virtual do Quest 3 - * - Joga o cubo com impulso (igual Half-Life: Alyx) - * - Mantém sistema 2-cliques FASE 4 (contato + direção) + * FIX v4 — AR Passthrough funcional em Meta Quest 3 + * + * Mudanças principais vs. versão anterior (3f01190): + * 1. ÁRVORE: ... (era ) + * — chama useThree() e useFrame() e PRECISA estar dentro do Canvas + * 2. REMOVIDO em AR — opacificava passthrough (issue pmndrs/xr#344) + * 3. requiredFeatures removido — usa store.enterAR() que respeita config do xrStore + * 4. gl.xr.enabled manual removido — agora é controlado pelo + * 5. cleanup: session.end() ao desmontar * ============================================================ */ @@ -74,11 +77,8 @@ const ARHudMessages = ({ alignPhase }: { alignPhase: AlignPhase }) => { // =================================================================== // CUBO FÍSICO com Rapier — HLA-like // =================================================================== -// (Implementado abaixo, depois dos imports do Rapier) - import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier"; -// Cubo com física rígida type CubeProps = { position: [number, number, number]; locked: boolean; @@ -116,15 +116,12 @@ const PhysicsCube = ({ position, locked, onFaceClick, resetSignal }: CubeProps) linearDamping={0.1} angularDamping={0.3} > - {/* collider gerado pra casar com a geometria do cubo */} - {/* 6 faces coloridas — HLA-like aesthetic */} - {/* marcação visual das faces usando overlays */} {[ { pos: [CUBE_SIZE / 2 + 0.001, 0, 0], rot: [0, Math.PI / 2, 0], color: "#ef4444" }, { pos: [-CUBE_SIZE / 2 - 0.001, 0, 0], rot: [0, -Math.PI / 2, 0], color: "#22c55e" }, @@ -147,7 +144,7 @@ const PhysicsCube = ({ position, locked, onFaceClick, resetSignal }: CubeProps) }; // =================================================================== -// MESA FÍSICA — superfície de contato (estática) +// MESA FÍSICA // =================================================================== type TableProps = { onSurfaceClick: () => void; @@ -157,7 +154,6 @@ type TableProps = { const PhysicsTable = ({ onSurfaceClick, onEdgeClick }: TableProps) => { return ( - {/* Tampo superior (onde o cubo pousa) */} { - {/* Prateleira do meio (segunda superfície) */} { - {/* 4 pernas */} {[ [-0.95, 0.3, -0.95], [0.95, 0.3, -0.95], @@ -205,7 +199,6 @@ const PhysicsTable = ({ onSurfaceClick, onEdgeClick }: TableProps) => { ))} - {/* Arestas laterais (4 colunas — detectáveis pro alinhamento) */} {[ { pos: [-0.98, 0.4, 0], rot: [0, 0, 0] }, { pos: [0.98, 0.4, 0], rot: [0, 0, 0] }, @@ -230,7 +223,7 @@ const PhysicsTable = ({ onSurfaceClick, onEdgeClick }: TableProps) => { }; // =================================================================== -// COMPONENTE DO CUBO COM SISTEMA 2-CLIQUES (FASE 4) +// SCENE — sistema 2-cliques FASE 4 // =================================================================== type FaceId = "x+" | "x-" | "y+" | "y-" | "z+" | "z-"; type AlignmentState = { @@ -275,7 +268,6 @@ const PhysicsScene = ({ const onEdge = () => { if (alignPhase === "WAIT_EDGE") { setAlign({ ...align, edge: "norte" }); - // Após os 2 cliques, reseta pra próximo round setTimeout(() => { setAlignPhase("IDLE"); setAlign({ contactFace: null, surface: null, directionFace: null, edge: null }); @@ -287,7 +279,6 @@ const PhysicsScene = ({ <> - {/* Plano do chão — gigante, invisível na prática, recebe sombras */} @@ -296,7 +287,6 @@ const PhysicsScene = ({ - {/* O cubo dinâmico */} - {/* A mesa — pega cliques pra 2-click system */} @@ -312,10 +301,24 @@ const PhysicsScene = ({ }; // =================================================================== -// SCENE WRAPPER — junta o Canvas + R3F +// FUNDO DINÂMICO — NUNCA usa quando AR estiver ativo +// (issue pmndrs/xr#344: background color opacifica o passthrough) +// =================================================================== +const ARBackground = ({ isAR }: { isAR: boolean }) => { + if (isAR) return null; + + return ( + <> + + + + ); +}; + +// =================================================================== +// SCENE WRAPPER // =================================================================== const SceneContent = ({ locked, resetSignal }: { locked: boolean; resetSignal: number }) => { - // Detecta modo AR para ajustar iluminação e fundo const session = useXR((s) => s.session); const isAR = !!(session && (session as any).environmentBlendMode === "additive"); @@ -331,7 +334,6 @@ const SceneContent = ({ locked, resetSignal }: { locked: boolean; resetSignal: n shadow-mapSize={[1024, 1024]} /> - {/* Grid estilo HLA — funciona em ambos os modos */} { - // Em AR Passthrough: nada (canal alpha fica livre pra passthrough) - if (isAR) return null; - - // Fora de AR (modo desktop ou VR): fundo preto - return ( - <> - - - - ); -}; - -// =================================================================== -// JOYSTICK HANDLER — gamepad pro controle fora do XR +// GAMEPAD HANDLER // =================================================================== const GamepadHandler = () => { const [connected, setConnected] = useState(false); @@ -408,7 +394,7 @@ const GamepadHandler = () => { const rsx = Math.abs(pad.axes[2]) > STICK_DEADZONE ? pad.axes[2] : 0; const rsy = Math.abs(pad.axes[3]) > STICK_DEADZONE ? pad.axes[3] : 0; if (lsx || lsy || rsx || rsy) { - // só log pra debug — controle real via Cubo físico + // log para debug } } raf = requestAnimationFrame(tick); @@ -429,43 +415,33 @@ export default function QuestCubeLab() { const [autoEnter, setAutoEnter] = useState(false); useEffect(() => { - if (autoEnter) { - // Meta Quest 3 suporta immersive-ar (Passthrough AR). Tenta primeiro. - // Fallback pra immersive-vr se AR não disponível (óculos sem passthrough). - (async () => { + if (!autoEnter) return; + (async () => { + try { + // ✅ FIX: usa a API do store (createXRStore.enterAR) em vez de chamar + // navigator.xr.requestSession cru — alinha com o resto do projeto + await store.enterAR(); + } catch (err) { + console.warn("[Lab] Falha em enterAR, tentando enterVR:", err); try { - if ('xr' in navigator) { - const xr = (navigator as any).xr; - const supported: boolean = await xr.isSessionSupported('immersive-ar').catch(() => false); - if (supported) { - const sessionInit: XRSessionInit = { - requiredFeatures: ['local-floor'], - optionalFeatures: ['hand-tracking', 'plane-detection', 'hit-test'], - }; - await xr.requestSession('immersive-ar', sessionInit); - return; - } - } - // Fallback: usa o XR controller padrão do store - const ctrl: any = store as any; - const enterFn = ctrl.enterAR || ctrl.enterVR || ctrl.enterXR; - try { - await enterFn.call(ctrl, 'immersive-vr' as any); - } catch { - // ignore - } - } catch { - // qualquer erro, tenta o enter padrão - try { - await Promise.resolve(store.enterXR?.()); - } catch { - // ignore - } + await store.enterVR(); + } catch (err2) { + console.error("[Lab] Falha em enterVR também:", err2); } - })(); - } + } + })(); }, [autoEnter]); + // ✅ FIX: cleanup ao desmontar (issue pmndrs/xr#490) + useEffect(() => { + return () => { + const session = store.getState().session; + if (session) { + try { session.end(); } catch {} + } + }; + }, []); + const reset = () => { setResetSignal((n) => n + 1); }; @@ -525,20 +501,26 @@ export default function QuestCubeLab() { Rapier Physics ativo • Half-Life: Alyx-like - {/* Canvas com XR — gl.alpha:true permite passthrough por trás */} - - { - gl.setClearColor(0x000000, 0); // transparente - gl.xr.enabled = true; - }} - > + {/* ✅ FIX PRINCIPAL: árvore CORRETA — Canvas por fora, XR por dentro */} + { + gl.setClearColor(0x000000, 0); + // gl.xr.enabled agora é controlado pelo + }} + > + - - + + ); }