Files
SteelXR2/.lovable/plan.md
T
gpt-engineer-app[bot] a8951935e6 Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
2026-05-14 17:58:59 +00:00

116 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Menus dentro do passthrough + Compartilhamento que funciona em AR
Dois problemas distintos, duas soluções:
---
## Problema 1 — Menus DOM somem no passthrough
**Causa:** `XRHud` é HTML/CSS sobreposto ao `<canvas>`. Quando o Quest entra em `immersive-ar`, o headset para de renderizar o DOM da página — só renderiza o conteúdo WebGL do `<XR>`. Por isso lista de peças, botões, slider de opacidade etc. desaparecem dentro do óculos.
**Solução:** criar um HUD 3D nativo (Three.js) que vive **dentro** da cena XR, ancorado ao controlador esquerdo (estilo "smartwatch de pulso") e/ou em frente ao usuário. Toda interação acontece via raycaster do controlador direito (gatilho = clique).
### Componentes a criar
**`src/components/three/XRPanel3D.tsx`** — painel 3D reutilizável:
- Quad com `Text` (drei) e fundo translúcido escuro
- Suporta hover/click via `onPointerOver` / `onClick` do react-three-fiber (já funciona com controllers XR)
- Variantes: `button`, `toggle`, `slider`, `list-item`
**`src/components/three/XRWristMenu.tsx`** — menu radial preso ao controlador esquerdo:
- Usa `useXRInputSourceState('controller', 'left')` para obter a pose
- Renderiza painel ~15cm acima do controle quando o usuário gira o pulso para cima (detecta orientação)
- Conteúdo: ícones grandes para cada categoria (Cena, Ferramentas, Inspeção, Compartilhar, Ajuda)
**`src/components/three/XRToolPanel.tsx`** — painel grande flutuante (40×30cm) que abre quando o usuário escolhe uma categoria no wrist menu:
- Posicionado a ~70cm em frente ao usuário, segue o head (com inércia leve)
- Botão "fechar" (X) no canto
- Conteúdo varia por aba aberta:
- **Cena**: lista de modelos carregados (selecionar ativo, visibilidade, lock, duplicar, remover) + botão "+ Adicionar" que dispara o picker de arquivo (file input fica oculto, ativado via `<a>` com `download` ou via mensagem para sair do XR e abrir loader)
- **Ferramentas**: opacidade (slider 3D), modo render (Sólido/Wire/Bordas), grid on/off, medir on/off, snap on/off, escala on/off
- **Posição/Rotação**: 6 pares de botões /+ (já existem como joystick, mas duplicar no painel ajuda quem prefere clicar)
- **Inspeção**: lista de itens do checklist, cada um com ✓/✗
- **Compartilhar**: estado da live (ON/OFF), código da sala, contagem de viewers, botão encerrar
### Carregar arquivo dentro do XR
Carregar arquivos do disco local exige fileinput DOM, que não existe no passthrough. Duas alternativas:
1. **Cloud only no XR**: o painel "Cena → Adicionar" lista os modelos do Supabase Storage / cache local que já foram pré-carregados antes de entrar no AR. Para arquivo novo, mostra mensagem "saia do AR para fazer upload".
2. **Pré-carregar tudo antes**: usuário carrega todos os IFCs no Viewer 2D, marca quais quer levar, e dentro do AR só alterna entre eles.
Vou implementar (1) — mais flexível.
### Edits em `XRSession.tsx`
Dentro do bloco `effectiveInXR`, montar `<XRWristMenu />` e `<XRToolPanel />`. O `<XRHud>` DOM atual continua existindo para uso fora do AR (desktop preview e antes de entrar em sessão).
### Edits em `XRHud.tsx`
Sem mudanças funcionais — só passa a ser ocultado via CSS quando `inXR === true` (já é, por causa do passthrough, mas garantir `display:none` evita confusão).
---
## Problema 2 — Viewers veem tela preta no passthrough
**Causa:** `canvas.captureStream()` captura o `<canvas>` WebGL com `alpha: true` e `clearColor (0,0,0,0)` — ou seja, **transparente**. No headset, o compositor do sistema operacional do Quest mistura esse canvas transparente com o feed da câmera (passthrough). Mas o canvas em si, quando capturado e enviado via WebRTC, é só os modelos 3D sobre fundo transparente — viewers veem preto/transparente.
**Pior ainda:** durante uma sessão `immersive-ar`, a cada frame WebXR o WebGL renderiza diretamente para a XR layer; o `<canvas>` da página pode ficar congelado no último frame de fora do XR (o "captureStream" para de ter conteúdo novo).
**Solução:** criar um **segundo renderer offscreen opaco** que reproduz a cena XR a cada frame, e capturar o stream desse canvas.
### Componente: `src/components/three/XRBroadcastMirror.tsx`
```text
useFrame((state) => {
if (!broadcasting) return;
// 1. Pega câmera XR atual (state.gl.xr.getCamera())
// 2. Renderiza a mesma scene num WebGLRenderer offscreen
// com alpha:false, clearColor preto/cinza
// 3. O canvas do mirror tem captureStream() ativo
});
```
Detalhes:
- Cria `THREE.WebGLRenderer({ canvas: offscreenCanvas, alpha: false, antialias: true })`
- Tamanho fixo: 1280×720 (boa relação qualidade/banda para WebRTC)
- Usa a câmera XR ativa (`state.gl.xr.getCamera()`) — que é a câmera estéreo combinada do Quest; pegamos a sub-câmera esquerda para gerar mono feed
- Renderiza a mesma `state.scene`
- Opcionalmente, sobrepõe um plano com cor de fundo (cinza escuro #1a1a1a) representando o que seria o passthrough — viewers entendem que é o ponto de vista do operador, sem ver a câmera real (privacidade do ambiente)
### Mudanças em `ShareButton.tsx` / `broadcaster.ts`
Em vez de capturar `document.querySelector('canvas')` direto, usar:
1. Se não estiver em XR → comportamento atual (canvas principal).
2. Se estiver em XR → o `XRBroadcastMirror` registra seu canvas num contexto/ref global; `ShareButton.start()` lê esse canvas e chama `captureStream()` nele.
Adicionar `src/lib/webrtc/broadcastSource.ts`:
```text
let activeCanvas: HTMLCanvasElement | null = null;
export function setBroadcastSource(c: HTMLCanvasElement | null) { activeCanvas = c; }
export function getBroadcastSource() { return activeCanvas; }
```
`XRBroadcastMirror` chama `setBroadcastSource` no mount/unmount.
### Sobre câmera real do Quest
Acessar o feed real da câmera passthrough **não é possível via WebXR** (Meta bloqueia por privacidade — só apps nativos com WebView de sistema têm acesso). Então o melhor que conseguimos mostrar para os viewers é a **cena 3D do ponto de vista do operador, em fundo opaco**. Isso já é útil: o supervisor vê a peça, os ajustes, as medições, o checklist sendo marcado.
---
## Limitações e fora de escopo
- Sem captura real da câmera passthrough (limite WebXR/Meta).
- Mirror renderiza só olho esquerdo (mono) — estéreo dobraria custo de GPU e banda.
- O FPS do mirror pode cair se o headset estiver no limite — adicionar throttle (`captureStream(20)` em vez de 30).
- Wrist menu funciona com controllers; mãos (hand tracking) ficam para v2.
## Arquivos tocados
**Criar:**
- `src/components/three/XRPanel3D.tsx`
- `src/components/three/XRWristMenu.tsx`
- `src/components/three/XRToolPanel.tsx`
- `src/components/three/XRBroadcastMirror.tsx`
- `src/lib/webrtc/broadcastSource.ts`
**Editar:**
- `src/pages/XRSession.tsx` — montar wrist menu, tool panel e broadcast mirror dentro do `<XR>`
- `src/components/ShareButton.tsx` — preferir `getBroadcastSource()` quando disponível
- `src/components/XRHud.tsx` — esconder via CSS quando `inXR`