🚀 Auto-deploy: melhoria no snap e medição AR em 28/05/2026 21:40:54

This commit is contained in:
2026-05-28 21:40:54 +00:00
parent b270ca0bfd
commit d47fb9dd37
3 changed files with 34 additions and 8 deletions
+14 -5
View File
@@ -21,6 +21,8 @@ interface SingleGrabRecord {
hand: 'left' | 'right'; hand: 'left' | 'right';
/** Offset (world) = groupWorldPos - controllerWorldPos at grab start. */ /** Offset (world) = groupWorldPos - controllerWorldPos at grab start. */
offsetWorld: THREE.Vector3; offsetWorld: THREE.Vector3;
/** Altura Y de mundo da peça no momento em que o grab iniciou, para travar horizontalmente */
startWorldY: number;
} }
interface DualGrabRecord { interface DualGrabRecord {
@@ -44,8 +46,8 @@ const _tmpQuat = new THREE.Quaternion();
/** /**
* Touch Plus grip mapping (Meta Quest 3): * Touch Plus grip mapping (Meta Quest 3):
* *
* • One-hand grip → **PAN ONLY** (translação). A rotação e a escala * • 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. * 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 * • Two-hand grip → ORBIT (eixo entre as mãos) + ZOOM uniforme
* (distância entre os controles). O zoom está sempre ligado. * (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; 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; const activeHand: 'left' | 'right' | null = lActive ? 'left' : rActive ? 'right' : null;
if (activeHand) { if (activeHand) {
const slot = activeHand === 'left' ? L : R; const slot = activeHand === 'left' ? L : R;
@@ -154,14 +156,21 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
group.updateMatrixWorld(); group.updateMatrixWorld();
const groupWorldPos = new THREE.Vector3().setFromMatrixPosition(group.matrixWorld); const groupWorldPos = new THREE.Vector3().setFromMatrixPosition(group.matrixWorld);
const offsetWorld = new THREE.Vector3().subVectors(groupWorldPos, ctrlPos); 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?.(); } 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 // Target world position = controller + initial offset
const targetWorldPos = new THREE.Vector3().addVectors(ctrlPos, single.current.offsetWorld); 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) // Convert to parent-local position (preserve current rotation/scale)
if (group.parent) { if (group.parent) {
group.parent.updateMatrixWorld(); group.parent.updateMatrixWorld();
+5
View File
@@ -615,6 +615,9 @@ function GridFloorRow() {
const setGridAutoFollow = useModelStore((s) => s.setGridAutoFollow); const setGridAutoFollow = useModelStore((s) => s.setGridAutoFollow);
const setGridY = useModelStore((s) => s.setGridY); const setGridY = useModelStore((s) => s.setGridY);
const nudgeGridY = useModelStore((s) => s.nudgeGridY); const nudgeGridY = useModelStore((s) => s.nudgeGridY);
const gridCalibMode = useModelStore((s) => s.gridCalibMode);
const setGridCalibMode = useModelStore((s) => s.setGridCalibMode);
const placeAtFloor = () => { const placeAtFloor = () => {
// Headset eye Y minus avg standing eye height (1.6 m) // Headset eye Y minus avg standing eye height (1.6 m)
const floor = camera.position.y - 1.6; const floor = camera.position.y - 1.6;
@@ -635,6 +638,8 @@ function GridFloorRow() {
onClick={() => nudgeGridY(0.01)} /> onClick={() => nudgeGridY(0.01)} />
<XR3DButton position={[0.07, -0.175, 0]} size={[0.075, 0.022]} label="Pousar chão" <XR3DButton position={[0.07, -0.175, 0]} size={[0.075, 0.022]} label="Pousar chão"
onClick={placeAtFloor} /> onClick={placeAtFloor} />
<XR3DButton position={[0.155, -0.175, 0]} size={[0.075, 0.022]} label={gridCalibMode ? "Calibrando" : "Calibrar"}
active={gridCalibMode} onClick={() => setGridCalibMode(!gridCalibMode)} />
</group> </group>
); );
} }
+15 -3
View File
@@ -233,7 +233,19 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); 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 ( if (
xrCalibration.modelId === sceneModel.id && xrCalibration.modelId === sceneModel.id &&
(xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') && (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) { for (const source of inputSources) {
const gp = source.gamepad; const gp = source.gamepad;
if (!gp) continue; if (!gp) continue;
const gripBtn = gp.buttons[2]; const trigBtn = gp.buttons[0];
const trigBtn = gp.buttons[1]; const gripBtn = gp.buttons[1];
const gripVal = gripBtn ? (gripBtn.value || (gripBtn.pressed ? 1 : 0)) : 0; const gripVal = gripBtn ? (gripBtn.value || (gripBtn.pressed ? 1 : 0)) : 0;
if (gripBtn?.pressed) gripHeld = true; if (gripBtn?.pressed) gripHeld = true;
if (gripVal > 0.3) anyGripHeld = true; // matches grab hysteresis OFF if (gripVal > 0.3) anyGripHeld = true; // matches grab hysteresis OFF