Adjustou pivô de rotação ao meio
X-Lovable-Edit-ID: edt-958f573c-2f24-4729-870f-0fa19ee4352c Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
+58
-33
@@ -1,43 +1,68 @@
|
|||||||
|
# Giro da peça em torno do próprio eixo (botão do meio)
|
||||||
|
|
||||||
## Modo Posicionar (desktop)
|
## Objetivo
|
||||||
|
|
||||||
Permite que, antes de entrar no AR, o usuário mova e gire a peça **ativa** com o mouse — exatamente como faria com o controle no AR. As outras peças permanecem travadas. Ao desativar o botão, todas ficam fixas nas posições atuais e o estado persiste (já temos `savePlacement` por `fileName`).
|
No modo **Posicionar**, permitir rotação da peça ativa em torno do seu **eixo longitudinal próprio** (a linha que liga os dois extremos da peça), usando o **botão do meio (roda) do mouse** enquanto se arrasta.
|
||||||
|
|
||||||
### Comportamento
|
A rotação atual (botão direito) gira em torno do pivô do grupo (canto da peça). A nova precisa girar em torno do **centro geométrico da peça**, no eixo do seu maior comprimento — como rolar um perfil de aço em torno do seu próprio comprimento.
|
||||||
|
|
||||||
- Botão **Posicionar** (ícone Move3D) na barra `ViewerControls`, ao lado de Medir.
|
## Mudança de mapeamento dos botões
|
||||||
- Quando ativo:
|
|
||||||
- `OrbitControls` é desabilitado (não conflitar com o arrasto da peça).
|
|
||||||
- Cursor vira `move`.
|
|
||||||
- Apenas a peça com `activeModelId` responde ao mouse. Clicar em outra peça troca a ativa e passa a manipulá-la (as outras voltam a ficar travadas).
|
|
||||||
- **Arrastar com botão esquerdo** → translada no plano da câmera (X/Y da tela projetados para o mundo).
|
|
||||||
- **Shift + arrastar esquerdo** ou **arrastar com botão do meio** → translada no eixo Z (profundidade) usando o movimento vertical do mouse.
|
|
||||||
- **Arrastar com botão direito** → rotaciona: movimento horizontal = rotY (yaw), movimento vertical = rotX (pitch). **Shift + direito** = rotZ (roll).
|
|
||||||
- Cada movimento atualiza `fineTuning` via `setFineTuning` da peça ativa, o que já dispara `savePlacement` (persistência entre sessões/entrar-sair do AR — mesma chave usada hoje).
|
|
||||||
- Quando desativado: tudo volta ao normal (Orbit ativo, peças travadas onde foram deixadas). Modo Medir e Posicionar são mutuamente exclusivos.
|
|
||||||
|
|
||||||
### Arquivos a alterar
|
| Botão | Antes | Depois |
|
||||||
|
|---|---|---|
|
||||||
|
| Esquerdo | Translada plano câmera | igual |
|
||||||
|
| Shift + Esquerdo | Profundidade (Z câmera) | igual |
|
||||||
|
| **Meio (roda)** | Profundidade (Z câmera) | **Giro axial em torno do eixo próprio** |
|
||||||
|
| Direito | rotY/rotX (pivô atual) | igual |
|
||||||
|
| Shift + Direito | rotZ | igual |
|
||||||
|
|
||||||
1. **`src/stores/useModelStore.ts`**
|
A profundidade continua acessível via Shift+Esquerdo (sem perda de função).
|
||||||
- Adicionar `positionMode: boolean` + `setPositionMode(on)`.
|
|
||||||
- Em `setPositionMode(true)` desligar `measureMode`; e vice-versa em `setMeasureMode`.
|
|
||||||
|
|
||||||
2. **`src/components/three/ModelViewer.tsx`**
|
## Como calcular o "eixo próprio"
|
||||||
- Novo componente `PositionDragHandler` (dentro do Canvas) que:
|
|
||||||
- Escuta `pointerdown/move/up` no `gl.domElement` quando `positionMode` está on e existe `activeModelId`.
|
|
||||||
- Converte delta de pixels em delta de mundo usando a distância câmera↔peça e o FOV (`2 * dist * tan(fov/2) / canvasHeight`), depois divide pelo `scaleRatio.factor` para gravar em coordenadas locais coerentes com `fineTuning`.
|
|
||||||
- Aplica via `setFineTuning({ posX: ..., posY: ..., posZ: ..., rotX: ..., rotY: ..., rotZ: ... })`.
|
|
||||||
- Em `OrbitControls`, definir `enabled={!positionMode && !measureMode}` (medir já não rotaciona hoje só por cursor, mas reforça).
|
|
||||||
- Cursor: quando `positionMode`, setar `gl.domElement.style.cursor = 'move'`.
|
|
||||||
|
|
||||||
3. **`src/components/ViewerControls.tsx`**
|
No `PositionDragHandler` (`src/components/three/ModelViewer.tsx`), quando o `pointerdown` ocorrer com `button === 1`:
|
||||||
- Importar ícone `Move3D` do lucide.
|
|
||||||
- Novo botão **Posicionar** com `variant={positionMode ? 'default' : 'outline'}` chamando `setPositionMode(!positionMode)`. Label dinâmica: `Posicionar` / `Posicionando — [nome da peça ativa]`.
|
|
||||||
|
|
||||||
### Detalhes técnicos
|
1. Localizar o `THREE.Object3D` raiz da peça ativa na cena via `scene.getObjectByName(...)` ou um `userData.modelId`. Já existe um `GLBModel` montado; usar `scene.traverse` filtrando por `userData.modelId === activeId` (ajustar `GLBModel` para marcar `userData.modelId` se ainda não estiver).
|
||||||
|
2. Calcular `THREE.Box3().setFromObject(group)` em coordenadas mundo → obter `size` e `center`.
|
||||||
|
3. O eixo próprio = direção do maior lado da bbox local. Para isso, calcular a bbox em **espaço local** do grupo (`new THREE.Box3().setFromObject(group)` aplicado depois de zerar matriz, ou iterar geometrias). Mais simples: comparar `size.x`, `size.y`, `size.z` da bbox local → eixo local = (1,0,0), (0,1,0) ou (0,0,1) correspondente ao maior.
|
||||||
|
4. Transformar esse eixo local para mundo aplicando a `group.matrixWorld` (apenas a parte rotacional via `Vector3.transformDirection`).
|
||||||
|
5. Guardar em refs: `axisWorld: Vector3`, `centerWorld: Vector3` (centro da bbox mundo).
|
||||||
|
|
||||||
- A translação é gravada em `fineTuning.pos{X,Y,Z}` (espaço local da peça, antes do `renderFactor` da escala). Para que 1 pixel ↔ 1 mm visual fique coerente, o cálculo divide o delta-mundo por `scaleRatio.factor` (ex: em 1:50, mover 100 px arrasta o objeto 100 px de tela tanto na visualização quanto na coordenada interna).
|
## Aplicação da rotação no `pointermove`
|
||||||
- Rotação em graus (já é o formato de `fineTuning.rotX/Y/Z`), sensibilidade ~`0.4°/px`.
|
|
||||||
- O bloqueio de outras peças é automático: o handler só atualiza a `activeModelId`. Clicar em outra peça (handler `onClick` já existente em `GLBModel`) muda a ativa.
|
- Sensibilidade: `0.5° por pixel` de `dx` (movimento horizontal natural para "rolar").
|
||||||
- Persistência: `setFineTuning` já chama `savePlacement(fileName, fineTuning)`, então sair/voltar do AR mantém posições.
|
- Converter para radianos: `angle = dx * sens * Math.PI/180`.
|
||||||
- Sem mudanças no AR; apenas no viewer desktop.
|
- Construir quaternion: `qDelta = new Quaternion().setFromAxisAngle(axisWorld, angle)`.
|
||||||
|
- Precisamos atualizar `fineTuning.rotX/Y/Z` (graus, ordem Euler atual usada pelo grupo). Estratégia:
|
||||||
|
1. Ler quaternion atual do grupo: `qCurrent = group.quaternion.clone()`.
|
||||||
|
2. Aplicar `qNew = qDelta * qCurrent` (rotação em mundo aplica-se à esquerda).
|
||||||
|
3. Mas `fineTuning` é aplicado **em cima** de uma orientação base do modelo. Para preservar isso, calcular `qBase = qCurrent.clone().premultiply(qFTInverse)` onde `qFT` é a rotação atual do fineTuning como quaternion. Então o novo fineTuning quaternion = `qBase.inverse() * qNew`.
|
||||||
|
4. Converter para Euler na mesma ordem usada hoje (provavelmente `'XYZ'`) e gravar `rotX/Y/Z` em graus.
|
||||||
|
|
||||||
|
Para evitar complexidade da composição com `qBase`: alternativa mais simples e suficiente — **rotacionar diretamente o grupo via quaternion e também transladar para compensar a diferença entre pivô do grupo e centro da bbox**. Como `fineTuning.pos{X,Y,Z}` é a única fonte de translação, calcular:
|
||||||
|
|
||||||
|
- Antes da rotação: `offset = centerWorld - group.position`.
|
||||||
|
- Depois: `newOffset = qDelta.applyVector(offset)`.
|
||||||
|
- `deltaPos = offset - newOffset` (em mundo) → converter para local (dividindo por `scaleRatio.factor`) e somar a `posX/posY/posZ`.
|
||||||
|
|
||||||
|
Combinada com a atualização Euler descrita acima, isso garante que o **centro geométrico fique fixo na tela** enquanto a peça gira em torno do próprio eixo.
|
||||||
|
|
||||||
|
## Resumo dos arquivos
|
||||||
|
|
||||||
|
- **`src/components/three/ModelViewer.tsx`**
|
||||||
|
- Marcar o grupo de cada `GLBModel` com `userData.modelId = model.id` (se ainda não estiver).
|
||||||
|
- Em `PositionDragHandler`:
|
||||||
|
- No `pointerdown` com `button === 1`: localizar grupo da peça ativa, calcular bbox local, determinar eixo próprio, salvar `axisWorld` e `centerWorld` em refs.
|
||||||
|
- No `pointermove` com `button === 1`: aplicar rotação axial + correção de translação como descrito, gravar via `setFineTuning({ rotX, rotY, rotZ, posX, posY, posZ })`.
|
||||||
|
- Remover o uso de "Meio = profundidade" (mantém apenas Shift+Esquerdo).
|
||||||
|
|
||||||
|
- **`src/components/ViewerControls.tsx`**
|
||||||
|
- Atualizar o `title` do botão Posicionar para refletir o novo mapeamento: `Esquerdo: mover · Shift+Esq: profundidade · Meio: girar no próprio eixo · Direito: rotação livre · Shift+Dir: rotZ`.
|
||||||
|
|
||||||
|
## Detalhes técnicos
|
||||||
|
|
||||||
|
- Usar `THREE.Box3.setFromObject` cuidando para chamar `updateMatrixWorld(true)` antes.
|
||||||
|
- Para a bbox local: clonar o grupo ou aplicar inverso da matriz mundo nas posições, ou simplesmente iterar `child.geometry.boundingBox` se for um único mesh. Para robustez com vários meshes filhos, somar via `Box3.expandByObject` após copiar `matrixWorld` e remover translação.
|
||||||
|
- `Quaternion.applyVector` não existe diretamente — usar `vector.clone().applyQuaternion(qDelta)`.
|
||||||
|
- Conversão Euler→graus deve usar a mesma ordem em que `fineTuning` é aplicado hoje (verificar no JSX do `GLBModel`; provavelmente `rotation={[degToRad(rotX), degToRad(rotY), degToRad(rotZ)]}` com ordem padrão XYZ).
|
||||||
|
- Persistência: `setFineTuning` já chama `savePlacement`, então a posição/rotação ficam salvas como já acontece.
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function ViewerControls() {
|
|||||||
className="gap-2 h-9"
|
className="gap-2 h-9"
|
||||||
onClick={() => setPositionMode(!positionMode)}
|
onClick={() => setPositionMode(!positionMode)}
|
||||||
disabled={!activeModelId}
|
disabled={!activeModelId}
|
||||||
title="Arraste: mover · Shift+arraste ou meio: profundidade · Direito: girar · Shift+direito: rotZ"
|
title="Esquerdo: mover · Shift+Esq: profundidade · Meio (roda): girar no próprio eixo · Direito: rotação livre · Shift+Dir: rotZ"
|
||||||
>
|
>
|
||||||
<Move3D className="h-3.5 w-3.5" />
|
<Move3D className="h-3.5 w-3.5" />
|
||||||
<span className="font-mono text-xs">
|
<span className="font-mono text-xs">
|
||||||
|
|||||||
@@ -123,22 +123,33 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
|||||||
|
|
||||||
if (!sceneModel.visible) return null;
|
if (!sceneModel.visible) return null;
|
||||||
|
|
||||||
|
// Determine dominant local axis of the model (longest bbox side) for axial roll
|
||||||
|
const dominantAxis: 'x' | 'y' | 'z' =
|
||||||
|
modelInfo.size.x >= modelInfo.size.y && modelInfo.size.x >= modelInfo.size.z
|
||||||
|
? 'x'
|
||||||
|
: modelInfo.size.y >= modelInfo.size.z
|
||||||
|
? 'y'
|
||||||
|
: 'z';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
scale={[renderFactor, renderFactor, renderFactor]}
|
scale={[renderFactor, renderFactor, renderFactor]}
|
||||||
onClick={(e) => { e.stopPropagation(); setActive(sceneModel.id); }}
|
onClick={(e) => { e.stopPropagation(); setActive(sceneModel.id); }}
|
||||||
|
userData={{ modelId: sceneModel.id, dominantAxis }}
|
||||||
>
|
>
|
||||||
<group
|
{/* Translation */}
|
||||||
ref={ref}
|
<group position={[fineTuning.posX, fineTuning.posY, fineTuning.posZ]}>
|
||||||
position={[
|
{/* Rotation + scale around the geometry center */}
|
||||||
-modelInfo.center.x + fineTuning.posX,
|
<group
|
||||||
-modelInfo.center.y + fineTuning.posY,
|
ref={ref}
|
||||||
-modelInfo.center.z + fineTuning.posZ,
|
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||||
]}
|
scale={[s, s, s]}
|
||||||
rotation={[rotXRad, rotYRad, rotZRad]}
|
>
|
||||||
scale={[s, s, s]}
|
{/* Shift geometry so its center sits at the parent's origin (pivot = center) */}
|
||||||
>
|
<group position={[-modelInfo.center.x, -modelInfo.center.y, -modelInfo.center.z]}>
|
||||||
<primitive object={scene} />
|
<primitive object={scene} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
@@ -585,8 +596,25 @@ function PositionDragHandler() {
|
|||||||
rotX: ft.rotX + dy * sens,
|
rotX: ft.rotX + dy * sens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (button === 1 || (button === 0 && shiftKey)) {
|
} else if (button === 1) {
|
||||||
// Middle or Shift+left: depth (Z in camera space)
|
// Middle button (wheel): roll around the piece's own longitudinal axis
|
||||||
|
const sens = 0.5;
|
||||||
|
let dominant: 'x' | 'y' | 'z' = 'x';
|
||||||
|
scene.traverse((o) => {
|
||||||
|
if (o.userData?.modelId === active.id && o.userData?.dominantAxis) {
|
||||||
|
dominant = o.userData.dominantAxis;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const delta = dx * sens;
|
||||||
|
if (dominant === 'x') {
|
||||||
|
useModelStore.getState().setFineTuning({ rotX: ft.rotX + delta });
|
||||||
|
} else if (dominant === 'y') {
|
||||||
|
useModelStore.getState().setFineTuning({ rotY: ft.rotY + delta });
|
||||||
|
} else {
|
||||||
|
useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + delta });
|
||||||
|
}
|
||||||
|
} else if (button === 0 && shiftKey) {
|
||||||
|
// Shift+left: depth (Z in camera space)
|
||||||
const worldDelta = dy * pixelsPerWorldUnit;
|
const worldDelta = dy * pixelsPerWorldUnit;
|
||||||
const camDir = new THREE.Vector3();
|
const camDir = new THREE.Vector3();
|
||||||
camera.getWorldDirection(camDir);
|
camera.getWorldDirection(camDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user