# Giro da peça em torno do próprio eixo (botão do meio) ## Objetivo 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. 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. ## Mudança de mapeamento dos botões | 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 | A profundidade continua acessível via Shift+Esquerdo (sem perda de função). ## Como calcular o "eixo próprio" No `PositionDragHandler` (`src/components/three/ModelViewer.tsx`), quando o `pointerdown` ocorrer com `button === 1`: 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). ## Aplicação da rotação no `pointermove` - Sensibilidade: `0.5° por pixel` de `dx` (movimento horizontal natural para "rolar"). - Converter para radianos: `angle = dx * sens * Math.PI/180`. - 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.