Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
5.1 KiB
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.inputSourcesa cada frame e identifica osXRInputSource"left" e "right". - Para cada controle, expõe:
gripValue(0..1, analógico, vindo degamepad.buttons[2].value).triggerValue(0..1,buttons[1].value).isGrabbing(boolean com histerese: começa emvalue > 0.7, termina em< 0.3— evita flicker).gripPose: matrix4 da pose doinputSource.gripSpacenoreferenceSpaceatual, atualizada por frame viaXRFrame.getPose().
- Também escuta os eventos nativos
squeezestart/squeezeenddaXRSessioncomo 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 <XRModel/> em um <group> controlado e implementa as três modalidades de grab:
- One-hand grab: quando
isGrabbingde uma mão dispara, calcula matrix relativaM_offset = gripPose⁻¹ · modelPose. A cada frame, enquanto segura, fazmodelPose = 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).
- Pivô = ponto médio entre os dois
- Ao soltar (
isGrabbingcai), grava a pose final emuseModelStore.fineTuning(decompõe emposX/Y/Z,rotX/Y/Z, escalese 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).
posSteperotStepganham multiplicador derivado detriggerValueda 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 <group>.
5. UI: feedback visual e ajuda
XRGrabbablemostra um halo sutil ao redor da peça quandogripValue > 0.1em 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:
<XRHitTestPlacement> (posicionamento inicial)
<XRGrabbable> (grab 6DOF + two-hand)
<XRModel/>
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, ajustarControllerFineTuning(mapeamento + modulação por trigger + bypass quando grip ativo) - Editado:
src/stores/useModelStore.ts— camposcale - Editado:
src/components/three/ModelViewer.tsx— aplicarscale - Editado:
src/components/XRHud.tsx— toggle "permitir escala", ajuda nova