diff --git a/src/components/three/XRGrabbable.tsx b/src/components/three/XRGrabbable.tsx index 1876124..a565099 100644 --- a/src/components/three/XRGrabbable.tsx +++ b/src/components/three/XRGrabbable.tsx @@ -3,6 +3,7 @@ import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab'; import { useModelStore } from '@/stores/useModelStore'; +import { activeModelGroupRef } from './xrCalibrationBus'; interface XRGrabbableProps { /** @@ -72,7 +73,71 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta single.current = null; }, [scaleResetNonce]); - useFrame(() => { + useEffect(() => { + activeModelGroupRef.current = groupRef.current; + return () => { + if (activeModelGroupRef.current === groupRef.current) { + activeModelGroupRef.current = null; + } + }; + }); + + const syncGrabToStore = () => { + const group = groupRef.current; + if (!group) return; + + const state = useModelStore.getState(); + const activeModelId = state.activeModelId; + if (!activeModelId) return; + + const activeModel = state.models.find(m => m.id === activeModelId); + if (!activeModel) return; + + const ft = { ...activeModel.fineTuning }; + const renderFactor = state.scaleRatio?.factor ?? 1; + + // Model's matrix relative to XRGrabbable + const posModel = new THREE.Vector3(ft.posX * renderFactor, ft.posY * renderFactor, ft.posZ * renderFactor); + const rotModel = new THREE.Euler( + (ft.rotX * Math.PI) / 180, + (ft.rotY * Math.PI) / 180, + (ft.rotZ * Math.PI) / 180, + 'YXZ' + ); + const quatModel = new THREE.Quaternion().setFromEuler(rotModel); + const scaleModel = new THREE.Vector3(ft.scale * renderFactor, ft.scale * renderFactor, ft.scale * renderFactor); + + const mModel = new THREE.Matrix4().compose(posModel, quatModel, scaleModel); + + // Grab matrix of XRGrabbable group + const mGrab = new THREE.Matrix4().compose(group.position, group.quaternion, group.scale); + + // Combined matrix + const mCombined = new THREE.Matrix4().multiplyMatrices(mGrab, mModel); + + const finalPos = new THREE.Vector3(); + const finalQuat = new THREE.Quaternion(); + const finalScale = new THREE.Vector3(); + mCombined.decompose(finalPos, finalQuat, finalScale); + + // Reset grabbable group to identity so the transform is held entirely by fineTuning + group.position.set(0, 0, 0); + group.quaternion.identity(); + group.scale.set(1, 1, 1); + + // Save to store + state.setFineTuning({ + posX: finalPos.x / renderFactor, + posY: finalPos.y / renderFactor, + posZ: finalPos.z / renderFactor, + rotX: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').x * 180) / Math.PI, + rotY: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').y * 180) / Math.PI, + rotZ: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').z * 180) / Math.PI, + scale: finalScale.x / renderFactor, + }); + }; + + useFrame((_state, dt) => { const group = groupRef.current; if (!group) return; const snap: ControllerGrabSnapshot = grab.current; @@ -144,6 +209,7 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta } else if (dual.current) { console.log('[XR][grab] ◆ TWO-HAND end'); dual.current = null; + syncGrabToStore(); } // ─── One-hand mode (PAN ONLY - HORIZONTAL X/Z) ──────────────── @@ -162,10 +228,26 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta single.current = { hand: activeHand, offsetWorld, startWorldY }; if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); } - console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — horizontal pan`); + console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — horizontal pan & rotation`); } - // Target world position = controller + initial offset + // Rotação Y via analógico do controle ativo + const gp = slot.gamepad; + if (gp && gp.axes.length >= 4) { + const stickX = gp.axes[2]; // horizontal do thumbstick + const deadzone = 0.15; + if (Math.abs(stickX) > deadzone) { + const rotationSpeed = 1.5; // rad por segundo + const angle = Math.sign(stickX) * (Math.abs(stickX) - deadzone) * rotationSpeed * dt; + const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -angle); + group.quaternion.multiplyQuaternions(q, group.quaternion); + + // Rotaciona o offset para girar ao redor da mão + single.current.offsetWorld.applyQuaternion(q); + } + } + + // Target world position = controller + initial/rotated offset const targetWorldPos = new THREE.Vector3().addVectors(ctrlPos, single.current.offsetWorld); // Trava a translação no plano horizontal (XZ de mundo), preservando a altura Y original de calibração! @@ -179,7 +261,9 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta } group.position.copy(targetWorldPos); } else if (single.current) { + console.log('[XR][grab] ● ONE-HAND end'); single.current = null; + syncGrabToStore(); } }); diff --git a/src/components/three/xrCalibrationBus.ts b/src/components/three/xrCalibrationBus.ts index ca79ffa..37a2a34 100644 --- a/src/components/three/xrCalibrationBus.ts +++ b/src/components/three/xrCalibrationBus.ts @@ -270,3 +270,5 @@ export function computeVirtRealTransform( return { quaternion, position }; } + +export const activeModelGroupRef = { current: null as THREE.Group | null }; diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index f68457a..7abac38 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -34,6 +34,7 @@ import { pushXRRealPoint, pushXRVirtualPoint, computeVirtRealTransform, + activeModelGroupRef, } from '@/components/three/xrCalibrationBus'; // --- Diagnóstico XR --- @@ -973,20 +974,54 @@ function XRVirtRealDesktopAligner() { function XRGrid() { const showGrid = useModelStore((s) => s.showGrid); const gridY = useModelStore((s) => s.gridY); + const activeModelId = useModelStore((s) => s.activeModelId); + const activeModel = useModelStore((s) => s.models.find(m => m.id === activeModelId)); + const groupRef = useRef(null); + + useFrame(() => { + if (!showGrid || !groupRef.current) return; + + let px = 0; + let pz = 0; + let ry = 0; + + if (activeModelGroupRef.current) { + const g = activeModelGroupRef.current; + const wp = new THREE.Vector3(); + g.getWorldPosition(wp); + px = wp.x; + pz = wp.z; + + const wq = new THREE.Quaternion(); + g.getWorldQuaternion(wq); + const euler = new THREE.Euler().setFromQuaternion(wq, 'YXZ'); + ry = euler.y; + } else if (activeModel) { + px = activeModel.fineTuning.posX; + pz = activeModel.fineTuning.posZ; + ry = (activeModel.fineTuning.rotY * Math.PI) / 180; + } + + groupRef.current.position.set(px, gridY, pz); + groupRef.current.rotation.set(0, ry, 0); + }); + if (!showGrid) return null; + return ( - + + + ); }