Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
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:
- Localizar o
THREE.Object3Draiz da peça ativa na cena viascene.getObjectByName(...)ou umuserData.modelId. Já existe umGLBModelmontado; usarscene.traversefiltrando poruserData.modelId === activeId(ajustarGLBModelpara marcaruserData.modelIdse ainda não estiver). - Calcular
THREE.Box3().setFromObject(group)em coordenadas mundo → obtersizeecenter. - 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: compararsize.x,size.y,size.zda bbox local → eixo local = (1,0,0), (0,1,0) ou (0,0,1) correspondente ao maior. - Transformar esse eixo local para mundo aplicando a
group.matrixWorld(apenas a parte rotacional viaVector3.transformDirection). - Guardar em refs:
axisWorld: Vector3,centerWorld: Vector3(centro da bbox mundo).
Aplicação da rotação no pointermove
- Sensibilidade:
0.5° por pixeldedx(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:- Ler quaternion atual do grupo:
qCurrent = group.quaternion.clone(). - Aplicar
qNew = qDelta * qCurrent(rotação em mundo aplica-se à esquerda). - Mas
fineTuningé aplicado em cima de uma orientação base do modelo. Para preservar isso, calcularqBase = qCurrent.clone().premultiply(qFTInverse)ondeqFTé a rotação atual do fineTuning como quaternion. Então o novo fineTuning quaternion =qBase.inverse() * qNew. - Converter para Euler na mesma ordem usada hoje (provavelmente
'XYZ') e gravarrotX/Y/Zem graus.
- Ler quaternion atual do grupo:
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 porscaleRatio.factor) e somar aposX/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
GLBModelcomuserData.modelId = model.id(se ainda não estiver). - Em
PositionDragHandler:- No
pointerdowncombutton === 1: localizar grupo da peça ativa, calcular bbox local, determinar eixo próprio, salvaraxisWorldecenterWorldem refs. - No
pointermovecombutton === 1: aplicar rotação axial + correção de translação como descrito, gravar viasetFineTuning({ rotX, rotY, rotZ, posX, posY, posZ }).
- No
- Remover o uso de "Meio = profundidade" (mantém apenas Shift+Esquerdo).
- Marcar o grupo de cada
-
src/components/ViewerControls.tsx- Atualizar o
titledo 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.
- Atualizar o
Detalhes técnicos
- Usar
THREE.Box3.setFromObjectcuidando para chamarupdateMatrixWorld(true)antes. - Para a bbox local: clonar o grupo ou aplicar inverso da matriz mundo nas posições, ou simplesmente iterar
child.geometry.boundingBoxse for um único mesh. Para robustez com vários meshes filhos, somar viaBox3.expandByObjectapós copiarmatrixWorlde remover translação. Quaternion.applyVectornão existe diretamente — usarvector.clone().applyQuaternion(qDelta).- Conversão Euler→graus deve usar a mesma ordem em que
fineTuningé aplicado hoje (verificar no JSX doGLBModel; provavelmenterotation={[degToRad(rotX), degToRad(rotY), degToRad(rotZ)]}com ordem padrão XYZ). - Persistência:
setFineTuningjá chamasavePlacement, então a posição/rotação ficam salvas como já acontece.