🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 02:00:33
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user