## Objetivo Tornar a ferramenta **Medir** confiável tanto no desktop (mouse) quanto no modo AR (controles Touch Plus), com snap de pontos a vértices/arestas, marcadores visíveis em qualquer escala e labels com texto sempre legível. ## Problemas identificados 1. **Mouse não cria pontos**: o `MeasureClickHandler` usa o evento DOM `click` no canvas, que conflita com o `onClick` interno do R3F no `` do `GLBModel` (`setActive`). O `stopPropagation` do R3F não pára o evento DOM, mas o caminho que chega lá ignora se o mouse arrastou, e o `pointerdown` global de `PositionDragHandler` (que está sempre montado) chama `setPointerCapture` quando `positionMode` está ligado — em modo Medir não captura, mas o handler `click` puro é frágil. Vamos trocar por `pointerdown/up` próprios com gate de "não foi arrasto". 2. **Sem feedback de "ponto A pendente"** claro no AR — o ponto fica pequeno demais quando escala é 1:50. 3. **Tamanho dos marcadores e do texto é fixo em unidades de mundo** (`sphereGeometry [0.003]`, `fontSize 0.008`), então some quando a peça está em 1:1 grande ou fica enorme em 1:100. 4. **AR só tem gatilho** — sem desfazer último ponto, sem limpar, sem indicação visual de "armando 2º ponto". 5. **Snap de vértice só funciona em desktop** (`SmartSnapHandler` lê mouse DOM). No AR não há snap. ## O que vai ser construído ### A) Mouse (desktop) - Substituir `MeasureClickHandler` por handler `pointerdown` → `pointerup` no canvas: - Marca posição em `pointerdown`; se em `pointerup` o mouse moveu < 4 px e `measureMode` está on → adiciona ponto. - Usa `snapPoint` se houver; senão raycast. - Ignora botão direito (reservado para OrbitControls eventual no futuro). - Cursor `crosshair` apenas quando `measureMode`. - Adicionar **tecla Esc** para cancelar 1º ponto pendente, **tecla Z** (ou Ctrl+Z) para desfazer última medição. ### B) Snap inteligente compartilhado - Atualmente `SmartSnapHandler` (vértices) e `HoverDetector` (furo/aresta) só funcionam pelo mouse. Vamos: - Manter o snap de vértice via `findNearestVertex` (já existe em `SmartMeasure.ts`). - Adicionar **snap a aresta mais próxima** (já há `findNearestEdgeSegment`) como fallback quando não há vértice perto — prioridade: vértice > aresta > ponto bruto do raycast. - Expor uma função utilitária `resolveSnap(hitPoint, hitObject, camera, canvasSize)` reutilizada pelo mouse e pelo AR. ### C) AR (joysticks) Reescrever `XRControllerMeasure.tsx` para: - **Gatilho direito (trigger)**: adiciona ponto (com snap). - **Botão A (gamepad button 4) direito**: desfaz último ponto / última medição. - **Botão B (gamepad button 5) direito**: limpa todas as medições. - **Gatilho esquerdo**: alterna *snap to vertex* ON/OFF (com hysteresis para evitar toggling) — útil em peças orgânicas. - **Mira laser visível**: render de uma linha fina saindo do controle direito até o primeiro hit, com uma esfera no ponto que será marcado (verde se snap, amarelo se ponto bruto). Tamanho proporcional à distância do controle ao alvo. - Hysteresis já existe para o gatilho; aplicar o mesmo para A/B. ### D) Marcadores e labels proporcionais - `PointMarker`: raio passa a ser função da distância da câmera ao ponto, calculado em `useFrame` via `setScalar` no mesh — alvo: ~6 px de raio na tela em qualquer escala/zoom. - Label ``: substituir o `distanceFactor={1}` fixo por um valor calculado a partir da distância câmera↔ponto (mantém o texto com ~12 px na tela). - Linha verde da medição: usar `Line` do drei com `lineWidth` constante em pixels (já é o comportamento padrão). - No AR usar `` do `@react-three/drei` (já importado em `XRHudInWorld`) em vez de `` para os labels — render correto em XR. ### E) Botões de UI - **Desktop (`ViewerControls`)**: adicionar botão "Desfazer ponto" ao lado do "Limpar medições" quando `measurePoints.length === 1`. - **AR HUD (`XRHudInWorld`)**: já existe "✕ Medidas". Adicionar legenda visual abaixo: "Gatilho R: marcar · A: desfazer · B: limpar · Gatilho L: snap". ## Arquivos afetados | Arquivo | Mudança | |---|---| | `src/components/three/ModelViewer.tsx` | Reescrever `MeasureClickHandler` (pointerdown/up + gate de arrasto); `PointMarker` e `MeasurementOverlay` com tamanho/distanceFactor dinâmicos por frame | | `src/components/three/SmartMeasure.ts` | Adicionar helper `resolveSnap()` que combina vértice + aresta | | `src/components/three/XRControllerMeasure.tsx` | Adicionar handlers A/B + gatilho esquerdo, render de laser+esfera, snap via raycast no frame XR | | `src/components/three/XRHudInWorld.tsx` | Legenda de atalhos dos controles na aba Tools | | `src/components/ViewerControls.tsx` | Botão "Desfazer ponto"; atalhos Esc/Z | | `src/stores/useModelStore.ts` | Action `undoLastMeasurePoint()` (e `undoLastMeasurement()` se nenhum ponto pendente) | ## Critérios de aceite - Clicar em uma peça com Medir ON cria marcador amarelo; segundo clique fecha e gera label verde em mm reais (já compensado por escala — lógica do store mantida). - Snap a vértice mostra anel amarelo (`SnapRing`) e clique usa esse ponto. - Marcadores e labels permanecem do mesmo tamanho aparente em qualquer escala (1:1, 1:10, 1:100). - No AR, gatilho direito marca ponto sob o laser; A desfaz; B limpa; gatilho esquerdo alterna snap. - Esc cancela 1º ponto pendente; Z desfaz última medição (desktop).