🚀 Auto-deploy: melhoria no snap e medição AR em 30/05/2026 10:28:12

This commit is contained in:
2026-05-30 10:28:12 +00:00
parent 7ff4ed3176
commit 5a08868b83
3 changed files with 136 additions and 15 deletions
+87 -3
View File
@@ -3,6 +3,7 @@ import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
import { useModelStore } from '@/stores/useModelStore';
import { activeModelGroupRef } from './xrCalibrationBus';
interface XRGrabbableProps {
/**
@@ -72,7 +73,71 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
single.current = null;
}, [scaleResetNonce]);
useFrame(() => {
useEffect(() => {
activeModelGroupRef.current = groupRef.current;
return () => {
if (activeModelGroupRef.current === groupRef.current) {
activeModelGroupRef.current = null;
}
};
});
const syncGrabToStore = () => {
const group = groupRef.current;
if (!group) return;
const state = useModelStore.getState();
const activeModelId = state.activeModelId;
if (!activeModelId) return;
const activeModel = state.models.find(m => m.id === activeModelId);
if (!activeModel) return;
const ft = { ...activeModel.fineTuning };
const renderFactor = state.scaleRatio?.factor ?? 1;
// Model's matrix relative to XRGrabbable
const posModel = new THREE.Vector3(ft.posX * renderFactor, ft.posY * renderFactor, ft.posZ * renderFactor);
const rotModel = new THREE.Euler(
(ft.rotX * Math.PI) / 180,
(ft.rotY * Math.PI) / 180,
(ft.rotZ * Math.PI) / 180,
'YXZ'
);
const quatModel = new THREE.Quaternion().setFromEuler(rotModel);
const scaleModel = new THREE.Vector3(ft.scale * renderFactor, ft.scale * renderFactor, ft.scale * renderFactor);
const mModel = new THREE.Matrix4().compose(posModel, quatModel, scaleModel);
// Grab matrix of XRGrabbable group
const mGrab = new THREE.Matrix4().compose(group.position, group.quaternion, group.scale);
// Combined matrix
const mCombined = new THREE.Matrix4().multiplyMatrices(mGrab, mModel);
const finalPos = new THREE.Vector3();
const finalQuat = new THREE.Quaternion();
const finalScale = new THREE.Vector3();
mCombined.decompose(finalPos, finalQuat, finalScale);
// Reset grabbable group to identity so the transform is held entirely by fineTuning
group.position.set(0, 0, 0);
group.quaternion.identity();
group.scale.set(1, 1, 1);
// Save to store
state.setFineTuning({
posX: finalPos.x / renderFactor,
posY: finalPos.y / renderFactor,
posZ: finalPos.z / renderFactor,
rotX: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').x * 180) / Math.PI,
rotY: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').y * 180) / Math.PI,
rotZ: (new THREE.Euler().setFromQuaternion(finalQuat, 'YXZ').z * 180) / Math.PI,
scale: finalScale.x / renderFactor,
});
};
useFrame((_state, dt) => {
const group = groupRef.current;
if (!group) return;
const snap: ControllerGrabSnapshot = grab.current;
@@ -144,6 +209,7 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
} else if (dual.current) {
console.log('[XR][grab] ◆ TWO-HAND end');
dual.current = null;
syncGrabToStore();
}
// ─── One-hand mode (PAN ONLY - HORIZONTAL X/Z) ────────────────
@@ -162,10 +228,26 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
single.current = { hand: activeHand, offsetWorld, startWorldY };
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — horizontal pan`);
console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — horizontal pan & rotation`);
}
// Target world position = controller + initial offset
// Rotação Y via analógico do controle ativo
const gp = slot.gamepad;
if (gp && gp.axes.length >= 4) {
const stickX = gp.axes[2]; // horizontal do thumbstick
const deadzone = 0.15;
if (Math.abs(stickX) > deadzone) {
const rotationSpeed = 1.5; // rad por segundo
const angle = Math.sign(stickX) * (Math.abs(stickX) - deadzone) * rotationSpeed * dt;
const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -angle);
group.quaternion.multiplyQuaternions(q, group.quaternion);
// Rotaciona o offset para girar ao redor da mão
single.current.offsetWorld.applyQuaternion(q);
}
}
// Target world position = controller + initial/rotated 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!
@@ -179,7 +261,9 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
}
group.position.copy(targetWorldPos);
} else if (single.current) {
console.log('[XR][grab] ● ONE-HAND end');
single.current = null;
syncGrabToStore();
}
});
+2
View File
@@ -270,3 +270,5 @@ export function computeVirtRealTransform(
return { quaternion, position };
}
export const activeModelGroupRef = { current: null as THREE.Group | null };
+47 -12
View File
@@ -34,6 +34,7 @@ import {
pushXRRealPoint,
pushXRVirtualPoint,
computeVirtRealTransform,
activeModelGroupRef,
} from '@/components/three/xrCalibrationBus';
// --- Diagnóstico XR ---
@@ -973,20 +974,54 @@ function XRVirtRealDesktopAligner() {
function XRGrid() {
const showGrid = useModelStore((s) => s.showGrid);
const gridY = useModelStore((s) => s.gridY);
const activeModelId = useModelStore((s) => s.activeModelId);
const activeModel = useModelStore((s) => s.models.find(m => m.id === activeModelId));
const groupRef = useRef<THREE.Group>(null);
useFrame(() => {
if (!showGrid || !groupRef.current) return;
let px = 0;
let pz = 0;
let ry = 0;
if (activeModelGroupRef.current) {
const g = activeModelGroupRef.current;
const wp = new THREE.Vector3();
g.getWorldPosition(wp);
px = wp.x;
pz = wp.z;
const wq = new THREE.Quaternion();
g.getWorldQuaternion(wq);
const euler = new THREE.Euler().setFromQuaternion(wq, 'YXZ');
ry = euler.y;
} else if (activeModel) {
px = activeModel.fineTuning.posX;
pz = activeModel.fineTuning.posZ;
ry = (activeModel.fineTuning.rotY * Math.PI) / 180;
}
groupRef.current.position.set(px, gridY, pz);
groupRef.current.rotation.set(0, ry, 0);
});
if (!showGrid) return null;
return (
<Grid
position={[0, gridY, 0]}
infiniteGrid
cellSize={0.01}
sectionSize={0.1}
cellThickness={0.5}
sectionThickness={1}
cellColor="#334155"
sectionColor="#475569"
fadeDistance={5}
fadeStrength={1}
/>
<group ref={groupRef}>
<Grid
infiniteGrid
cellSize={0.01}
sectionSize={0.1}
cellThickness={0.5}
sectionThickness={1}
cellColor="#334155"
sectionColor="#475569"
fadeDistance={5}
fadeStrength={1}
/>
</group>
);
}