diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index b49b923..e851f05 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -504,9 +504,138 @@ function GridAutoFollower() { }, 100); return () => clearTimeout(id); }, [models, scene]); +/** Mouse drag handler for desktop "Posicionar" mode: translates/rotates the active model. */ +function PositionDragHandler() { + const { camera, gl, scene } = useThree(); + const positionMode = useModelStore((s) => s.positionMode); + const activeId = useModelStore((s) => s.activeModelId); + + useEffect(() => { + if (!positionMode) { + gl.domElement.style.cursor = 'grab'; + return; + } + gl.domElement.style.cursor = 'move'; + + const canvas = gl.domElement; + let dragging = false; + let button = 0; + let shiftKey = false; + let lastX = 0; + let lastY = 0; + let pixelsPerWorldUnit = 1; + + const computePixelScale = () => { + const st = useModelStore.getState(); + const active = st.models.find((m) => m.id === st.activeModelId); + if (!active) return; + // Find the model's group position (approx world): use renderFactor + fineTuning pos + const factor = st.scaleRatio?.factor ?? 1; + const wp = new THREE.Vector3( + active.fineTuning.posX * factor, + active.fineTuning.posY * factor, + active.fineTuning.posZ * factor, + ); + const dist = camera.position.distanceTo(wp); + const perspCam = camera as THREE.PerspectiveCamera; + const fov = (perspCam.fov ?? 50) * (Math.PI / 180); + const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / canvas.clientHeight; + pixelsPerWorldUnit = worldPerPixel; // world units per pixel + }; + + const onDown = (e: PointerEvent) => { + const st = useModelStore.getState(); + if (!st.positionMode || !st.activeModelId) return; + dragging = true; + button = e.button; + shiftKey = e.shiftKey; + lastX = e.clientX; + lastY = e.clientY; + computePixelScale(); + (e.target as Element).setPointerCapture?.(e.pointerId); + e.preventDefault(); + }; + + const onMove = (e: PointerEvent) => { + if (!dragging) return; + const st = useModelStore.getState(); + const active = st.models.find((m) => m.id === st.activeModelId); + if (!active) return; + + const dx = e.clientX - lastX; + const dy = e.clientY - lastY; + lastX = e.clientX; + lastY = e.clientY; + shiftKey = e.shiftKey; + + const ft = active.fineTuning; + const factor = st.scaleRatio?.factor ?? 1; + + if (button === 2) { + // Right button: rotate + const sens = 0.4; // deg per pixel + if (shiftKey) { + useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + dx * sens }); + } else { + useModelStore.getState().setFineTuning({ + rotY: ft.rotY + dx * sens, + rotX: ft.rotX + dy * sens, + }); + } + } else if (button === 1 || (button === 0 && shiftKey)) { + // Middle or Shift+left: depth (Z in camera space) + const worldDelta = dy * pixelsPerWorldUnit; + const camDir = new THREE.Vector3(); + camera.getWorldDirection(camDir); + const move = camDir.multiplyScalar(worldDelta).divideScalar(factor); + useModelStore.getState().setFineTuning({ + posX: ft.posX + move.x, + posY: ft.posY + move.y, + posZ: ft.posZ + move.z, + }); + } else { + // Left: translate in camera plane + const right = new THREE.Vector3(); + const up = new THREE.Vector3(); + camera.matrixWorld.extractBasis(right, up, new THREE.Vector3()); + const worldDX = dx * pixelsPerWorldUnit; + const worldDY = -dy * pixelsPerWorldUnit; + const move = right.multiplyScalar(worldDX).add(up.multiplyScalar(worldDY)).divideScalar(factor); + useModelStore.getState().setFineTuning({ + posX: ft.posX + move.x, + posY: ft.posY + move.y, + posZ: ft.posZ + move.z, + }); + } + }; + + const onUp = (e: PointerEvent) => { + dragging = false; + (e.target as Element).releasePointerCapture?.(e.pointerId); + }; + + const onContext = (e: MouseEvent) => e.preventDefault(); + + canvas.addEventListener('pointerdown', onDown); + canvas.addEventListener('pointermove', onMove); + canvas.addEventListener('pointerup', onUp); + canvas.addEventListener('pointercancel', onUp); + canvas.addEventListener('contextmenu', onContext); + return () => { + canvas.removeEventListener('pointerdown', onDown); + canvas.removeEventListener('pointermove', onMove); + canvas.removeEventListener('pointerup', onUp); + canvas.removeEventListener('pointercancel', onUp); + canvas.removeEventListener('contextmenu', onContext); + gl.domElement.style.cursor = 'grab'; + }; + }, [positionMode, activeId, camera, gl, scene]); + return null; } + + export function ModelViewerCanvas({ url }: ModelViewerProps) { return (