Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-21 21:05:48 +00:00
parent a203681849
commit c6d50b07a4
+67 -69
View File
@@ -1,83 +1,81 @@
## Objetivo
# Seleção de elementos IFC — Esconder, Isolar e Exportar GLB
Alinhar o comportamento dos controles Touch Plus (Meta Quest 3) ao padrão pedido:
Adicionar um modo de **seleção por elemento** (viga, chapa, etc.) que permite ao usuário escolher partes do modelo e então **esconder**, **isolar** ou **exportar como GLB reduzido** para reuso. Funciona no desktop e no AR/XR (Quest 3) em uma única fase.
- **Grip** = único responsável por mover a peça (pan, zoom, orbit).
- **Trigger** = apenas seleção, medição e ações pontuais (nunca move/rotaciona a peça).
- **Thumbstick** = navegação do usuário (teletransporte/recuar/girar a câmera), nunca mais ajuste fino do modelo.
- **Botão Menu** = mantém comportamento nativo (não tocar).
## Fluxo do usuário
## Mapeamento alvo
1. Usuário ativa botão **"Selecionar"** no ViewerControls (e no XRHud).
2. Clica em elementos do modelo — cada clique alterna seleção (highlight laranja/emissive).
3. Contador mostra "N elementos selecionados".
4. Três ações disponíveis:
- **Esconder selecionados** → some com eles
- **Isolar selecionados** → esconde todo o resto
- **Mostrar tudo** → restaura
5. Botão extra **"Exportar GLB"** salva arquivo `.glb` contendo apenas os elementos selecionados, baixável pelo navegador. Esse GLB pode ser recarregado no app pelo fluxo normal de import.
6. Estado de visibilidade é **persistido por `fileName`** no `localStorage`, igual ao `PLACEMENT_KEY`, sobrevivendo a recarregar a página e a sair/entrar do AR.
## Granularidade
Seleção opera no **elemento IFC inteiro** (uma viga, uma chapa). O converter de IFC preserva a hierarquia: cada `IfcProduct` vira um `Group` ou `Mesh` nomeado no GLB. O picking sobe na árvore até encontrar o ancestral marcado como "elemento IFC raiz" (campo `userData.ifcElement = true` adicionado durante a conversão), garantindo que clicar em qualquer face seleciona a viga toda.
## Detalhes técnicos
### 1. `src/lib/convertIFC.ts`
- Durante a conversão IFC→GLB, marcar cada `IfcProduct` resultante com `obj.userData.ifcElement = true` e `userData.ifcId = expressId`. Sem isso, a árvore three.js fica plana e não dá pra distinguir "elemento" de "submesh".
### 2. `src/stores/useModelStore.ts`
Novo slice de seleção:
```ts
selectionMode: boolean
selectedElementIds: Set<string> // chave: `${modelId}:${ifcId|uuid}`
hiddenElementIds: Set<string>
isolatedElementIds: Set<string> | null // null = sem isolamento
toggleSelection(key), clearSelection(),
hideSelected(), isolateSelected(), showAll()
```text
Mão Botão Ação
─────────────────────────────────────────────────────────
Esquerda/Direita Grip (1 mão) Pan da peça (translação 6DOF, sem rotação)
Ambas as mãos Grip + afastar Zoom uniforme (escala)
Ambas as mãos Grip + giro oposto Orbitar/rotacionar a peça
Direita Trigger Selecionar componente / adicionar ponto de medida
Esquerda Trigger Alternar snap de vértice (já existe)
Direita Botão A Desfazer última medida (já existe)
Direita Botão B Limpar medidas (já existe)
Thumbstick Frente + soltar Teletransporte para o ponto apontado
Thumbstick Lados Snap-rotate da câmera (±30°)
Thumbstick Trás Recuar o usuário (passo)
```
Persistência de `hiddenElementIds`/`isolatedElementIds` por `fileName` em `localStorage` (chave nova `tsxr_visibility_v1`), aplicada ao carregar o modelo.
### 3. `src/components/three/ModelViewer.tsx`
- Novo componente `SelectionHandler`: ativo quando `selectionMode === true`. Raycaster no `onPointerDown`, sobe a árvore via `object.parent` até achar `userData.ifcElement === true`, faz toggle no store. Desabilita OrbitControls durante o clique para não conflitar.
- Aplicar visibilidade: `useEffect` percorre `scene.traverse`, define `obj.visible` conforme `hiddenElementIds`/`isolatedElementIds`.
- Highlight de seleção: material `emissive` laranja (`#f59e0b`, mesma cor de holes) com `emissiveIntensity = 0.4`, restaurado ao deselecionar. Guardar material original em `userData.originalMaterial`.
- `selectionMode` é mutuamente exclusivo com `measureMode` e `positionMode` (mesma lógica que esses dois já têm entre si).
## Mudanças no código
### 4. `src/components/three/XRControllerMeasure.tsx` (ou novo `XRControllerSelect.tsx`)
- Quando `selectionMode === true` em XR: usar o mesmo ray do controller, no `selectstart` (trigger) faz toggle de seleção em vez de pontos de medição.
- Tip do laser muda para laranja durante o modo seleção.
### 1. `src/components/three/XRGrabbable.tsx`
### 5. `src/components/ViewerControls.tsx` + `src/components/XRHud.tsx`
Novos botões:
- `Selecionar` (toggle, ícone `MousePointerClick`)
- `Esconder` / `Isolar` / `Mostrar tudo` (visíveis quando `selectionMode` ou há ocultos)
- `Exportar GLB` (visível quando há seleção)
- Badge com contador "N selecionados"
- **One-hand grip**: passar a aplicar **apenas translação** (pan). Hoje aplica matriz completa do grip → leva rotação junto. Vou capturar o offset de posição e atualizar só `group.position`, mantendo `quaternion` e `scale`.
- **Two-hand grip**: manter rotação pelo eixo entre as mãos + **forçar `allowScale = true` sempre** (zoom passa a vir do grip, não depende mais do toggle do HUD).
- Remover a prop `allowScale` da assinatura (ou ignorar e tratar como `true`).
### 6. `src/lib/exportSelectionGLB.ts` (novo)
```ts
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
// Clona apenas os elementos selecionados em uma nova Scene,
// preserva transformações mundiais, exporta como .glb binário,
// dispara download via Blob + <a download>.
```
Nome do arquivo: `{original}_selecao_{N}elem.glb`. O GLB resultante pode ser aberto pelo próprio app via fluxo normal de import (já aceita `.glb`).
### 2. `src/pages/XRSession.tsx`
### 7. Performance
- Aplicar visibilidade via `obj.visible = false` (não remove geometria — reversível e barato).
- Frustum culling do three.js já ignora invisíveis no render → ganho real de FPS no Quest mesmo sem exportar.
- Remover o estado `allowScale` e os toggles correspondentes nas duas instâncias de `<XRGrabbable>`.
- Substituir `<ControllerFineTuning />` por um novo `<ControllerLocomotion />` (ver item 4) — o joystick não deve mais editar `fineTuning` do modelo.
- Garantir que `XRHitTestPlacement` só consome `onSelect` (trigger) quando `placementMode === true`; fora disso o trigger é roteado integralmente para `XRControllerMeasure`.
## Arquivos tocados
### 3. `src/components/XRHud.tsx` e `src/components/three/XRHudInWorld.tsx`
- `src/lib/convertIFC.ts`marcar `userData.ifcElement`
- `src/lib/exportSelectionGLB.ts`**novo**
- `src/stores/useModelStore.ts` — slice de seleção + persistência
- `src/components/three/ModelViewer.tsx``SelectionHandler`, highlight, aplicar visibilidade
- `src/components/three/XRControllerMeasure.tsx` — handler de seleção em XR
- `src/components/ViewerControls.tsx` — botões desktop
- `src/components/XRHud.tsx` — botões XR
- Remover botão/toggle “Permitir escala” (`allowScale`)agora é sempre via grip duplo.
- Remover/relabelar o toggle “Movimento livre” (`freeMove`) já que o joystick deixa de ajustar o modelo; manter apenas se ainda fizer sentido para outra função (caso não, remover prop também de `XRSession.tsx`).
## Fora do escopo (decidido)
### 4. Novo `src/components/three/ControllerLocomotion.tsx`
- **Não** exportar IFC reduzido (decidido: arquivo é só pra este app, GLB resolve e é trivial).
- **Não** suportar seleção por face/sub-mesh (decidido: elemento inteiro).
- **Não** salvar conjuntos nomeados de seleção ("layout A", "layout B") — pode virar Fase 2 se você pedir.
Componente que substitui `ControllerFineTuning`. Lê `useXR().session` e implementa:
- **Joystick esquerdo (ou ambos, configurável)**:
- `axes[1] < -0.7` mantido por ~200 ms → dispara teleport: raycast a partir do controle direito contra `scene.children` no plano horizontal mais próximo; aplica `xrOrigin.position` para reposicionar.
- `axes[1] > 0.7` → recua o `XROrigin` em passos de 0.3 m por trigger.
- `axes[0]` cruzando ±0.7 (com histerese) → snap-rotate de ±30° no `XROrigin.rotation.y`.
- Usar a API `XROrigin` já montada em `XRSession.tsx` (referência via `useRef` ou store) para escrever pose.
- Desligar tudo se `lockedActive` (modelo travado) — locomoção do usuário ainda funciona, só não move o modelo.
### 5. `src/components/three/XRControllerMeasure.tsx`
- Sem mudanças funcionais — já usa apenas trigger e botões A/B. Apenas confirmar que o early-return `(!measureMode && !selectionMode)` continua: quando nenhum dos modos está ativo, o trigger não dispara nada relacionado à medição (e não interfere em `XRHitTestPlacement`, que respeita `placementMode`).
### 6. `src/components/three/XRHitTestPlacement.tsx`
- Garantir que o handler `onSelect` (trigger) verifica `placementMode` antes de colocar o modelo. Hoje o tap-plane invisível pode estar capturando o select mesmo fora do placement — adicionar guarda explícita e `visible={placementMode}`.
## Validação
1. **Desktop SimXR** (devkit): pressionar Q/W (grip simulado) move a peça; pressionar A/S (trigger simulado) não muda pose nenhuma.
2. **Quest 3 real (passthrough)**:
- Grip único arrasta a peça sem rotacionar.
- Dois grips afastando = zoom; dois grips em rotação oposta = orbit.
- Trigger direito fora dos modos medir/selecionar/colocar = nada acontece.
- Trigger direito em modo Selecionar = seleciona componente IFC.
- Trigger direito em modo Medir = adiciona ponto com snap.
- Joystick esquerdo para frente = teletransporte; lados = snap-rotate; trás = recua.
3. Logs `[XR][grab]` continuam mostrando início/fim de one-hand e two-hand.
## Fora do escopo
- Não mexer no botão Menu (Universal Menu do Quest é nativo).
- Não alterar handlers DOM/mouse do modo desktop (Viewer).
- Não tocar em RLS, store de dados ou backend.