Files
SteelXR2/.lovable/plan.md
T
gpt-engineer-app[bot] a134f1b0bf Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
2026-05-20 20:49:40 +00:00

5.3 KiB

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.