Habilitou OrbitControls
X-Lovable-Edit-ID: edt-d7e88634-cd60-4ffe-885e-414d9b85c854 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
+42
-63
@@ -1,71 +1,50 @@
|
||||
## Objetivo
|
||||
# Correções no modo Medir
|
||||
|
||||
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
|
||||
1. Ao ativar **Medir**, o OrbitControls é desabilitado (`enabled={!positionMode && !measureMode}`), bloqueando rotação/zoom/pan naturais com o mouse.
|
||||
2. O label da medida usa `useLabelDistanceFactor` que recalcula `distanceFactor` por frame para manter tamanho **constante** na tela. Resultado: ao aproximar a peça o texto não cresce e fica ilegível.
|
||||
|
||||
## Problemas identificados
|
||||
## Mudanças em `src/components/three/ModelViewer.tsx`
|
||||
|
||||
1. **Mouse não cria pontos**: o `MeasureClickHandler` usa o evento DOM `click` no canvas, que conflita com o `onClick` interno do R3F no `<group>` 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.
|
||||
### 1. Reabilitar controles de câmera no modo Medir
|
||||
No `<OrbitControls>` (linha ~792), trocar:
|
||||
```tsx
|
||||
enabled={!positionMode && !measureMode}
|
||||
```
|
||||
por:
|
||||
```tsx
|
||||
enabled={!positionMode}
|
||||
```
|
||||
O `MeasureClickHandler` já tem **drag gate de 4 px** (`pointerdown` + `pointerup` só conta como clique se o mouse moveu menos que 4 px e foi botão esquerdo). Isso significa:
|
||||
- Botão esquerdo arrastando → OrbitControls rotaciona, nenhum ponto é adicionado.
|
||||
- Botão direito → pan do OrbitControls (handler ignora `e.button !== 0`).
|
||||
- Roda → zoom do OrbitControls (handler ignora).
|
||||
- Clique curto esquerdo → adiciona ponto de medida (com snap).
|
||||
|
||||
## O que vai ser construído
|
||||
### 2. Label da medida escala com o zoom e fica ~4x maior
|
||||
Substituir `MeasurementLabel` para usar `distanceFactor` **fixo** (em vez de calculado por frame). Com `<Html distanceFactor={N}>`, o tamanho na tela é proporcional a `N / distance` — ou seja, **cresce naturalmente ao aproximar a câmera**.
|
||||
|
||||
### A) Mouse (desktop)
|
||||
- Remover o uso de `useLabelDistanceFactor` no label de medida (manter, se desejado, apenas no `hoverInfo`).
|
||||
- Definir `distanceFactor` fixo calibrado para 1:1 mm (valor inicial ~`8`, ajustável).
|
||||
- Aumentar o tamanho base do texto de `text-[11px]` para `text-[16px]` e `px-2 py-0.5` → `px-3 py-1` para acompanhar a proporção (~4x área visual final somando o aumento do distanceFactor + texto).
|
||||
|
||||
- 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.
|
||||
```tsx
|
||||
function MeasurementLabel({ position, text, variant }: { ... }) {
|
||||
const borderClass = variant === 'success' ? 'border-success/60' : 'border-primary/60';
|
||||
const textClass = variant === 'success' ? 'text-success' : 'text-primary';
|
||||
return (
|
||||
<Html position={position} center distanceFactor={8} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
|
||||
<div className={`rounded bg-card/95 border ${borderClass} px-3 py-1 shadow-lg backdrop-blur-sm`}>
|
||||
<span className={`font-mono text-[16px] font-bold ${textClass} whitespace-nowrap`}>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### B) Snap inteligente compartilhado
|
||||
O `hoverInfo` (auto-detect) pode manter o comportamento atual de tamanho constante para não poluir a cena ao passar o mouse.
|
||||
|
||||
- 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 `<Html distanceFactor={...}>`: 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 `<Text>` do `@react-three/drei` (já importado em `XRHudInWorld`) em vez de `<Html>` 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).
|
||||
## Sem mudanças em outros arquivos
|
||||
Store, AR, snap e PointMarker permanecem intocados.
|
||||
|
||||
@@ -228,14 +228,15 @@ function useLabelDistanceFactor(position: [number, number, number]): number {
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function MeasurementLabel({ position, text, variant }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' }) {
|
||||
const df = useLabelDistanceFactor(position);
|
||||
function MeasurementLabel({ position, text, variant, fixed = false }: { position: [number, number, number]; text: string; variant: 'success' | 'primary'; fixed?: boolean }) {
|
||||
const dfDynamic = useLabelDistanceFactor(position);
|
||||
const df = fixed ? dfDynamic : 8;
|
||||
const borderClass = variant === 'success' ? 'border-success/60' : 'border-primary/60';
|
||||
const textClass = variant === 'success' ? 'text-success' : 'text-primary';
|
||||
return (
|
||||
<Html position={position} center distanceFactor={df} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
|
||||
<div className={`rounded bg-card/95 border ${borderClass} px-2 py-0.5 shadow-lg backdrop-blur-sm`}>
|
||||
<span className={`font-mono text-[11px] font-bold ${textClass} whitespace-nowrap`}>
|
||||
<div className={`rounded bg-card/95 border ${borderClass} px-3 py-1 shadow-lg backdrop-blur-sm`}>
|
||||
<span className={`font-mono text-[16px] font-bold ${textClass} whitespace-nowrap`}>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
@@ -266,7 +267,9 @@ function MeasurementOverlay() {
|
||||
position={hoverInfo.position}
|
||||
text={`${hoverInfo.type === 'hole' ? '⌀ ' : ''}${hoverInfo.value.toFixed(1)} mm`}
|
||||
variant="primary"
|
||||
fixed
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
{/* Pending first point */}
|
||||
@@ -789,7 +792,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
dampingFactor={0.1}
|
||||
minDistance={0.05}
|
||||
maxDistance={50}
|
||||
enabled={!positionMode && !measureMode}
|
||||
enabled={!positionMode}
|
||||
/>
|
||||
</Canvas>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user