🚀 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,
|
detectCircularEdgeAtPoint3D,
|
||||||
findNearestEdgeSegment3D
|
findNearestEdgeSegment3D
|
||||||
} from './SmartMeasure';
|
} from './SmartMeasure';
|
||||||
|
import { xrCalibration, pushXRRealPoint } from './xrCalibrationBus';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const TRIG_ON = 0.7;
|
const TRIG_ON = 0.7;
|
||||||
const TRIG_OFF = 0.3;
|
const TRIG_OFF = 0.3;
|
||||||
@@ -80,12 +82,31 @@ export function XRControllerMeasure() {
|
|||||||
// running them every frame at 72fps can stall the Quest right after
|
// running them every frame at 72fps can stall the Quest right after
|
||||||
// toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz.
|
// toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz.
|
||||||
// Trigger/A/B/L-trigger polling continues every frame for responsiveness.
|
// 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) => {
|
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
||||||
const measureMode = useModelStore.getState().measureMode;
|
const measureMode = useModelStore.getState().measureMode;
|
||||||
const selectionMode = useModelStore.getState().selectionMode;
|
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 (laserRef.current) laserRef.current.visible = false;
|
||||||
if (tipRef.current) tipRef.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 session = frame.session;
|
||||||
const refSpace = gl.xr.getReferenceSpace();
|
const refSpace = gl.xr.getReferenceSpace();
|
||||||
@@ -126,6 +147,43 @@ export function XRControllerMeasure() {
|
|||||||
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
||||||
raycaster.current.far = MAX_RAY;
|
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 snappedPoint: THREE.Vector3 | null = null;
|
||||||
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
|
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;
|
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 ───────────────────────────────────────────
|
// ── Update laser visual ───────────────────────────────────────────
|
||||||
if (laserRef.current && tipRef.current) {
|
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;
|
const positions = laserGeom.current.attributes.position as THREE.BufferAttribute;
|
||||||
positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z);
|
positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z);
|
||||||
positions.setXYZ(1, end.x, end.y, end.z);
|
positions.setXYZ(1, end.x, end.y, end.z);
|
||||||
positions.needsUpdate = true;
|
positions.needsUpdate = true;
|
||||||
laserGeom.current.computeBoundingSphere();
|
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);
|
tipColor.current.set(color);
|
||||||
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
||||||
((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current);
|
((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current);
|
||||||
|
|
||||||
laserRef.current.visible = true;
|
laserRef.current.visible = true;
|
||||||
if (snappedPoint) {
|
|
||||||
|
const tipPos = isRealStep ? realHitPoint : snappedPoint;
|
||||||
|
if (tipPos) {
|
||||||
tipRef.current.visible = true;
|
tipRef.current.visible = true;
|
||||||
tipRef.current.position.copy(snappedPoint);
|
tipRef.current.position.copy(tipPos);
|
||||||
// Tip scales with distance from controller. Snap "forte" (vértice,
|
const dist = tmpOrigin.current.distanceTo(tipPos);
|
||||||
// aresta, furo) recebe um marcador ~3× maior do que uma superfície
|
const strongSnap = isAligning || snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
||||||
// 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';
|
|
||||||
const factor = strongSnap ? 0.030 : 0.012;
|
const factor = strongSnap ? 0.030 : 0.012;
|
||||||
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
||||||
tipRef.current.scale.setScalar(s);
|
tipRef.current.scale.setScalar(s);
|
||||||
@@ -274,6 +337,21 @@ export function XRControllerMeasure() {
|
|||||||
if (!trigState.current && trigVal > TRIG_ON) {
|
if (!trigState.current && trigVal > TRIG_ON) {
|
||||||
trigState.current = true;
|
trigState.current = true;
|
||||||
const st = useModelStore.getState();
|
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) {
|
if (st.selectionMode) {
|
||||||
// Fresh raycast on the press itself (not every frame) for selection
|
// Fresh raycast on the press itself (not every frame) for selection
|
||||||
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
||||||
|
|||||||
@@ -904,10 +904,16 @@ function CalibrationTab() {
|
|||||||
|
|
||||||
const isCalibrating = xrCalibration.step !== 'idle' && xrCalibration.step !== 'done';
|
const isCalibrating = xrCalibration.step !== 'idle' && xrCalibration.step !== 'done';
|
||||||
const isDone = xrCalibration.step === 'done';
|
const isDone = xrCalibration.step === 'done';
|
||||||
const progress = isDone ? 1 : xrCalibration.progress;
|
|
||||||
|
const isCubeMode = xrCalibration.alignType === 'cube';
|
||||||
|
const isVirtRealMode = xrCalibration.alignType === 'virt-real';
|
||||||
|
|
||||||
const onClickCalibrar = () => {
|
const onClickCube = () => {
|
||||||
if (isCalibrating || isDone) {
|
if (isCalibrating && isCubeMode) {
|
||||||
|
cancelXRCalibration();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDone && isCubeMode) {
|
||||||
cancelXRCalibration();
|
cancelXRCalibration();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -919,7 +925,27 @@ function CalibrationTab() {
|
|||||||
toast.error('Peça travada — destranque a peça para calibrar');
|
toast.error('Peça travada — destranque a peça para calibrar');
|
||||||
return;
|
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 = () => {
|
const onResetCalibration = () => {
|
||||||
@@ -935,10 +961,54 @@ function CalibrationTab() {
|
|||||||
'await-model-2': '2/3 - Clique na face correspondente da peça',
|
'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-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-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!',
|
'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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -948,7 +1018,7 @@ function CalibrationTab() {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text position={[-0.24, 0.07, 0]} fontSize={0.008} color="#cbd5e1" anchorX="left" anchorY="top" maxWidth={0.28}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
{/* Caixa de status do passo atual */}
|
{/* Caixa de status do passo atual */}
|
||||||
@@ -957,10 +1027,10 @@ function CalibrationTab() {
|
|||||||
<planeGeometry args={[0.26, 0.05]} />
|
<planeGeometry args={[0.26, 0.05]} />
|
||||||
<meshBasicMaterial color="#111827" transparent opacity={0.65} />
|
<meshBasicMaterial color="#111827" transparent opacity={0.65} />
|
||||||
</mesh>
|
</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}
|
{currentHint}
|
||||||
</Text>
|
</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">
|
<Text position={[0.01, -0.016, 0.001]} fontSize={0.007} color="#22c55e" anchorX="left" anchorY="middle">
|
||||||
Erro residual calculado: {xrCalibration.verifyErrorDeg.toFixed(1)}°
|
Erro residual calculado: {xrCalibration.verifyErrorDeg.toFixed(1)}°
|
||||||
</Text>
|
</Text>
|
||||||
@@ -970,20 +1040,30 @@ function CalibrationTab() {
|
|||||||
{/* Botões na parte inferior esquerda */}
|
{/* Botões na parte inferior esquerda */}
|
||||||
<group position={[-0.24, -0.06, 0]}>
|
<group position={[-0.24, -0.06, 0]}>
|
||||||
<XR3DButton
|
<XR3DButton
|
||||||
position={[0.06, 0, 0]}
|
position={[0.045, 0, 0]}
|
||||||
size={[0.11, 0.026]}
|
size={[0.08, 0.026]}
|
||||||
label={isDone ? 'Concluído' : isCalibrating ? 'Cancelar' : 'Iniciar Calibrar'}
|
label={cubeLabel}
|
||||||
active={isCalibrating || isDone}
|
active={cubeActive}
|
||||||
color={isDone ? '#22c55e' : isCalibrating ? '#d97706' : '#3b82f6'}
|
color={cubeColor}
|
||||||
onClick={onClickCalibrar}
|
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}
|
fontSize={0.008}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{active?.calibrationQuat && !isCalibrating && (
|
{active?.calibrationQuat && !isCalibrating && (
|
||||||
<XR3DButton
|
<XR3DButton
|
||||||
position={[0.18, 0, 0]}
|
position={[0.225, 0, 0]}
|
||||||
size={[0.09, 0.026]}
|
size={[0.08, 0.026]}
|
||||||
label="Remover Cal."
|
label="Limpar"
|
||||||
color="#dc2626"
|
color="#dc2626"
|
||||||
onClick={onResetCalibration}
|
onClick={onResetCalibration}
|
||||||
fontSize={0.008}
|
fontSize={0.008}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ export type XRCalibrationStep =
|
|||||||
| 'await-model-2'
|
| 'await-model-2'
|
||||||
| 'await-cube-3' // opcional verificação
|
| 'await-cube-3' // opcional verificação
|
||||||
| 'await-model-3'
|
| 'await-model-3'
|
||||||
|
| 'await-real-1'
|
||||||
|
| 'await-virtual-1'
|
||||||
|
| 'await-real-2'
|
||||||
|
| 'await-virtual-2'
|
||||||
|
| 'await-real-3'
|
||||||
|
| 'await-virtual-3'
|
||||||
| 'done';
|
| 'done';
|
||||||
|
|
||||||
interface XRPair {
|
interface XRPair {
|
||||||
@@ -19,7 +25,10 @@ interface XRPair {
|
|||||||
interface XRCalState {
|
interface XRCalState {
|
||||||
step: XRCalibrationStep;
|
step: XRCalibrationStep;
|
||||||
modelId: string | null;
|
modelId: string | null;
|
||||||
|
alignType: 'cube' | 'virt-real' | null;
|
||||||
pairs: XRPair[];
|
pairs: XRPair[];
|
||||||
|
realPoints: THREE.Vector3[];
|
||||||
|
virtualPoints: THREE.Vector3[];
|
||||||
pendingCube: THREE.Vector3 | null;
|
pendingCube: THREE.Vector3 | null;
|
||||||
progress: number;
|
progress: number;
|
||||||
verifyErrorDeg: number;
|
verifyErrorDeg: number;
|
||||||
@@ -29,7 +38,10 @@ interface XRCalState {
|
|||||||
export const xrCalibration: XRCalState = {
|
export const xrCalibration: XRCalState = {
|
||||||
step: 'idle',
|
step: 'idle',
|
||||||
modelId: null,
|
modelId: null,
|
||||||
|
alignType: null,
|
||||||
pairs: [],
|
pairs: [],
|
||||||
|
realPoints: [],
|
||||||
|
virtualPoints: [],
|
||||||
pendingCube: null,
|
pendingCube: null,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
verifyErrorDeg: NaN,
|
verifyErrorDeg: NaN,
|
||||||
@@ -47,20 +59,31 @@ export function subscribeXRCalibration(fn: () => void): () => void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startXRCalibration(modelId: string) {
|
export function startXRCalibration(modelId: string, type: 'cube' | 'virt-real' = 'cube') {
|
||||||
xrCalibration.step = 'await-cube-1';
|
|
||||||
xrCalibration.modelId = modelId;
|
xrCalibration.modelId = modelId;
|
||||||
|
xrCalibration.alignType = type;
|
||||||
xrCalibration.pairs = [];
|
xrCalibration.pairs = [];
|
||||||
|
xrCalibration.realPoints = [];
|
||||||
|
xrCalibration.virtualPoints = [];
|
||||||
xrCalibration.pendingCube = null;
|
xrCalibration.pendingCube = null;
|
||||||
xrCalibration.progress = 0;
|
xrCalibration.progress = 0;
|
||||||
xrCalibration.verifyErrorDeg = NaN;
|
xrCalibration.verifyErrorDeg = NaN;
|
||||||
|
|
||||||
|
if (type === 'cube') {
|
||||||
|
xrCalibration.step = 'await-cube-1';
|
||||||
|
} else {
|
||||||
|
xrCalibration.step = 'await-real-1';
|
||||||
|
}
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelXRCalibration() {
|
export function cancelXRCalibration() {
|
||||||
xrCalibration.step = 'idle';
|
xrCalibration.step = 'idle';
|
||||||
xrCalibration.modelId = null;
|
xrCalibration.modelId = null;
|
||||||
|
xrCalibration.alignType = null;
|
||||||
xrCalibration.pairs = [];
|
xrCalibration.pairs = [];
|
||||||
|
xrCalibration.realPoints = [];
|
||||||
|
xrCalibration.virtualPoints = [];
|
||||||
xrCalibration.pendingCube = null;
|
xrCalibration.pendingCube = null;
|
||||||
xrCalibration.progress = 0;
|
xrCalibration.progress = 0;
|
||||||
xrCalibration.verifyErrorDeg = NaN;
|
xrCalibration.verifyErrorDeg = NaN;
|
||||||
@@ -83,6 +106,44 @@ export function pushXRCubeFace(dirWorld: THREE.Vector3) {
|
|||||||
notify();
|
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 {
|
function snapToPrincipalAxis(v: THREE.Vector3, maxDeg = 12): THREE.Vector3 {
|
||||||
const cosT = Math.cos(THREE.MathUtils.degToRad(maxDeg));
|
const cosT = Math.cos(THREE.MathUtils.degToRad(maxDeg));
|
||||||
const axes = [
|
const axes = [
|
||||||
@@ -162,3 +223,50 @@ export function computeXRVerifyError(pairs: XRPair[]): number {
|
|||||||
const cos = THREE.MathUtils.clamp(predicted.dot(target), -1, 1);
|
const cos = THREE.MathUtils.clamp(predicted.dot(target), -1, 1);
|
||||||
return THREE.MathUtils.radToDeg(Math.acos(cos));
|
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,
|
pushXRModelFaceNormal,
|
||||||
computeXRCalibrationQuaternion,
|
computeXRCalibrationQuaternion,
|
||||||
subscribeXRCalibration,
|
subscribeXRCalibration,
|
||||||
|
pushXRRealPoint,
|
||||||
|
pushXRVirtualPoint,
|
||||||
|
computeVirtRealTransform,
|
||||||
} from '@/components/three/xrCalibrationBus';
|
} from '@/components/three/xrCalibrationBus';
|
||||||
|
|
||||||
// --- Diagnóstico XR ---
|
// --- Diagnóstico XR ---
|
||||||
@@ -110,6 +113,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
const ref = useRef<THREE.Group>(null);
|
const ref = useRef<THREE.Group>(null);
|
||||||
const calGroupRef = useRef<THREE.Group>(null);
|
const calGroupRef = useRef<THREE.Group>(null);
|
||||||
const localFrameRef = 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
|
// Escuta mudanças de calibração para re-renderizar no AR
|
||||||
const [, setCalTick] = useState(0);
|
const [, setCalTick] = useState(0);
|
||||||
@@ -229,6 +233,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
|
ref={topGroupRef}
|
||||||
scale={[renderFactor, renderFactor, renderFactor]}
|
scale={[renderFactor, renderFactor, renderFactor]}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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.
|
// Calibração XR da Peça: captura normal da face do modelo em espaço de mundo.
|
||||||
if (
|
if (
|
||||||
xrCalibration.modelId === sceneModel.id &&
|
xrCalibration.modelId === sceneModel.id &&
|
||||||
|
xrCalibration.alignType === 'cube' &&
|
||||||
(xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') &&
|
(xrCalibration.step === 'await-model-1' || xrCalibration.step === 'await-model-2' || xrCalibration.step === 'await-model-3') &&
|
||||||
e.face && e.object
|
e.face && e.object
|
||||||
) {
|
) {
|
||||||
@@ -267,6 +273,48 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
}
|
}
|
||||||
return;
|
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 */}
|
{/* Translação */}
|
||||||
@@ -760,6 +808,53 @@ function XRSnapHandler() {
|
|||||||
return null;
|
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 ───────────────────────────────────────────
|
// ─── XR Grid ───────────────────────────────────────────
|
||||||
function XRGrid() {
|
function XRGrid() {
|
||||||
const showGrid = useModelStore((s) => s.showGrid);
|
const showGrid = useModelStore((s) => s.showGrid);
|
||||||
@@ -1025,6 +1120,7 @@ const XRSession = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<XRSnapHandler />
|
<XRSnapHandler />
|
||||||
|
<XRVirtRealDesktopAligner />
|
||||||
<XRGrid />
|
<XRGrid />
|
||||||
<XRGridAutoFollower />
|
<XRGridAutoFollower />
|
||||||
</XR>
|
</XR>
|
||||||
|
|||||||
Reference in New Issue
Block a user