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