## Plano: Grab nativo dos controles Touch Plus (Quest 3) para "vestir" a peça virtual Objetivo: permitir que o usuário **agarre a peça virtual com o grip** do Touch Plus e a movimente em 6 graus de liberdade junto com a mão (translação + rotação simultâneas), e use **as duas mãos juntas** para girar/escalar com naturalidade. Joystick atual continua existindo como ajuste fino. ### 1. Camada de input nova: `useControllerGrab` Novo hook em `src/hooks/useControllerGrab.ts` que encapsula tudo que falta hoje: - Lê `session.inputSources` a cada frame e identifica os `XRInputSource` "left" e "right". - Para cada controle, expõe: - `gripValue` (0..1, analógico, vindo de `gamepad.buttons[2].value`). - `triggerValue` (0..1, `buttons[1].value`). - `isGrabbing` (boolean com histerese: começa em `value > 0.7`, termina em `< 0.3` — evita flicker). - `gripPose`: matrix4 da pose do `inputSource.gripSpace` no `referenceSpace` atual, atualizada por frame via `XRFrame.getPose()`. - Também escuta os eventos nativos `squeezestart` / `squeezeend` da `XRSession` como fallback caso o gamepad não exponha valor analógico. ### 2. Componente `XRGrabbable` em volta da peça Novo arquivo `src/components/three/XRGrabbable.tsx`. Wrappa o `` em um `` controlado e implementa as três modalidades de grab: - **One-hand grab**: quando `isGrabbing` de uma mão dispara, calcula matrix relativa `M_offset = gripPose⁻¹ · modelPose`. A cada frame, enquanto segura, faz `modelPose = gripPose · M_offset`. Resultado: a peça vira "extensão da mão", com translação + rotação simultâneas. - **Two-hand grab** (quando ambas as mãos estão segurando): - Pivô = ponto médio entre os dois `gripPose`. - Rotação = orientação do vetor entre as duas mãos (mantém o cabo da viga sempre alinhado com o eixo entre os punhos). - Escala = razão entre a distância atual e a distância no momento em que a 2ª mão agarrou (com clamp 0.1× a 10×). **Opcional**: pode ser desabilitada na HUD para usos onde a escala 1:1 é obrigatória (caso "vestir" sobre peça real). - Ao soltar (`isGrabbing` cai), grava a pose final em `useModelStore.fineTuning` (decompõe em `posX/Y/Z`, `rotX/Y/Z`, e `scale` se two-hand estiver ativo) — assim o estado fica consistente com o joystick e os controles do HUD. ### 3. Joystick continua, agora modulado pelo grip analógico Em `ControllerFineTuning` (dentro de `XRSession.tsx`): - Mantém o mapeamento atual mas **só ativa quando NENHUM grip está apertado** (para não brigar com o grab). - `posStep` e `rotStep` ganham multiplicador derivado de `triggerValue` da mesma mão: trigger leve = passo fino (×0.3), trigger forte = passo rápido (×3). Resolve a queixa de "movimento mecânico". - Mapeamento corrigido para o padrão VR (já discutido): - Esquerdo: X horizontal / Z vertical (frente-trás). - Direito: rotY horizontal / Y altura vertical. - Adiciona aceleração proporcional (curva quadrática no input do thumbstick) e deadzone de 0.15. ### 4. Escala no store `src/stores/useModelStore.ts`: adicionar `scale: number` ao `FineTuning` (default 1). `XRModel` e `ModelViewer` aplicam `scale={[s,s,s]}` no ``. ### 5. UI: feedback visual e ajuda - `XRGrabbable` mostra um halo sutil ao redor da peça quando `gripValue > 0.1` em qualquer mão (intensidade proporcional → o usuário sente a pressão analógica). - `src/components/XRHud.tsx`: na seção de Ajuda (?), adicionar bloco "Agarrar a peça": - "Aperte o grip lateral de um controle perto da peça → ela vira extensão da sua mão." - "Agarre com os dois grips ao mesmo tempo → rotacione girando os punhos / aproxime-afaste para escalar." - "Solte o grip para fixar." - "Trigger modula a velocidade do joystick (leve = fino, forte = rápido)." - Adicionar toggle "Permitir escala (two-hand)" — default OFF para preservar 1:1. ### 6. Integração e ordem de aplicação No `XRSession.tsx`, a árvore vira: ``` (posicionamento inicial) (grab 6DOF + two-hand) ``` Hit-test só atua enquanto `placementMode = true`. Quando o usuário agarra pela primeira vez, `placementMode` é desligado automaticamente. ### Detalhes técnicos - Tudo via WebXR Gamepad API + `XRFrame.getPose(gripSpace, referenceSpace)`. Sem dependências novas. - Histerese do grip evita perder o grab por tremor de mão. - Decomposição de matrix usa `THREE.Matrix4.decompose()` para gravar de volta no store sem perder rotação composta. - Plane snapping continua ativo durante o grab (snap acontece no `onRelease`, não a cada frame, para não travar o movimento). - Logs `[XR][grab]` para diagnóstico (start/end, qual mão, valor analógico). ### Arquivos afetados - **Novo**: `src/hooks/useControllerGrab.ts` - **Novo**: `src/components/three/XRGrabbable.tsx` - **Editado**: `src/pages/XRSession.tsx` — envolver modelo, ajustar `ControllerFineTuning` (mapeamento + modulação por trigger + bypass quando grip ativo) - **Editado**: `src/stores/useModelStore.ts` — campo `scale` - **Editado**: `src/components/three/ModelViewer.tsx` — aplicar `scale` - **Editado**: `src/components/XRHud.tsx` — toggle "permitir escala", ajuda nova