🚀 Auto-deploy: melhoria no snap e medição AR em 28/05/2026 21:16:09
This commit is contained in:
@@ -12,8 +12,16 @@ import {
|
|||||||
stopRecording, captureScreenshot, formatElapsed,
|
stopRecording, captureScreenshot, formatElapsed,
|
||||||
} from '@/hooks/useCanvasRecorder';
|
} from '@/hooks/useCanvasRecorder';
|
||||||
import { getAllModelLocalGroups } from '@/lib/modelTransforms';
|
import { getAllModelLocalGroups } from '@/lib/modelTransforms';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
xrCalibration,
|
||||||
|
startXRCalibration,
|
||||||
|
cancelXRCalibration,
|
||||||
|
subscribeXRCalibration,
|
||||||
|
} from './xrCalibrationBus';
|
||||||
|
import { XRViewCube } from './XRViewCube';
|
||||||
|
|
||||||
type Tab = 'scene' | 'tools' | 'cuts' | 'inspection' | 'share' | 'capture' | 'webxr';
|
type Tab = 'scene' | 'tools' | 'calib' | 'cuts' | 'inspection' | 'share' | 'capture' | 'webxr';
|
||||||
|
|
||||||
interface XRHudInWorldProps {
|
interface XRHudInWorldProps {
|
||||||
freeMove: boolean;
|
freeMove: boolean;
|
||||||
@@ -225,6 +233,7 @@ function FloatingPanel({
|
|||||||
const tabs: { id: Tab; label: string; icon: string }[] = [
|
const tabs: { id: Tab; label: string; icon: string }[] = [
|
||||||
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
||||||
{ id: 'tools', label: 'Ferram.', icon: '🛠' },
|
{ id: 'tools', label: 'Ferram.', icon: '🛠' },
|
||||||
|
{ id: 'calib', label: 'Calibrar', icon: '🎯' },
|
||||||
{ id: 'cuts', label: 'Cortes', icon: '✂' },
|
{ id: 'cuts', label: 'Cortes', icon: '✂' },
|
||||||
{ id: 'inspection', label: 'Inspeção', icon: '✓' },
|
{ id: 'inspection', label: 'Inspeção', icon: '✓' },
|
||||||
{ id: 'capture', label: 'Captura', icon: '🎥' },
|
{ id: 'capture', label: 'Captura', icon: '🎥' },
|
||||||
@@ -257,15 +266,16 @@ function FloatingPanel({
|
|||||||
<group position={[0, H / 2 - 0.045, 0.001]}>
|
<group position={[0, H / 2 - 0.045, 0.001]}>
|
||||||
{tabs.map((t, i) => (
|
{tabs.map((t, i) => (
|
||||||
<XR3DButton key={t.id}
|
<XR3DButton key={t.id}
|
||||||
position={[-W / 2 + 0.038 + i * 0.072, 0, 0]} size={[0.066, 0.022]}
|
position={[-W / 2 + 0.0495 + i * 0.063, 0, 0]} size={[0.058, 0.020]}
|
||||||
label={t.label} icon={t.icon} active={tab === t.id}
|
label={t.label} icon={t.icon} active={tab === t.id}
|
||||||
onClick={() => setTab(t.id)} fontSize={0.0068} />
|
onClick={() => setTab(t.id)} fontSize={0.0062} />
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group position={[0, -0.018, 0.001]}>
|
<group position={[0, -0.018, 0.001]}>
|
||||||
{tab === 'scene' && <SceneTab />}
|
{tab === 'scene' && <SceneTab />}
|
||||||
{tab === 'tools' && <ToolsTab {...p} />}
|
{tab === 'tools' && <ToolsTab {...p} />}
|
||||||
|
{tab === 'calib' && <CalibrationTab />}
|
||||||
{tab === 'cuts' && <CutsTab />}
|
{tab === 'cuts' && <CutsTab />}
|
||||||
{tab === 'inspection' && <InspectionTab />}
|
{tab === 'inspection' && <InspectionTab />}
|
||||||
{tab === 'capture' && <CaptureTab />}
|
{tab === 'capture' && <CaptureTab />}
|
||||||
@@ -879,3 +889,123 @@ function RecIndicator() {
|
|||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CalibrationTab() {
|
||||||
|
const activeModelId = useModelStore((s) => s.activeModelId);
|
||||||
|
const models = useModelStore((s) => s.models);
|
||||||
|
const setCalibrationStore = useModelStore((s) => s.setCalibration);
|
||||||
|
const active = models.find((m) => m.id === activeModelId);
|
||||||
|
|
||||||
|
// Escuta o barramento de calibração do XR
|
||||||
|
const [, force] = useState(0);
|
||||||
|
useEffect(() => subscribeXRCalibration(() => force((t) => t + 1)), []);
|
||||||
|
|
||||||
|
const isCalibrating = xrCalibration.step !== 'idle' && xrCalibration.step !== 'done';
|
||||||
|
const isDone = xrCalibration.step === 'done';
|
||||||
|
const progress = isDone ? 1 : xrCalibration.progress;
|
||||||
|
|
||||||
|
const onClickCalibrar = () => {
|
||||||
|
if (isCalibrating || isDone) {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResetCalibration = () => {
|
||||||
|
if (!active) return;
|
||||||
|
setCalibrationStore(active.id, null);
|
||||||
|
toast.success('Calibração removida');
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEP_HINTS_AR: Record<string, string> = {
|
||||||
|
'await-cube-1': '1/3 - Clique numa face do Cubo à direita',
|
||||||
|
'await-model-1': '1/3 - Aponte e clique na face correspondente da peça',
|
||||||
|
'await-cube-2': '2/3 - Clique em outra face do Cubo',
|
||||||
|
'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',
|
||||||
|
'done': 'Calibração concluída com sucesso!',
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentHint = STEP_HINTS_AR[xrCalibration.step] ?? 'Clique em "Iniciar Calibrar" para alinhar o modelo';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Coluna da esquerda: Informações e Fluxo */}
|
||||||
|
<Text position={[-0.24, 0.11, 0]} fontSize={0.012} color="#3b82f6" anchorX="left" anchorY="middle" fontStyle="bold">
|
||||||
|
Calibração 3D da Peça
|
||||||
|
</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.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Caixa de status do passo atual */}
|
||||||
|
<group position={[-0.24, 0.00, 0]}>
|
||||||
|
<mesh position={[0.13, 0, -0.0005]}>
|
||||||
|
<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}>
|
||||||
|
{currentHint}
|
||||||
|
</Text>
|
||||||
|
{isDone && Number.isFinite(xrCalibration.verifyErrorDeg) && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* 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}
|
||||||
|
fontSize={0.008}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{active?.calibrationQuat && !isCalibrating && (
|
||||||
|
<XR3DButton
|
||||||
|
position={[0.18, 0, 0]}
|
||||||
|
size={[0.09, 0.026]}
|
||||||
|
label="Remover Cal."
|
||||||
|
color="#dc2626"
|
||||||
|
onClick={onResetCalibration}
|
||||||
|
fontSize={0.008}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<Text position={[-0.24, -0.11, 0]} fontSize={0.007} color="#64748b" anchorX="left" maxWidth={0.28}>
|
||||||
|
{active ? `Peça ativa: ${active.fileName}` : 'Selecione uma peça na aba "Cena"'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Coluna da direita: Cubo de views 3D interativo */}
|
||||||
|
<group position={[0.14, -0.01, 0.04]}>
|
||||||
|
<XRViewCube activeModelId={activeModelId} size={0.075} />
|
||||||
|
{/* Tripé dos eixos abaixo do cubo */}
|
||||||
|
<group position={[0, -0.06, -0.04]} scale={[0.035, 0.035, 0.035]}>
|
||||||
|
<arrowHelper args={[new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 0), 0.7, 0xff4444, 0.18, 0.12]} />
|
||||||
|
<arrowHelper args={[new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 0), 0.7, 0x44ff66, 0.18, 0.12]} />
|
||||||
|
<arrowHelper args={[new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 0, 0), 0.7, 0x4488ff, 0.18, 0.12]} />
|
||||||
|
</group>
|
||||||
|
<Text position={[0, 0.065, -0.04]} fontSize={0.008} color="#cbd5e1" anchorX="center" anchorY="middle">
|
||||||
|
Cubo de Calibração
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { useRef, useMemo, useEffect, useState } from 'react';
|
||||||
|
import { useFrame, ThreeEvent } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { pushXRCubeFace } from './xrCalibrationBus';
|
||||||
|
import { getModelLocalGroup } from '@/lib/modelTransforms';
|
||||||
|
|
||||||
|
function makeFaceTexture(label: string, accent: boolean): THREE.CanvasTexture {
|
||||||
|
const size = 256;
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = size;
|
||||||
|
c.height = size;
|
||||||
|
const ctx = c.getContext('2d')!;
|
||||||
|
const grad = ctx.createLinearGradient(0, 0, 0, size);
|
||||||
|
grad.addColorStop(0, accent ? '#1e3a8a' : '#111827');
|
||||||
|
grad.addColorStop(1, accent ? '#0f172a' : '#030712');
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
ctx.strokeStyle = '#3b82f6';
|
||||||
|
ctx.lineWidth = 8;
|
||||||
|
ctx.strokeRect(4, 4, size - 8, size - 8);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = '700 52px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.shadowColor = '#3b82f6';
|
||||||
|
ctx.shadowBlur = 10;
|
||||||
|
ctx.fillText(label, size / 2, size / 2);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.anisotropy = 4;
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FACE_DEFS: { label: string; dir: [number, number, number] }[] = [
|
||||||
|
{ label: 'DIR', dir: [ 1, 0, 0] },
|
||||||
|
{ label: 'ESQ', dir: [-1, 0, 0] },
|
||||||
|
{ label: 'TOPO', dir: [ 0, 1, 0] },
|
||||||
|
{ label: 'BASE', dir: [ 0,-1, 0] },
|
||||||
|
{ label: 'FRENTE', dir: [ 0, 0, 1] },
|
||||||
|
{ label: 'ATRÁS', dir: [ 0, 0,-1] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function XRViewCube({ activeModelId, size = 0.06 }: { activeModelId: string | null; size?: number }) {
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const [hover, setHover] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const materials = useMemo(() => {
|
||||||
|
return FACE_DEFS.map((f, i) => {
|
||||||
|
const tex = makeFaceTexture(f.label, false);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ map: tex });
|
||||||
|
mat.userData.baseTex = tex;
|
||||||
|
mat.userData.hoverTex = makeFaceTexture(f.label, true);
|
||||||
|
mat.userData.faceIndex = i;
|
||||||
|
return mat;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
materials.forEach((m) => {
|
||||||
|
m.userData.baseTex?.dispose?.();
|
||||||
|
m.userData.hoverTex?.dispose?.();
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [materials]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
materials.forEach((m, i) => {
|
||||||
|
m.map = i === hover ? m.userData.hoverTex : m.userData.baseTex;
|
||||||
|
m.needsUpdate = true;
|
||||||
|
});
|
||||||
|
}, [hover, materials]);
|
||||||
|
|
||||||
|
const modelQuat = useMemo(() => new THREE.Quaternion(), []);
|
||||||
|
|
||||||
|
const getModelWorldQuat = (): THREE.Quaternion => {
|
||||||
|
const g = getModelLocalGroup(activeModelId);
|
||||||
|
if (g) {
|
||||||
|
g.updateWorldMatrix(true, false);
|
||||||
|
g.getWorldQuaternion(modelQuat);
|
||||||
|
return modelQuat;
|
||||||
|
}
|
||||||
|
modelQuat.identity();
|
||||||
|
return modelQuat;
|
||||||
|
};
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (groupRef.current) {
|
||||||
|
// Sincroniza a rotação do cubo no HUD AR com a rotação do modelo ativo no mundo AR.
|
||||||
|
// Desta forma, o cubo reflete a orientação 3D da peça.
|
||||||
|
groupRef.current.quaternion.copy(getModelWorldQuat());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClick = (e: ThreeEvent<MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = e.face?.materialIndex;
|
||||||
|
if (idx == null) return;
|
||||||
|
const def = FACE_DEFS[idx];
|
||||||
|
if (!def) return;
|
||||||
|
const dirWorld = new THREE.Vector3(...def.dir).applyQuaternion(getModelWorldQuat()).normalize();
|
||||||
|
pushXRCubeFace(dirWorld);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
<mesh
|
||||||
|
material={materials}
|
||||||
|
onClick={onClick}
|
||||||
|
onPointerOver={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = e.face?.materialIndex ?? null;
|
||||||
|
setHover(idx);
|
||||||
|
}}
|
||||||
|
onPointerOut={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setHover(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[size, size, size]} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
|
||||||
|
export type XRCalibrationStep =
|
||||||
|
| 'idle'
|
||||||
|
| 'await-cube-1'
|
||||||
|
| 'await-model-1'
|
||||||
|
| 'await-cube-2'
|
||||||
|
| 'await-model-2'
|
||||||
|
| 'await-cube-3' // opcional verificação
|
||||||
|
| 'await-model-3'
|
||||||
|
| 'done';
|
||||||
|
|
||||||
|
interface XRPair {
|
||||||
|
cube: THREE.Vector3;
|
||||||
|
model: THREE.Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface XRCalState {
|
||||||
|
step: XRCalibrationStep;
|
||||||
|
modelId: string | null;
|
||||||
|
pairs: XRPair[];
|
||||||
|
pendingCube: THREE.Vector3 | null;
|
||||||
|
progress: number;
|
||||||
|
verifyErrorDeg: number;
|
||||||
|
listeners: Set<() => void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const xrCalibration: XRCalState = {
|
||||||
|
step: 'idle',
|
||||||
|
modelId: null,
|
||||||
|
pairs: [],
|
||||||
|
pendingCube: null,
|
||||||
|
progress: 0,
|
||||||
|
verifyErrorDeg: NaN,
|
||||||
|
listeners: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function notify() {
|
||||||
|
xrCalibration.listeners.forEach((fn) => {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeXRCalibration(fn: () => void): () => void {
|
||||||
|
xrCalibration.listeners.add(fn);
|
||||||
|
return () => {
|
||||||
|
xrCalibration.listeners.delete(fn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startXRCalibration(modelId: string) {
|
||||||
|
xrCalibration.step = 'await-cube-1';
|
||||||
|
xrCalibration.modelId = modelId;
|
||||||
|
xrCalibration.pairs = [];
|
||||||
|
xrCalibration.pendingCube = null;
|
||||||
|
xrCalibration.progress = 0;
|
||||||
|
xrCalibration.verifyErrorDeg = NaN;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelXRCalibration() {
|
||||||
|
xrCalibration.step = 'idle';
|
||||||
|
xrCalibration.modelId = null;
|
||||||
|
xrCalibration.pairs = [];
|
||||||
|
xrCalibration.pendingCube = null;
|
||||||
|
xrCalibration.progress = 0;
|
||||||
|
xrCalibration.verifyErrorDeg = NaN;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushXRCubeFace(dirWorld: THREE.Vector3) {
|
||||||
|
if (xrCalibration.step === 'await-cube-1') {
|
||||||
|
xrCalibration.pendingCube = dirWorld.clone().normalize();
|
||||||
|
xrCalibration.step = 'await-model-1';
|
||||||
|
} else if (xrCalibration.step === 'await-cube-2') {
|
||||||
|
xrCalibration.pendingCube = dirWorld.clone().normalize();
|
||||||
|
xrCalibration.step = 'await-model-2';
|
||||||
|
} else if (xrCalibration.step === 'await-cube-3') {
|
||||||
|
xrCalibration.pendingCube = dirWorld.clone().normalize();
|
||||||
|
xrCalibration.step = 'await-model-3';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapToPrincipalAxis(v: THREE.Vector3, maxDeg = 12): THREE.Vector3 {
|
||||||
|
const cosT = Math.cos(THREE.MathUtils.degToRad(maxDeg));
|
||||||
|
const axes = [
|
||||||
|
new THREE.Vector3(1, 0, 0),
|
||||||
|
new THREE.Vector3(-1, 0, 0),
|
||||||
|
new THREE.Vector3(0, 1, 0),
|
||||||
|
new THREE.Vector3(0, -1, 0),
|
||||||
|
new THREE.Vector3(0, 0, 1),
|
||||||
|
new THREE.Vector3(0, 0, -1),
|
||||||
|
];
|
||||||
|
let best = v;
|
||||||
|
let bestDot = cosT;
|
||||||
|
for (const a of axes) {
|
||||||
|
const d = v.dot(a);
|
||||||
|
if (d > bestDot) {
|
||||||
|
bestDot = d;
|
||||||
|
best = a.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushXRModelFaceNormal(normalWorld: THREE.Vector3, calGroupWorldQuat: THREE.Quaternion) {
|
||||||
|
if (!xrCalibration.pendingCube) return;
|
||||||
|
const cubeWorld = xrCalibration.pendingCube;
|
||||||
|
const inv = calGroupWorldQuat.clone().invert();
|
||||||
|
const cubeLocal = cubeWorld.clone().applyQuaternion(inv).normalize();
|
||||||
|
const modelLocalRaw = normalWorld.clone().applyQuaternion(inv).normalize();
|
||||||
|
const modelLocal = snapToPrincipalAxis(modelLocalRaw, 12);
|
||||||
|
xrCalibration.pairs.push({ cube: cubeLocal, model: modelLocal });
|
||||||
|
xrCalibration.pendingCube = null;
|
||||||
|
|
||||||
|
if (xrCalibration.step === 'await-model-1') {
|
||||||
|
xrCalibration.step = 'await-cube-2';
|
||||||
|
xrCalibration.progress = 0.33;
|
||||||
|
} else if (xrCalibration.step === 'await-model-2') {
|
||||||
|
xrCalibration.progress = 0.75;
|
||||||
|
xrCalibration.step = 'await-cube-3';
|
||||||
|
} else if (xrCalibration.step === 'await-model-3') {
|
||||||
|
xrCalibration.progress = 1;
|
||||||
|
xrCalibration.verifyErrorDeg = computeXRVerifyError(xrCalibration.pairs);
|
||||||
|
xrCalibration.step = 'done';
|
||||||
|
}
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeXRCalibrationQuaternion(pairs: XRPair[]): THREE.Quaternion | null {
|
||||||
|
if (pairs.length < 2) return null;
|
||||||
|
const n1 = pairs[0].model.clone().normalize();
|
||||||
|
const n2raw = pairs[1].model.clone();
|
||||||
|
let n2 = n2raw.sub(n1.clone().multiplyScalar(n2raw.dot(n1)));
|
||||||
|
if (n2.lengthSq() < 1e-6) return null;
|
||||||
|
n2.normalize();
|
||||||
|
const n3 = new THREE.Vector3().crossVectors(n1, n2);
|
||||||
|
|
||||||
|
const c1 = pairs[0].cube.clone().normalize();
|
||||||
|
const c2raw = pairs[1].cube.clone();
|
||||||
|
let c2 = c2raw.sub(c1.clone().multiplyScalar(c2raw.dot(c1)));
|
||||||
|
if (c2.lengthSq() < 1e-6) return null;
|
||||||
|
c2.normalize();
|
||||||
|
const c3 = new THREE.Vector3().crossVectors(c1, c2);
|
||||||
|
|
||||||
|
const N = new THREE.Matrix4().makeBasis(n1, n2, n3);
|
||||||
|
const C = new THREE.Matrix4().makeBasis(c1, c2, c3);
|
||||||
|
const Nt = N.clone().transpose();
|
||||||
|
const R = new THREE.Matrix4().multiplyMatrices(C, Nt);
|
||||||
|
const q = new THREE.Quaternion().setFromRotationMatrix(R);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeXRVerifyError(pairs: XRPair[]): number {
|
||||||
|
if (pairs.length < 3) return NaN;
|
||||||
|
const q = computeXRCalibrationQuaternion(pairs.slice(0, 2));
|
||||||
|
if (!q) return NaN;
|
||||||
|
const predicted = pairs[2].model.clone().applyQuaternion(q).normalize();
|
||||||
|
const target = pairs[2].cube.clone().normalize();
|
||||||
|
const cos = THREE.MathUtils.clamp(predicted.dot(target), -1, 1);
|
||||||
|
return THREE.MathUtils.radToDeg(Math.acos(cos));
|
||||||
|
}
|
||||||
+68
-15
@@ -26,6 +26,12 @@ import { VisibilityApplier } from '@/components/three/ModelViewer';
|
|||||||
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
|
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
|
||||||
import { parseIFCtoThree } from '@/lib/convertIFC';
|
import { parseIFCtoThree } from '@/lib/convertIFC';
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
import {
|
||||||
|
xrCalibration,
|
||||||
|
pushXRModelFaceNormal,
|
||||||
|
computeXRCalibrationQuaternion,
|
||||||
|
subscribeXRCalibration,
|
||||||
|
} from '@/components/three/xrCalibrationBus';
|
||||||
|
|
||||||
// --- Diagnóstico XR ---
|
// --- Diagnóstico XR ---
|
||||||
console.log('[XR] Inicializando store...');
|
console.log('[XR] Inicializando store...');
|
||||||
@@ -100,8 +106,15 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
}, [sceneModel.url, sceneModel.fileName]);
|
}, [sceneModel.url, sceneModel.fileName]);
|
||||||
|
|
||||||
const ref = useRef<THREE.Group>(null);
|
const ref = useRef<THREE.Group>(null);
|
||||||
|
const calGroupRef = useRef<THREE.Group>(null);
|
||||||
|
const localFrameRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
|
// Escuta mudanças de calibração para re-renderizar no AR
|
||||||
|
const [, setCalTick] = useState(0);
|
||||||
|
useEffect(() => subscribeXRCalibration(() => setCalTick(t => t + 1)), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const g = ref.current;
|
const g = localFrameRef.current;
|
||||||
if (!g) return;
|
if (!g) return;
|
||||||
registerModelLocalGroup(sceneModel.id, g);
|
registerModelLocalGroup(sceneModel.id, g);
|
||||||
return () => unregisterModelLocalGroup(sceneModel.id, g);
|
return () => unregisterModelLocalGroup(sceneModel.id, g);
|
||||||
@@ -201,24 +214,64 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
const scaleRatio = useModelStore((st) => st.scaleRatio);
|
const scaleRatio = useModelStore((st) => st.scaleRatio);
|
||||||
const renderFactor = scaleRatio?.factor ?? 1;
|
const renderFactor = scaleRatio?.factor ?? 1;
|
||||||
|
|
||||||
|
const calQuatArr = sceneModel.calibrationQuat;
|
||||||
|
const isCalibratingThis = xrCalibration.modelId === sceneModel.id && xrCalibration.step !== 'idle' && xrCalibration.step !== 'done';
|
||||||
|
const calQuat = useMemo(() => {
|
||||||
|
if (isCalibratingThis) return new THREE.Quaternion();
|
||||||
|
if (!calQuatArr) return new THREE.Quaternion();
|
||||||
|
return new THREE.Quaternion(calQuatArr[0], calQuatArr[1], calQuatArr[2], calQuatArr[3]);
|
||||||
|
}, [calQuatArr, isCalibratingThis]);
|
||||||
|
|
||||||
if (!sceneModel.visible) return null;
|
if (!sceneModel.visible) return null;
|
||||||
if (!scene) return null;
|
if (!scene) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group scale={[renderFactor, renderFactor, renderFactor]}>
|
<group
|
||||||
<group
|
scale={[renderFactor, renderFactor, renderFactor]}
|
||||||
ref={ref}
|
onClick={(e) => {
|
||||||
userData={{ modelId: sceneModel.id }}
|
e.stopPropagation();
|
||||||
position={[
|
|
||||||
-modelInfo.center.x + fineTuning.posX,
|
// Calibração XR: captura normal da face do modelo em espaço de mundo.
|
||||||
-modelInfo.center.y + fineTuning.posY,
|
if (
|
||||||
-modelInfo.center.z + fineTuning.posZ,
|
xrCalibration.modelId === sceneModel.id &&
|
||||||
]}
|
(xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') &&
|
||||||
rotation={[rotXRad, rotYRad, rotZRad]}
|
e.face && e.object
|
||||||
scale={[s, s, s]}
|
) {
|
||||||
>
|
const n = e.face.normal.clone();
|
||||||
<primitive object={scene} />
|
const nm = new THREE.Matrix3().getNormalMatrix(e.object.matrixWorld);
|
||||||
<XRLocalModelMeasurements modelId={sceneModel.id} />
|
n.applyMatrix3(nm).normalize();
|
||||||
|
const wq = new THREE.Quaternion();
|
||||||
|
(calGroupRef.current ?? e.object).getWorldQuaternion(wq);
|
||||||
|
pushXRModelFaceNormal(n, wq);
|
||||||
|
|
||||||
|
// Aplica/atualiza calibração se tivermos >= 2 pares
|
||||||
|
if (xrCalibration.pairs.length >= 2) {
|
||||||
|
const q = computeXRCalibrationQuaternion(xrCalibration.pairs);
|
||||||
|
if (q) {
|
||||||
|
useModelStore.getState().setCalibration(sceneModel.id, [q.x, q.y, q.z, q.w]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Translação */}
|
||||||
|
<group position={[fineTuning.posX, fineTuning.posY, fineTuning.posZ]}>
|
||||||
|
{/* Rotação e escala em volta do centro geométrico */}
|
||||||
|
<group
|
||||||
|
ref={ref}
|
||||||
|
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||||
|
scale={[s, s, s]}
|
||||||
|
>
|
||||||
|
{/* Rotação de calibração */}
|
||||||
|
<group ref={calGroupRef} quaternion={calQuat}>
|
||||||
|
{/* Sistema local do modelo com pivot no centro */}
|
||||||
|
<group ref={localFrameRef} position={[-modelInfo.center.x, -modelInfo.center.y, -modelInfo.center.z]} userData={{ modelId: sceneModel.id }}>
|
||||||
|
<primitive object={scene} />
|
||||||
|
<XRLocalModelMeasurements modelId={sceneModel.id} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user