diff --git a/src/components/three/XRGrabbable.tsx b/src/components/three/XRGrabbable.tsx index 69acba7..1876124 100644 --- a/src/components/three/XRGrabbable.tsx +++ b/src/components/three/XRGrabbable.tsx @@ -21,6 +21,8 @@ interface SingleGrabRecord { hand: 'left' | 'right'; /** Offset (world) = groupWorldPos - controllerWorldPos at grab start. */ offsetWorld: THREE.Vector3; + /** Altura Y de mundo da peça no momento em que o grab iniciou, para travar horizontalmente */ + startWorldY: number; } interface DualGrabRecord { @@ -44,8 +46,8 @@ const _tmpQuat = new THREE.Quaternion(); /** * Touch Plus grip mapping (Meta Quest 3): * - * • One-hand grip → **PAN ONLY** (translação). A rotação e a escala - * da peça NÃO mudam — o grip simples só arrasta. + * • One-hand grip → **PAN ONLY (RESTRICTED HORIZONTALLY)** (translação). A rotação e a escala + * da peça NÃO mudam — o grip simples só arrasta no plano horizontal X/Z para preservar a altura Y calibrada. * • Two-hand grip → ORBIT (eixo entre as mãos) + ZOOM uniforme * (distância entre os controles). O zoom está sempre ligado. * @@ -144,7 +146,7 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta dual.current = null; } - // ─── One-hand mode (PAN ONLY) ──────────────────────────────── + // ─── One-hand mode (PAN ONLY - HORIZONTAL X/Z) ──────────────── const activeHand: 'left' | 'right' | null = lActive ? 'left' : rActive ? 'right' : null; if (activeHand) { const slot = activeHand === 'left' ? L : R; @@ -154,14 +156,21 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta group.updateMatrixWorld(); const groupWorldPos = new THREE.Vector3().setFromMatrixPosition(group.matrixWorld); const offsetWorld = new THREE.Vector3().subVectors(groupWorldPos, ctrlPos); - single.current = { hand: activeHand, offsetWorld }; + + // Salva a altura Y original de mundo + const startWorldY = groupWorldPos.y; + + single.current = { hand: activeHand, offsetWorld, startWorldY }; if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); } - console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — pan only`); + console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — horizontal pan`); } // Target world position = controller + initial 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! + targetWorldPos.y = single.current.startWorldY; + // Convert to parent-local position (preserve current rotation/scale) if (group.parent) { group.parent.updateMatrixWorld(); diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index c0d0538..fa2afab 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -615,6 +615,9 @@ function GridFloorRow() { const setGridAutoFollow = useModelStore((s) => s.setGridAutoFollow); const setGridY = useModelStore((s) => s.setGridY); const nudgeGridY = useModelStore((s) => s.nudgeGridY); + const gridCalibMode = useModelStore((s) => s.gridCalibMode); + const setGridCalibMode = useModelStore((s) => s.setGridCalibMode); + const placeAtFloor = () => { // Headset eye Y minus avg standing eye height (1.6 m) const floor = camera.position.y - 1.6; @@ -635,6 +638,8 @@ function GridFloorRow() { onClick={() => nudgeGridY(0.01)} /> + setGridCalibMode(!gridCalibMode)} /> ); } diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 3ca7cea..6e3bf45 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -233,7 +233,19 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). onClick={(e) => { e.stopPropagation(); - // Calibração XR: captura normal da face do modelo em espaço de mundo. + // Calibração do Grid em AR (clonando os mesmos princípios do modo viewer) + const gridCalibMode = useModelStore.getState().gridCalibMode; + if (gridCalibMode && e.point) { + useModelStore.setState({ + gridY: e.point.y - 0.001, + gridAutoFollow: false, + gridCalibMode: false, + showGrid: true, + }); + return; + } + + // Calibração XR da Peça: captura normal da face do modelo em espaço de mundo. if ( xrCalibration.modelId === sceneModel.id && (xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') && @@ -328,8 +340,8 @@ function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { for (const source of inputSources) { const gp = source.gamepad; if (!gp) continue; - const gripBtn = gp.buttons[2]; - const trigBtn = gp.buttons[1]; + const trigBtn = gp.buttons[0]; + const gripBtn = gp.buttons[1]; const gripVal = gripBtn ? (gripBtn.value || (gripBtn.pressed ? 1 : 0)) : 0; if (gripBtn?.pressed) gripHeld = true; if (gripVal > 0.3) anyGripHeld = true; // matches grab hysteresis OFF