From c85dc85936c4ae5e56d6edfe78d8dbed6ed23b7e Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Fri, 22 May 2026 21:20:32 +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=2022/05/2026=2021:20:32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 + src/components/three/ModelViewer.tsx | 16 +-- src/components/three/XRControllerMeasure.tsx | 116 ++++++++----------- src/lib/remoteLogger.ts | 40 +++++++ src/pages/Index.tsx | 60 ++++++++++ src/pages/XRSession.tsx | 26 ++++- 6 files changed, 181 insertions(+), 78 deletions(-) create mode 100644 src/lib/remoteLogger.ts diff --git a/src/App.tsx b/src/App.tsx index 7b72dc8..42fbbb6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Viewer from "./pages/Viewer"; import XRSession from "./pages/XRSession"; import Watch from "./pages/Watch"; import NotFound from "./pages/NotFound"; +import "@/lib/remoteLogger"; const queryClient = new QueryClient(); diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index a6fa571..b4e71c5 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -114,6 +114,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: const checklist = useModelStore((s) => s.checklist); const setActive = useModelStore((s) => s.setActiveModel); const selectionMode = useModelStore((s) => s.selectionMode); + const measureMode = useModelStore((s) => s.measureMode); const fineTuning = sceneModel.fineTuning; const modelInfo = useMemo(() => { @@ -160,12 +161,13 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: originalColors.current.set(mat, mat.color.clone()); } - if (renderMode === 'edges') { + if (renderMode === 'edges' && !measureMode) { mat.visible = false; } else { mat.visible = true; - mat.transparent = opacity < 1; - mat.opacity = opacity; + const targetOpacity = measureMode ? 0.25 : opacity; + mat.transparent = targetOpacity < 1; + mat.opacity = targetOpacity; mat.wireframe = renderMode === 'wireframe'; if (renderMode === 'wireframe') { mat.wireframeLinewidth = wireframeThickness; @@ -183,11 +185,11 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: } }); - if (renderMode === 'edges' && child.geometry) { + if ((renderMode === 'edges' || measureMode) && child.geometry) { const edgesGeo = new THREE.EdgesGeometry(child.geometry, edgeThresholdAngle); const lineMat = new THREE.LineBasicMaterial({ - color: wireframeColor, - linewidth: wireframeThickness, + color: measureMode ? '#00f3ff' : wireframeColor, + linewidth: measureMode ? 2 : wireframeThickness, }); const lineSegments = new THREE.LineSegments(edgesGeo, lineMat); lineSegments.userData.__edgeLine = true; @@ -195,7 +197,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: } } }); - }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color]); + }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode]); const rotXRad = (fineTuning.rotX * Math.PI) / 180; const rotYRad = (fineTuning.rotY * Math.PI) / 180; diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index fdb4801..49450c6 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -69,17 +69,12 @@ 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 heavyFrame = useRef(0); - const cachedSnappedPoint = useRef(null); - const cachedSnapKind = useRef<'vertex' | 'edge' | 'surface' | 'hole'>('surface'); - useFrame((_state, _dt, frame: XRFrame | undefined) => { const measureMode = useModelStore.getState().measureMode; const selectionMode = useModelStore.getState().selectionMode; if (laserRef.current) laserRef.current.visible = false; if (tipRef.current) tipRef.current.visible = false; if ((!measureMode && !selectionMode) || !frame) return; - const doHeavy = (heavyFrame.current++ % 3) === 0; const session = frame.session; const refSpace = gl.xr.getReferenceSpace(); @@ -124,70 +119,61 @@ export function XRControllerMeasure() { let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface'; let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null; - if (doHeavy) { - const hits = raycaster.current.intersectObjects(scene.children, true); - const hit = hits.find((h) => { - const o = h.object; - if (!isPickableModelMesh(o)) return false; - if (o.userData.__edgeLine) return false; - if (o.geometry instanceof THREE.SphereGeometry) return false; - if (o.geometry instanceof THREE.RingGeometry) return false; - if (o.geometry instanceof THREE.PlaneGeometry) return false; - return true; - }); + const hits = raycaster.current.intersectObjects(scene.children, true); + const hit = hits.find((h) => { + const o = h.object; + if (!isPickableModelMesh(o)) return false; + if (o.userData.__edgeLine) return false; + if (o.geometry instanceof THREE.SphereGeometry) return false; + if (o.geometry instanceof THREE.RingGeometry) return false; + if (o.geometry instanceof THREE.PlaneGeometry) return false; + return true; + }); - if (hit && hit.object instanceof THREE.Mesh) { - // Snap to existing registered hole centers first (within ~4cm) - const existing = useModelStore.getState().measurements; - let bestHoleCenter: THREE.Vector3 | null = null; - let bestHoleDist = Infinity; - for (const m of existing) { - if (m.kind !== 'hole') continue; - const c = new THREE.Vector3(m.pointA.x, m.pointA.y, m.pointA.z); - const d = hit.point.distanceTo(c); - if (d < 0.04 && d < bestHoleDist) { - bestHoleDist = d; - bestHoleCenter = c; - } - } + if (hit && hit.object instanceof THREE.Mesh) { + const dist = tmpOrigin.current.distanceTo(hit.point); + const vertexThreshold = Math.max(0.06, dist * 0.08); + const edgeThreshold = Math.max(0.08, dist * 0.10); - if (snapEnabled && bestHoleCenter) { - snappedPoint = bestHoleCenter; - snapKind = 'hole'; - } else if (snapEnabled) { - // Try detecting a circular edge (hole) at hit (8cm radius) - const circle = detectCircularEdgeAtPoint3D(hit.object, hit.point, 0.08); - if (circle) { - snappedPoint = circle.center; - snapKind = 'hole'; - hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center }; - } else { - // Snap 3D físico puro (7cm para vértices, 9cm para arestas) - const snap = resolveSnap3D(hit.object, hit.point, 0.07, 0.09); - snappedPoint = snap.point; - snapKind = snap.type; - if (snap.type === 'edge') { - const seg = findNearestEdgeSegment3D(hit.object, hit.point, 0.09); - if (seg) { - const lenMM = seg.a.distanceTo(seg.b) * 1000; - hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, endpoints: { a: seg.a, b: seg.b } }; - } - } - } - } else { - snappedPoint = hit.point.clone(); - snapKind = 'surface'; + // Snap to existing registered hole centers first + const existing = useModelStore.getState().measurements; + let bestHoleCenter: THREE.Vector3 | null = null; + let bestHoleDist = Infinity; + for (const m of existing) { + if (m.kind !== 'hole') continue; + const c = new THREE.Vector3(m.pointA.x, m.pointA.y, m.pointA.z); + const d = hit.point.distanceTo(c); + if (d < 0.05 && d < bestHoleDist) { + bestHoleDist = d; + bestHoleCenter = c; } } - // Cache for non-heavy frames so the laser + trigger still have a target. - // Sticky: se conseguimos um snap "forte" (vertex/edge/hole), preservamos - // ele entre frames leves para evitar cintilação. - cachedSnappedPoint.current = snappedPoint ? snappedPoint.clone() : null; - cachedSnapKind.current = snapKind; - } else { - // Light frame: reuse the last computed snap so visuals stay smooth - snappedPoint = cachedSnappedPoint.current ? cachedSnappedPoint.current.clone() : null; - snapKind = cachedSnapKind.current; + + if (snapEnabled && bestHoleCenter) { + snappedPoint = bestHoleCenter; + snapKind = 'hole'; + } else if (snapEnabled) { + const circle = detectCircularEdgeAtPoint3D(hit.object, hit.point, Math.max(0.08, dist * 0.12)); + if (circle) { + snappedPoint = circle.center; + snapKind = 'hole'; + hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center }; + } else { + const snap = resolveSnap3D(hit.object, hit.point, vertexThreshold, edgeThreshold); + snappedPoint = snap.point; + snapKind = snap.type; + if (snap.type === 'edge') { + const seg = findNearestEdgeSegment3D(hit.object, hit.point, edgeThreshold); + if (seg) { + const lenMM = seg.a.distanceTo(seg.b) * 1000; + hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, endpoints: { a: seg.a, b: seg.b } }; + } + } + } + } else { + snappedPoint = hit.point.clone(); + snapKind = 'surface'; + } } // ── Dwell detection (1 s) to auto-register hovered hole/edge ───── diff --git a/src/lib/remoteLogger.ts b/src/lib/remoteLogger.ts new file mode 100644 index 0000000..d7fea9c --- /dev/null +++ b/src/lib/remoteLogger.ts @@ -0,0 +1,40 @@ +import { supabase } from '@/integrations/supabase/client'; + +const logChannel = supabase.channel('global-app-logs', { + config: { broadcast: { self: true, ack: false } }, +}); + +logChannel.subscribe(); + +export function sendRemoteLog(level: 'info' | 'warn' | 'error', message: string, data?: any) { + console.log(`[RemoteLog] [${level}] ${message}`, data || ''); + logChannel.send({ + type: 'broadcast', + event: 'log', + payload: { + level, + message, + data: data ? JSON.parse(JSON.stringify(data)) : null, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + }, + }).catch(err => { + console.error('Falha ao enviar log remoto:', err); + }); +} + +// Captura global de erros em tempo de execução +if (typeof window !== 'undefined') { + window.addEventListener('error', (event) => { + sendRemoteLog('error', `Global error: ${event.message} at ${event.filename}:${event.lineno}`, { + colno: event.colno, + stack: event.error?.stack, + }); + }); + + window.addEventListener('unhandledrejection', (event) => { + sendRemoteLog('error', `Unhandled Promise rejection: ${event.reason}`, { + reason: event.reason?.stack || event.reason, + }); + }); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index df44d2b..d29c219 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -4,6 +4,7 @@ import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box, Package } from 'lu import { Button } from '@/components/ui/button'; import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; +import { supabase } from '@/integrations/supabase/client'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; import { validateModelFile } from '@/lib/validateModelFile'; @@ -23,6 +24,22 @@ const Index = () => { const [converting, setConverting] = useState(false); const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); + // --- Painel de Logs Remotos em Tempo Real --- + const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: any }[]>([]); + const [showLogs, setShowLogs] = useState(() => new URLSearchParams(window.location.search).get('viewlogs') === 'true'); + + useEffect(() => { + if (!showLogs) return; + const logChannel = supabase.channel('global-app-logs-viewer'); + logChannel.on('broadcast', { event: 'log' }, ({ payload }) => { + setDebugLogs(prev => [payload, ...prev].slice(0, 100)); + }).subscribe(); + + return () => { + logChannel.unsubscribe(); + }; + }, [showLogs]); + const [ifcConvert, setIfcConvert] = useState(() => { const saved = localStorage.getItem('tsxr_ifc_convert'); return saved !== 'false'; @@ -312,6 +329,49 @@ const Index = () => {

TrackSteelXR v1.04 — Q.C. Inspection

+ + {showLogs && ( +
+
+ + + Console de Logs Remotos (Quest 3) + +
+ + +
+
+
+ {debugLogs.length === 0 ? ( +

Aguardando logs do headset Quest 3...

+ ) : ( + debugLogs.map((log, i) => ( +
+
+ {new Date(log.timestamp).toLocaleTimeString()} + {log.level} +
+

{log.message}

+ {log.data && ( +
+                      {JSON.stringify(log.data, null, 2)}
+                    
+ )} +
+ )) + )} +
+
+ )} ); }; diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 7826d80..f15b985 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -23,6 +23,7 @@ import { useDevKit } from '@/devkit/useDevKit'; import { DevPanel } from '@/devkit/DevPanel'; import { FakeControllers } from '@/devkit/FakeControllers'; import { VisibilityApplier } from '@/components/three/ModelViewer'; +import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms'; import { parseIFCtoThree } from '@/lib/convertIFC'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; @@ -99,6 +100,13 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). }, [sceneModel.url, sceneModel.fileName]); const ref = useRef(null); + useEffect(() => { + const g = ref.current; + if (!g) return; + registerModelLocalGroup(sceneModel.id, g); + return () => unregisterModelLocalGroup(sceneModel.id, g); + }, [sceneModel.id]); + const fineTuning = sceneModel.fineTuning; const opacity = useModelStore((s) => s.opacity); const renderMode = useModelStore((s) => s.renderMode); @@ -106,6 +114,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). const wireframeThickness = useModelStore((s) => s.wireframeThickness); const edgeThresholdAngle = useModelStore((s) => s.edgeThresholdAngle); const checklist = useModelStore((s) => s.checklist); + const measureMode = useModelStore((s) => s.measureMode); const modelInfo = useMemo(() => { if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() }; @@ -145,12 +154,13 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach((mat: THREE.Material) => { if (mat instanceof THREE.MeshStandardMaterial) { - if (renderMode === 'edges') { + if (renderMode === 'edges' && !measureMode) { mat.visible = false; } else { mat.visible = true; - mat.transparent = opacity < 1; - mat.opacity = opacity; + const targetOpacity = measureMode ? 0.25 : opacity; + mat.transparent = targetOpacity < 1; + mat.opacity = targetOpacity; mat.wireframe = renderMode === 'wireframe'; if (renderMode === 'wireframe') { mat.wireframeLinewidth = wireframeThickness; @@ -167,16 +177,19 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). } }); - if (renderMode === 'edges' && child.geometry) { + if ((renderMode === 'edges' || measureMode) && child.geometry) { const edgesGeo = new THREE.EdgesGeometry(child.geometry, edgeThresholdAngle); - const lineMat = new THREE.LineBasicMaterial({ color: wireframeColor, linewidth: wireframeThickness }); + const lineMat = new THREE.LineBasicMaterial({ + color: measureMode ? '#00f3ff' : wireframeColor, + linewidth: measureMode ? 2 : wireframeThickness, + }); const lineSegments = new THREE.LineSegments(edgesGeo, lineMat); lineSegments.userData.__edgeLine = true; child.add(lineSegments); } } }); - }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color]); + }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode]); const rotXRad = (fineTuning.rotX * Math.PI) / 180; const rotYRad = (fineTuning.rotY * Math.PI) / 180; @@ -457,6 +470,7 @@ function XRSnapHandler() { // Click to measure useEffect(() => { if (!measureMode) return; + if (gl.xr.isPresenting) return; // Ignora cliques simulados do DOM quando o WebXR está ativo const onClick = (e: MouseEvent) => { if (snapPoint) { addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z });