🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 02:00:33

This commit is contained in:
2026-05-24 02:00:33 +00:00
parent c839653a97
commit c8d51e0e1b
7 changed files with 170 additions and 26 deletions
+75 -5
View File
@@ -1,9 +1,25 @@
import { useRef, ReactNode, useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
import { useModelStore } from '@/stores/useModelStore';
function triggerHaptic(session: XRSession | null, handedness: 'left' | 'right', intensity = 0.5, duration = 40) {
if (!session) return;
try {
for (const source of session.inputSources) {
if (source.handedness === handedness) {
const gp = source.gamepad;
if (gp && gp.hapticActuators && gp.hapticActuators.length > 0) {
gp.hapticActuators[0].pulse(intensity, duration).catch(() => {});
}
}
}
} catch (e) {
console.warn('[XR][Haptic] erro ao vibrar controle no grab:', e);
}
}
interface XRGrabbableProps {
/**
* Quando true (padrão), o grip duplo aplica ZOOM uniforme além da rotação.
@@ -70,9 +86,12 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
single.current = null;
}, [scaleResetNonce]);
useFrame(() => {
const { gl } = useThree();
useFrame((_state, dt) => {
const group = groupRef.current;
if (!group) return;
const session = gl.xr.getSession();
const snap: ControllerGrabSnapshot = grab.current;
const L = snap.left;
const R = snap.right;
@@ -95,6 +114,33 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
haloRef.current.visible = maxGrip > 0.05;
}
const commitGrabTransforms = () => {
const activeId = useModelStore.getState().activeModelId;
if (!activeId) return;
const active = useModelStore.getState().models.find(m => m.id === activeId);
if (!active) return;
const ft = { ...active.fineTuning };
ft.posX += group.position.x;
ft.posY += group.position.y;
ft.posZ += group.position.z;
ft.rotX += (group.rotation.x * 180) / Math.PI;
ft.rotY += (group.rotation.y * 180) / Math.PI;
ft.rotZ += (group.rotation.z * 180) / Math.PI;
ft.scale = (ft.scale ?? 1) * group.scale.x;
useModelStore.getState().setFineTuning(ft);
group.position.set(0, 0, 0);
group.rotation.set(0, 0, 0);
group.scale.set(1, 1, 1);
triggerHaptic(session, 'left', 0.25, 12);
triggerHaptic(session, 'right', 0.25, 12);
};
// ─── Two-hand mode (orbit + zoom, sempre) ────────────────────
if (lActive && rActive) {
_tmpVecL.setFromMatrixPosition(L.gripWorld);
@@ -116,12 +162,13 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
};
single.current = null;
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
triggerHaptic(session, 'left', 0.5, 30);
triggerHaptic(session, 'right', 0.5, 30);
console.log('[XR][grab] ◆ TWO-HAND start (orbit+zoom)');
}
const d = dual.current;
const deltaQuat = new THREE.Quaternion().setFromUnitVectors(d.startAxis, axisNow);
// ZOOM só quando allowScale=true. Caso contrário, mantém escala 1× (só orbita).
const scaleRatio = allowScale
? THREE.MathUtils.clamp(distNow / d.startDist, 0.1, 10)
: 1;
@@ -137,10 +184,27 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
.multiply(tToOrigin)
.multiply(d.startGroupWorld);
applyWorldMatrixToLocal(group, m);
// Decompose target world matrix
const targetPos = new THREE.Vector3();
const targetQuat = new THREE.Quaternion();
const targetScale = new THREE.Vector3();
let parentLocalMatrix = m;
if (group.parent) {
group.parent.updateMatrixWorld();
parentLocalMatrix = new THREE.Matrix4().copy(group.parent.matrixWorld).invert().multiply(m);
}
parentLocalMatrix.decompose(targetPos, targetQuat, targetScale);
// Apply smooth interpolation (anti-jitter dampening)
const t = Math.min(1.0, dt * 18);
group.position.lerp(targetPos, t);
group.quaternion.slerp(targetQuat, t);
group.scale.lerp(targetScale, t);
return;
} else if (dual.current) {
console.log('[XR][grab] ◆ TWO-HAND end');
commitGrabTransforms();
dual.current = null;
}
@@ -156,6 +220,7 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
const offsetWorld = new THREE.Vector3().subVectors(groupWorldPos, ctrlPos);
single.current = { hand: activeHand, offsetWorld };
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
triggerHaptic(session, activeHand, 0.45, 25);
console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — pan only`);
}
@@ -168,8 +233,13 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
const invParent = new THREE.Matrix4().copy(group.parent.matrixWorld).invert();
targetWorldPos.applyMatrix4(invParent);
}
group.position.copy(targetWorldPos);
// Amortecimento suave da translação
const t = Math.min(1.0, dt * 18);
group.position.lerp(targetWorldPos, t);
} else if (single.current) {
console.log('[XR][grab] ● ONE-HAND end');
commitGrabTransforms();
single.current = null;
}
});