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). ```text
2. Clica em elementos do modelo — cada clique alterna seleção (highlight laranja/emissive). Mão Botão Ação
3. Contador mostra "N elementos selecionados". ─────────────────────────────────────────────────────────
4. Três ações disponíveis: Esquerda/Direita Grip (1 mão) Pan da peça (translação 6DOF, sem rotação)
- **Esconder selecionados** → some com eles Ambas as mãos Grip + afastar Zoom uniforme (escala)
- **Isolar selecionados** → esconde todo o resto Ambas as mãos Grip + giro oposto Orbitar/rotacionar a peça
- **Mostrar tudo** → restaura Direita Trigger Selecionar componente / adicionar ponto de medida
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. Esquerda Trigger Alternar snap de vértice (já existe)
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. Direita Botão A Desfazer última medida (já existe)
Direita Botão B Limpar medidas (já existe)
## Granularidade Thumbstick Frente + soltar Teletransporte para o ponto apontado
Thumbstick Lados Snap-rotate da câmera (±30°)
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. Thumbstick Trás Recuar o usuário (passo)
## 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()
``` ```
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` ## Mudanças no código
- 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).
### 4. `src/components/three/XRControllerMeasure.tsx` (ou novo `XRControllerSelect.tsx`) ### 1. `src/components/three/XRGrabbable.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.
### 5. `src/components/ViewerControls.tsx` + `src/components/XRHud.tsx` - **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`.
Novos botões: - **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).
- `Selecionar` (toggle, ícone `MousePointerClick`) - Remover a prop `allowScale` da assinatura (ou ignorar e tratar como `true`).
- `Esconder` / `Isolar` / `Mostrar tudo` (visíveis quando `selectionMode` ou há ocultos)
- `Exportar GLB` (visível quando há seleção)
- Badge com contador "N selecionados"
### 6. `src/lib/exportSelectionGLB.ts` (novo) ### 2. `src/pages/XRSession.tsx`
```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`).
### 7. Performance - Remover o estado `allowScale` e os toggles correspondentes nas duas instâncias de `<XRGrabbable>`.
- Aplicar visibilidade via `obj.visible = false` (não remove geometria — reversível e barato). - Substituir `<ControllerFineTuning />` por um novo `<ControllerLocomotion />` (ver item 4) — o joystick não deve mais editar `fineTuning` do modelo.
- Frustum culling do three.js já ignora invisíveis no render → ganho real de FPS no Quest mesmo sem exportar. - 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` - Remover botão/toggle “Permitir escala” (`allowScale`)agora é sempre via grip duplo.
- `src/lib/exportSelectionGLB.ts`**novo** - 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`).
- `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
## 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). Componente que substitui `ControllerFineTuning`. Lê `useXR().session` e implementa:
- **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. - **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.