From 0f017d0e4b1bfb0ab54ad289ca3161a997a8faf2 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Fri, 29 May 2026 20:32:41 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20melhoria=20no=20s?= =?UTF-8?q?nap=20e=20medi=C3=A7=C3=A3o=20AR=20em=2029/05/2026=2020:32:41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/three/XRControllerMeasure.tsx | 98 ++++++++++++++-- src/components/three/XRHudInWorld.tsx | 114 ++++++++++++++++--- src/components/three/xrCalibrationBus.ts | 112 +++++++++++++++++- src/pages/XRSession.tsx | 96 ++++++++++++++++ 4 files changed, 391 insertions(+), 29 deletions(-) diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index ffb0dff..e1a9953 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -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(null); + const hitTestRequestedRef = useRef(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); diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 3e86107..ce0799b 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -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 isCubeMode = xrCalibration.alignType === 'cube'; + const isVirtRealMode = xrCalibration.alignType === 'virt-real'; - const onClickCalibrar = () => { - if (isCalibrating || isDone) { + 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 ( @@ -948,7 +1018,7 @@ function CalibrationTab() { - 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"). {/* Caixa de status do passo atual */} @@ -957,10 +1027,10 @@ function CalibrationTab() { - + {currentHint} - {isDone && Number.isFinite(xrCalibration.verifyErrorDeg) && ( + {isDone && Number.isFinite(xrCalibration.verifyErrorDeg) && isCubeMode && ( Erro residual calculado: {xrCalibration.verifyErrorDeg.toFixed(1)}° @@ -970,20 +1040,30 @@ function CalibrationTab() { {/* Botões na parte inferior esquerda */} + + {active?.calibrationQuat && !isCalibrating && ( 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 }; +} diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 592d3a4..ee60f6f 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -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(null); const calGroupRef = useRef(null); const localFrameRef = useRef(null); + const topGroupRef = useRef(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 ( { 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 = () => { )} +