🚀 Auto-deploy: melhoria no snap e medição AR em 29/05/2026 20:32:41
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
detectCircularEdgeAtPoint3D,
|
||||
findNearestEdgeSegment3D
|
||||
} from './SmartMeasure';
|
||||
import { xrCalibration, pushXRRealPoint } from './xrCalibrationBus';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const TRIG_ON = 0.7;
|
||||
const TRIG_OFF = 0.3;
|
||||
@@ -80,12 +82,31 @@ export function XRControllerMeasure() {
|
||||
// running them every frame at 72fps can stall the Quest right after
|
||||
// toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz.
|
||||
// Trigger/A/B/L-trigger polling continues every frame for responsiveness.
|
||||
const hitTestSourceRef = useRef<any>(null);
|
||||
const hitTestRequestedRef = useRef<boolean>(false);
|
||||
|
||||
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
||||
const measureMode = useModelStore.getState().measureMode;
|
||||
const selectionMode = useModelStore.getState().selectionMode;
|
||||
|
||||
// Alinhamento Virt/Real ativo
|
||||
const isAligning = xrCalibration.alignType === 'virt-real' &&
|
||||
(xrCalibration.step === 'await-real-1' ||
|
||||
xrCalibration.step === 'await-virtual-1' ||
|
||||
xrCalibration.step === 'await-real-2' ||
|
||||
xrCalibration.step === 'await-virtual-2' ||
|
||||
xrCalibration.step === 'await-real-3' ||
|
||||
xrCalibration.step === 'await-virtual-3');
|
||||
|
||||
if (laserRef.current) laserRef.current.visible = false;
|
||||
if (tipRef.current) tipRef.current.visible = false;
|
||||
if ((!measureMode && !selectionMode) || !frame) return;
|
||||
if ((!measureMode && !selectionMode && !isAligning) || !frame) {
|
||||
if (hitTestRequestedRef.current) {
|
||||
hitTestSourceRef.current = null;
|
||||
hitTestRequestedRef.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const session = frame.session;
|
||||
const refSpace = gl.xr.getReferenceSpace();
|
||||
@@ -126,6 +147,43 @@ export function XRControllerMeasure() {
|
||||
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
||||
raycaster.current.far = MAX_RAY;
|
||||
|
||||
// Hit test do mundo real para alinhamento
|
||||
const isRealStep = xrCalibration.alignType === 'virt-real' &&
|
||||
(xrCalibration.step === 'await-real-1' ||
|
||||
xrCalibration.step === 'await-real-2' ||
|
||||
xrCalibration.step === 'await-real-3');
|
||||
|
||||
if (session && right && !hitTestRequestedRef.current && isAligning) {
|
||||
hitTestRequestedRef.current = true;
|
||||
session.requestHitTestSource({ space: right.targetRaySpace })
|
||||
.then((source) => {
|
||||
hitTestSourceRef.current = source;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[XR] Failed to request controller hit test source:', err);
|
||||
hitTestRequestedRef.current = false;
|
||||
});
|
||||
}
|
||||
|
||||
let realHitPoint: THREE.Vector3 | null = null;
|
||||
if (isRealStep) {
|
||||
if (hitTestSourceRef.current && frame) {
|
||||
const results = frame.getHitTestResults(hitTestSourceRef.current);
|
||||
if (results.length > 0) {
|
||||
const poseResult = results[0].getPose(refSpace);
|
||||
if (poseResult) {
|
||||
realHitPoint = new THREE.Vector3().setFromMatrixPosition(
|
||||
new THREE.Matrix4().fromArray(poseResult.transform.matrix)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!realHitPoint) {
|
||||
// Fallback: 1.5 metros na direção do controle
|
||||
realHitPoint = tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(1.5));
|
||||
}
|
||||
}
|
||||
|
||||
let snappedPoint: THREE.Vector3 | null = null;
|
||||
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
|
||||
let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; modelId?: string; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
|
||||
@@ -240,27 +298,32 @@ export function XRControllerMeasure() {
|
||||
|
||||
// ── Update laser visual ───────────────────────────────────────────
|
||||
if (laserRef.current && tipRef.current) {
|
||||
const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY));
|
||||
const end = isRealStep
|
||||
? realHitPoint
|
||||
: (snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY)));
|
||||
|
||||
const positions = laserGeom.current.attributes.position as THREE.BufferAttribute;
|
||||
positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z);
|
||||
positions.setXYZ(1, end.x, end.y, end.z);
|
||||
positions.needsUpdate = true;
|
||||
laserGeom.current.computeBoundingSphere();
|
||||
|
||||
const color = selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308');
|
||||
const color = isAligning
|
||||
? '#eab308' // dourado para alinhamento Virt/Real
|
||||
: (selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308'));
|
||||
|
||||
tipColor.current.set(color);
|
||||
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
||||
((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current);
|
||||
|
||||
laserRef.current.visible = true;
|
||||
if (snappedPoint) {
|
||||
|
||||
const tipPos = isRealStep ? realHitPoint : snappedPoint;
|
||||
if (tipPos) {
|
||||
tipRef.current.visible = true;
|
||||
tipRef.current.position.copy(snappedPoint);
|
||||
// Tip scales with distance from controller. Snap "forte" (vértice,
|
||||
// aresta, furo) recebe um marcador ~3× maior do que uma superfície
|
||||
// genérica, para que o ponto destacado fique bem visível.
|
||||
const dist = tmpOrigin.current.distanceTo(snappedPoint);
|
||||
const strongSnap = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
||||
tipRef.current.position.copy(tipPos);
|
||||
const dist = tmpOrigin.current.distanceTo(tipPos);
|
||||
const strongSnap = isAligning || snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
||||
const factor = strongSnap ? 0.030 : 0.012;
|
||||
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
||||
tipRef.current.scale.setScalar(s);
|
||||
@@ -274,6 +337,21 @@ export function XRControllerMeasure() {
|
||||
if (!trigState.current && trigVal > TRIG_ON) {
|
||||
trigState.current = true;
|
||||
const st = useModelStore.getState();
|
||||
|
||||
if (isRealStep) {
|
||||
if (realHitPoint) {
|
||||
pushXRRealPoint(realHitPoint);
|
||||
const stepNum = xrCalibration.step.endsWith('1') ? 1 : xrCalibration.step.endsWith('2') ? 2 : 3;
|
||||
toast.success(`Ponto real ${stepNum} registrado!`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAligning) {
|
||||
// Ignora o trigger físico para medições quando no modo virtual pois o clique do modelo é capturado pelo R3F onClick.
|
||||
return;
|
||||
}
|
||||
|
||||
if (st.selectionMode) {
|
||||
// Fresh raycast on the press itself (not every frame) for selection
|
||||
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
||||
|
||||
@@ -904,10 +904,16 @@ function CalibrationTab() {
|
||||
|
||||
const isCalibrating = xrCalibration.step !== 'idle' && xrCalibration.step !== 'done';
|
||||
const isDone = xrCalibration.step === 'done';
|
||||
const progress = isDone ? 1 : xrCalibration.progress;
|
||||
|
||||
const onClickCalibrar = () => {
|
||||
if (isCalibrating || isDone) {
|
||||
const isCubeMode = xrCalibration.alignType === 'cube';
|
||||
const isVirtRealMode = xrCalibration.alignType === 'virt-real';
|
||||
|
||||
const onClickCube = () => {
|
||||
if (isCalibrating && isCubeMode) {
|
||||
cancelXRCalibration();
|
||||
return;
|
||||
}
|
||||
if (isDone && isCubeMode) {
|
||||
cancelXRCalibration();
|
||||
return;
|
||||
}
|
||||
@@ -919,7 +925,27 @@ function CalibrationTab() {
|
||||
toast.error('Peça travada — destranque a peça para calibrar');
|
||||
return;
|
||||
}
|
||||
startXRCalibration(active.id);
|
||||
startXRCalibration(active.id, 'cube');
|
||||
};
|
||||
|
||||
const onClickVirtReal = () => {
|
||||
if (isCalibrating && isVirtRealMode) {
|
||||
cancelXRCalibration();
|
||||
return;
|
||||
}
|
||||
if (isDone && isVirtRealMode) {
|
||||
cancelXRCalibration();
|
||||
return;
|
||||
}
|
||||
if (!active) {
|
||||
toast.error('Selecione uma peça antes de calibrar');
|
||||
return;
|
||||
}
|
||||
if (active.locked) {
|
||||
toast.error('Peça travada — destranque a peça para calibrar');
|
||||
return;
|
||||
}
|
||||
startXRCalibration(active.id, 'virt-real');
|
||||
};
|
||||
|
||||
const onResetCalibration = () => {
|
||||
@@ -935,10 +961,54 @@ function CalibrationTab() {
|
||||
'await-model-2': '2/3 - Clique na face correspondente da peça',
|
||||
'await-cube-3': '3/3 (Verificar) - Clique em uma 3ª face do Cubo (opcional)',
|
||||
'await-model-3': '3/3 - Clique na face correspondente da peça',
|
||||
'await-real-1': '1/3 Real - Clique na 1ª superfície da peça REAL',
|
||||
'await-virtual-1': '1/3 Virtual - Clique na 1ª correspondente da peça VIRTUAL',
|
||||
'await-real-2': '2/3 Real - Clique na 2ª superfície da peça REAL',
|
||||
'await-virtual-2': '2/3 Virtual - Clique na 2ª correspondente da peça VIRTUAL',
|
||||
'await-real-3': '3/3 Real - Clique na 3ª superfície da peça REAL',
|
||||
'await-virtual-3': '3/3 Virtual - Clique na 3ª correspondente da peça VIRTUAL',
|
||||
'done': 'Calibração concluída com sucesso!',
|
||||
};
|
||||
|
||||
const currentHint = STEP_HINTS_AR[xrCalibration.step] ?? 'Clique em "Iniciar Calibrar" para alinhar o modelo';
|
||||
const currentHint = STEP_HINTS_AR[xrCalibration.step] ?? 'Selecione "Cubo" ou "Virt/Real" para calibrar o modelo';
|
||||
|
||||
// Configurações dinâmicas de estados de botão
|
||||
let cubeLabel = 'Cubo';
|
||||
let cubeActive = false;
|
||||
let cubeColor = '#3b82f6';
|
||||
|
||||
let vrLabel = 'Virt/Real';
|
||||
let vrActive = false;
|
||||
let vrColor = '#3b82f6';
|
||||
|
||||
if (isCalibrating) {
|
||||
if (isCubeMode) {
|
||||
cubeLabel = 'Cancelar';
|
||||
cubeActive = true;
|
||||
cubeColor = '#d97706';
|
||||
} else {
|
||||
cubeColor = '#475569';
|
||||
}
|
||||
|
||||
if (isVirtRealMode) {
|
||||
vrLabel = 'Cancelar';
|
||||
vrActive = true;
|
||||
vrColor = '#d97706';
|
||||
} else {
|
||||
vrColor = '#475569';
|
||||
}
|
||||
} else if (isDone) {
|
||||
if (isCubeMode) {
|
||||
cubeLabel = 'Concluído';
|
||||
cubeActive = true;
|
||||
cubeColor = '#22c55e';
|
||||
}
|
||||
if (isVirtRealMode) {
|
||||
vrLabel = 'Concluído';
|
||||
vrActive = true;
|
||||
vrColor = '#22c55e';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -948,7 +1018,7 @@ function CalibrationTab() {
|
||||
</Text>
|
||||
|
||||
<Text position={[-0.24, 0.07, 0]} fontSize={0.008} color="#cbd5e1" anchorX="left" anchorY="top" maxWidth={0.28}>
|
||||
Alinhe os eixos intrínsecos do modelo 3D com a estrutura física correspondente no ambiente.
|
||||
Alinhe o modelo virtual 3D com a peça real usando o Cubo ou clique em 3 pontos de controle ("Virt/Real").
|
||||
</Text>
|
||||
|
||||
{/* Caixa de status do passo atual */}
|
||||
@@ -957,10 +1027,10 @@ function CalibrationTab() {
|
||||
<planeGeometry args={[0.26, 0.05]} />
|
||||
<meshBasicMaterial color="#111827" transparent opacity={0.65} />
|
||||
</mesh>
|
||||
<Text position={[0.01, 0, 0.001]} fontSize={0.0085} color={isDone ? '#22c55e' : isCalibrating ? '#f59e0b' : '#ffffff'} anchorX="left" anchorY="middle" maxWidth={0.24}>
|
||||
<Text position={[0.01, 0, 0.001]} fontSize={0.008} color={isDone ? '#22c55e' : isCalibrating ? '#f59e0b' : '#ffffff'} anchorX="left" anchorY="middle" maxWidth={0.24}>
|
||||
{currentHint}
|
||||
</Text>
|
||||
{isDone && Number.isFinite(xrCalibration.verifyErrorDeg) && (
|
||||
{isDone && Number.isFinite(xrCalibration.verifyErrorDeg) && isCubeMode && (
|
||||
<Text position={[0.01, -0.016, 0.001]} fontSize={0.007} color="#22c55e" anchorX="left" anchorY="middle">
|
||||
Erro residual calculado: {xrCalibration.verifyErrorDeg.toFixed(1)}°
|
||||
</Text>
|
||||
@@ -970,20 +1040,30 @@ function CalibrationTab() {
|
||||
{/* Botões na parte inferior esquerda */}
|
||||
<group position={[-0.24, -0.06, 0]}>
|
||||
<XR3DButton
|
||||
position={[0.06, 0, 0]}
|
||||
size={[0.11, 0.026]}
|
||||
label={isDone ? 'Concluído' : isCalibrating ? 'Cancelar' : 'Iniciar Calibrar'}
|
||||
active={isCalibrating || isDone}
|
||||
color={isDone ? '#22c55e' : isCalibrating ? '#d97706' : '#3b82f6'}
|
||||
onClick={onClickCalibrar}
|
||||
position={[0.045, 0, 0]}
|
||||
size={[0.08, 0.026]}
|
||||
label={cubeLabel}
|
||||
active={cubeActive}
|
||||
color={cubeColor}
|
||||
onClick={onClickCube}
|
||||
fontSize={0.008}
|
||||
/>
|
||||
|
||||
<XR3DButton
|
||||
position={[0.135, 0, 0]}
|
||||
size={[0.08, 0.026]}
|
||||
label={vrLabel}
|
||||
active={vrActive}
|
||||
color={vrColor}
|
||||
onClick={onClickVirtReal}
|
||||
fontSize={0.008}
|
||||
/>
|
||||
|
||||
{active?.calibrationQuat && !isCalibrating && (
|
||||
<XR3DButton
|
||||
position={[0.18, 0, 0]}
|
||||
size={[0.09, 0.026]}
|
||||
label="Remover Cal."
|
||||
position={[0.225, 0, 0]}
|
||||
size={[0.08, 0.026]}
|
||||
label="Limpar"
|
||||
color="#dc2626"
|
||||
onClick={onResetCalibration}
|
||||
fontSize={0.008}
|
||||
|
||||
@@ -9,6 +9,12 @@ export type XRCalibrationStep =
|
||||
| 'await-model-2'
|
||||
| 'await-cube-3' // opcional verificação
|
||||
| 'await-model-3'
|
||||
| 'await-real-1'
|
||||
| 'await-virtual-1'
|
||||
| 'await-real-2'
|
||||
| 'await-virtual-2'
|
||||
| 'await-real-3'
|
||||
| 'await-virtual-3'
|
||||
| 'done';
|
||||
|
||||
interface XRPair {
|
||||
@@ -19,7 +25,10 @@ interface XRPair {
|
||||
interface XRCalState {
|
||||
step: XRCalibrationStep;
|
||||
modelId: string | null;
|
||||
alignType: 'cube' | 'virt-real' | null;
|
||||
pairs: XRPair[];
|
||||
realPoints: THREE.Vector3[];
|
||||
virtualPoints: THREE.Vector3[];
|
||||
pendingCube: THREE.Vector3 | null;
|
||||
progress: number;
|
||||
verifyErrorDeg: number;
|
||||
@@ -29,7 +38,10 @@ interface XRCalState {
|
||||
export const xrCalibration: XRCalState = {
|
||||
step: 'idle',
|
||||
modelId: null,
|
||||
alignType: null,
|
||||
pairs: [],
|
||||
realPoints: [],
|
||||
virtualPoints: [],
|
||||
pendingCube: null,
|
||||
progress: 0,
|
||||
verifyErrorDeg: NaN,
|
||||
@@ -47,20 +59,31 @@ export function subscribeXRCalibration(fn: () => void): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
export function startXRCalibration(modelId: string) {
|
||||
xrCalibration.step = 'await-cube-1';
|
||||
export function startXRCalibration(modelId: string, type: 'cube' | 'virt-real' = 'cube') {
|
||||
xrCalibration.modelId = modelId;
|
||||
xrCalibration.alignType = type;
|
||||
xrCalibration.pairs = [];
|
||||
xrCalibration.realPoints = [];
|
||||
xrCalibration.virtualPoints = [];
|
||||
xrCalibration.pendingCube = null;
|
||||
xrCalibration.progress = 0;
|
||||
xrCalibration.verifyErrorDeg = NaN;
|
||||
|
||||
if (type === 'cube') {
|
||||
xrCalibration.step = 'await-cube-1';
|
||||
} else {
|
||||
xrCalibration.step = 'await-real-1';
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
export function cancelXRCalibration() {
|
||||
xrCalibration.step = 'idle';
|
||||
xrCalibration.modelId = null;
|
||||
xrCalibration.alignType = null;
|
||||
xrCalibration.pairs = [];
|
||||
xrCalibration.realPoints = [];
|
||||
xrCalibration.virtualPoints = [];
|
||||
xrCalibration.pendingCube = null;
|
||||
xrCalibration.progress = 0;
|
||||
xrCalibration.verifyErrorDeg = NaN;
|
||||
@@ -83,6 +106,44 @@ export function pushXRCubeFace(dirWorld: THREE.Vector3) {
|
||||
notify();
|
||||
}
|
||||
|
||||
export function pushXRRealPoint(pointWorld: THREE.Vector3) {
|
||||
if (xrCalibration.alignType !== 'virt-real') return;
|
||||
const p = pointWorld.clone();
|
||||
if (xrCalibration.step === 'await-real-1') {
|
||||
xrCalibration.realPoints[0] = p;
|
||||
xrCalibration.step = 'await-virtual-1';
|
||||
xrCalibration.progress = 0.16;
|
||||
} else if (xrCalibration.step === 'await-real-2') {
|
||||
xrCalibration.realPoints[1] = p;
|
||||
xrCalibration.step = 'await-virtual-2';
|
||||
xrCalibration.progress = 0.5;
|
||||
} else if (xrCalibration.step === 'await-real-3') {
|
||||
xrCalibration.realPoints[2] = p;
|
||||
xrCalibration.step = 'await-virtual-3';
|
||||
xrCalibration.progress = 0.83;
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
export function pushXRVirtualPoint(pointLocalPivot: THREE.Vector3) {
|
||||
if (xrCalibration.alignType !== 'virt-real') return;
|
||||
const p = pointLocalPivot.clone();
|
||||
if (xrCalibration.step === 'await-virtual-1') {
|
||||
xrCalibration.virtualPoints[0] = p;
|
||||
xrCalibration.step = 'await-real-2';
|
||||
xrCalibration.progress = 0.33;
|
||||
} else if (xrCalibration.step === 'await-virtual-2') {
|
||||
xrCalibration.virtualPoints[1] = p;
|
||||
xrCalibration.step = 'await-real-3';
|
||||
xrCalibration.progress = 0.66;
|
||||
} else if (xrCalibration.step === 'await-virtual-3') {
|
||||
xrCalibration.virtualPoints[2] = p;
|
||||
xrCalibration.step = 'done';
|
||||
xrCalibration.progress = 1.0;
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
function snapToPrincipalAxis(v: THREE.Vector3, maxDeg = 12): THREE.Vector3 {
|
||||
const cosT = Math.cos(THREE.MathUtils.degToRad(maxDeg));
|
||||
const axes = [
|
||||
@@ -162,3 +223,50 @@ export function computeXRVerifyError(pairs: XRPair[]): number {
|
||||
const cos = THREE.MathUtils.clamp(predicted.dot(target), -1, 1);
|
||||
return THREE.MathUtils.radToDeg(Math.acos(cos));
|
||||
}
|
||||
|
||||
export function computeVirtRealTransform(
|
||||
v: THREE.Vector3[],
|
||||
r: THREE.Vector3[]
|
||||
): { quaternion: THREE.Quaternion; position: THREE.Vector3 } | null {
|
||||
if (v.length < 3 || r.length < 3) return null;
|
||||
|
||||
const v1 = v[0];
|
||||
const v2 = v[1];
|
||||
const v3 = v[2];
|
||||
|
||||
const r1 = r[0];
|
||||
const r2 = r[1];
|
||||
const r3 = r[2];
|
||||
|
||||
// 1. Basis local virtual
|
||||
const xv = new THREE.Vector3().subVectors(v2, v1).normalize();
|
||||
const v3_proj = new THREE.Vector3().subVectors(v3, v1);
|
||||
const dot_v = v3_proj.dot(xv);
|
||||
const yv = v3_proj.clone().sub(xv.clone().multiplyScalar(dot_v));
|
||||
if (yv.lengthSq() < 1e-6) return null;
|
||||
yv.normalize();
|
||||
const zv = new THREE.Vector3().crossVectors(xv, yv).normalize();
|
||||
|
||||
// 2. Basis de mundo real
|
||||
const xr = new THREE.Vector3().subVectors(r2, r1).normalize();
|
||||
const r3_proj = new THREE.Vector3().subVectors(r3, r1);
|
||||
const dot_r = r3_proj.dot(xr);
|
||||
const yr = r3_proj.clone().sub(xr.clone().multiplyScalar(dot_r));
|
||||
if (yr.lengthSq() < 1e-6) return null;
|
||||
yr.normalize();
|
||||
const zr = new THREE.Vector3().crossVectors(xr, yr).normalize();
|
||||
|
||||
// 3. Matriz de rotação R que mapeia virtual para real
|
||||
const Mv = new THREE.Matrix4().makeBasis(xv, yv, zv);
|
||||
const Mr = new THREE.Matrix4().makeBasis(xr, yr, zr);
|
||||
const Mv_t = Mv.clone().transpose();
|
||||
const R = Mr.clone().multiply(Mv_t);
|
||||
|
||||
const quaternion = new THREE.Quaternion().setFromRotationMatrix(R);
|
||||
|
||||
// 4. Translação T = r1 - R * v1
|
||||
const rv1 = v1.clone().applyQuaternion(quaternion);
|
||||
const position = new THREE.Vector3().subVectors(r1, rv1);
|
||||
|
||||
return { quaternion, position };
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
pushXRModelFaceNormal,
|
||||
computeXRCalibrationQuaternion,
|
||||
subscribeXRCalibration,
|
||||
pushXRRealPoint,
|
||||
pushXRVirtualPoint,
|
||||
computeVirtRealTransform,
|
||||
} from '@/components/three/xrCalibrationBus';
|
||||
|
||||
// --- Diagnóstico XR ---
|
||||
@@ -110,6 +113,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
const calGroupRef = useRef<THREE.Group>(null);
|
||||
const localFrameRef = useRef<THREE.Group>(null);
|
||||
const topGroupRef = useRef<THREE.Group>(null);
|
||||
|
||||
// Escuta mudanças de calibração para re-renderizar no AR
|
||||
const [, setCalTick] = useState(0);
|
||||
@@ -229,6 +233,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={topGroupRef}
|
||||
scale={[renderFactor, renderFactor, renderFactor]}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -248,6 +253,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
||||
// Calibração XR da Peça: captura normal da face do modelo em espaço de mundo.
|
||||
if (
|
||||
xrCalibration.modelId === sceneModel.id &&
|
||||
xrCalibration.alignType === 'cube' &&
|
||||
(xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') &&
|
||||
e.face && e.object
|
||||
) {
|
||||
@@ -267,6 +273,48 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Calibração Virt/Real da Peça: captura os pontos virtuais.
|
||||
if (
|
||||
xrCalibration.modelId === sceneModel.id &&
|
||||
xrCalibration.alignType === 'virt-real' &&
|
||||
(xrCalibration.step === 'await-virtual-1' || xrCalibration.step === 'await-virtual-2' || xrCalibration.step === 'await-virtual-3') &&
|
||||
localFrameRef.current
|
||||
) {
|
||||
const localPoint = localFrameRef.current.worldToLocal(e.point.clone());
|
||||
const localPivotPoint = localPoint.clone().sub(modelInfo.center);
|
||||
const stepNum = xrCalibration.step.endsWith('1') ? 1 : xrCalibration.step.endsWith('2') ? 2 : 3;
|
||||
|
||||
if (stepNum === 3 && xrCalibration.realPoints.length >= 3 && topGroupRef.current) {
|
||||
const vPoints = [...xrCalibration.virtualPoints, localPivotPoint];
|
||||
const realWorldPoints = xrCalibration.realPoints;
|
||||
const parentPoints = realWorldPoints.map(rw =>
|
||||
topGroupRef.current!.worldToLocal(rw.clone())
|
||||
);
|
||||
|
||||
const res = computeVirtRealTransform(vPoints, parentPoints);
|
||||
if (res) {
|
||||
const { quaternion, position } = res;
|
||||
useModelStore.getState().setCalibration(sceneModel.id, [quaternion.x, quaternion.y, quaternion.z, quaternion.w]);
|
||||
useModelStore.getState().setFineTuning({
|
||||
posX: position.x,
|
||||
posY: position.y,
|
||||
posZ: position.z,
|
||||
rotX: 0,
|
||||
rotY: 0,
|
||||
rotZ: 0,
|
||||
});
|
||||
pushXRVirtualPoint(localPivotPoint);
|
||||
toast.success("Peça virtual ajustada com sucesso na peça real!");
|
||||
} else {
|
||||
toast.error("Erro ao alinhar: pontos são colineares!");
|
||||
}
|
||||
} else {
|
||||
pushXRVirtualPoint(localPivotPoint);
|
||||
toast.success(`Ponto virtual ${stepNum} registrado!`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Translação */}
|
||||
@@ -760,6 +808,53 @@ function XRSnapHandler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Desktop Aligner Fallback for Virt/Real Calibration ─
|
||||
function XRVirtRealDesktopAligner() {
|
||||
const { gl, camera, scene } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
const isAligning = xrCalibration.alignType === 'virt-real' &&
|
||||
(xrCalibration.step === 'await-real-1' ||
|
||||
xrCalibration.step === 'await-real-2' ||
|
||||
xrCalibration.step === 'await-real-3');
|
||||
if (!isAligning) return;
|
||||
if (gl.xr.isPresenting) return;
|
||||
|
||||
const mouse = new THREE.Vector2();
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true);
|
||||
|
||||
const activeModelId = useModelStore.getState().activeModelId;
|
||||
const hit = intersects.find(i => {
|
||||
if (i.object.userData?.__edgeLine) return false;
|
||||
let cur: THREE.Object3D | null = i.object;
|
||||
while (cur) {
|
||||
if (cur.userData?.modelId === activeModelId) return false;
|
||||
cur = cur.parent;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const point = hit ? hit.point.clone() : new THREE.Vector3(0, 0, 0);
|
||||
pushXRRealPoint(point);
|
||||
|
||||
const stepNum = xrCalibration.step.endsWith('1') ? 1 : xrCalibration.step.endsWith('2') ? 2 : 3;
|
||||
toast.success(`Ponto real ${stepNum} registrado (Simulado)!`);
|
||||
};
|
||||
|
||||
gl.domElement.addEventListener('click', onClick);
|
||||
return () => gl.domElement.removeEventListener('click', onClick);
|
||||
}, [gl, camera, scene, xrCalibration.step, xrCalibration.alignType]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── XR Grid ───────────────────────────────────────────
|
||||
function XRGrid() {
|
||||
const showGrid = useModelStore((s) => s.showGrid);
|
||||
@@ -1025,6 +1120,7 @@ const XRSession = () => {
|
||||
)}
|
||||
|
||||
<XRSnapHandler />
|
||||
<XRVirtRealDesktopAligner />
|
||||
<XRGrid />
|
||||
<XRGridAutoFollower />
|
||||
</XR>
|
||||
|
||||
Reference in New Issue
Block a user