import { useEffect, useRef, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box, Package } from 'lucide-react'; 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'; import { CloudLoader } from '@/components/CloudLoader'; import { SceneModelList } from '@/components/SceneModelList'; import { RecentFilesList } from '@/components/RecentFilesList'; import { addRecentFile } from '@/lib/recentFiles'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; const Index = () => { const navigate = useNavigate(); const fileInputRef = useRef(null); const [loadingDemoIFC, setLoadingDemoIFC] = useState(false); const [loadingDemoCotovelo, setLoadingDemoCotovelo] = useState(false); 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'; }); const handleToggleIfcConvert = (checked: boolean) => { setIfcConvert(checked); localStorage.setItem('tsxr_ifc_convert', checked ? 'true' : 'false'); }; useEffect(() => { if (navigator.xr) { navigator.xr.isSessionSupported('immersive-ar').then(setXrSupported).catch(() => setXrSupported(false)); } else { setXrSupported(false); } }, [setXrSupported]); const handleFileUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const ext = getSupportedExtension(file.name); if (!ext) { toast.error('Formato inválido. Selecione um arquivo .GLB, .OBJ, .STL ou .IFC'); return; } // Pré-validação rápida (tamanho) antes de ler o arquivo inteiro na memória const sizeCheck = validateModelFile(file.name, file.size, ext); if (!sizeCheck.ok) { toast.error(sizeCheck.reason!); return; } if (ext === 'glb') { // Para GLB, valida assinatura "glTF" antes de criar o objectURL try { const head = await file.slice(0, 16).arrayBuffer(); const sigCheck = validateModelFile(file.name, file.size, ext, head); if (!sigCheck.ok) { toast.error(sigCheck.reason!); if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); return; } } catch (err) { console.error('[upload] header read failed', err); } const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); addRecentFile(file.name, file).catch(() => {}); toast.success(`Modelo "${file.name}" carregado com sucesso!`); return; } // Convert OBJ/STL/IFC to GLB setConverting(true); try { const buffer = await file.arrayBuffer(); const sigCheck = validateModelFile(file.name, file.size, ext, buffer); if (!sigCheck.ok) { toast.error(sigCheck.reason!); if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); setConverting(false); return; } if (ext === 'ifc' && !ifcConvert) { const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); addRecentFile(file.name, file).catch(() => {}); toast.success(`Modelo "${file.name}" carregado de forma nativa!`); setConverting(false); return; } let result; if (ext === 'ifc') { result = await convertIFCtoGLB(buffer, file.name); } else { result = await convertToGLB(buffer, ext, file.name); } const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success(`"${file.name}" convertido para GLB e carregado!`); } catch (err) { console.error('[upload] conversion failed', err); const msg = err instanceof Error ? err.message : 'erro desconhecido'; toast.error(`Falha ao converter "${file.name}": ${msg}`); } finally { setConverting(false); } }, [addModel, ifcConvert]); // Carregar da Nuvem agora é encapsulado em const handleLoadDemoIFC = useCallback(async () => { setLoadingDemoIFC(true); try { const response = await fetch('/models/demo250hp.ifc'); const buffer = await response.arrayBuffer(); if (!ifcConvert) { const blob = new Blob([buffer], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); addModel({ fileName: 'demo250hp.ifc', fileSize: blob.size, url }); addRecentFile('demo250hp.ifc', blob).catch(() => {}); toast.success('Modelo demo "250 HP" (IFC nativo) carregado!'); return; } const result = await convertIFCtoGLB(buffer, 'demo250hp.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success('Modelo demo "250 HP" (IFC) carregado!'); } catch (err) { console.error(err); toast.error('Falha ao processar modelo demo IFC'); } finally { setLoadingDemoIFC(false); } }, [addModel, ifcConvert]); const handleLoadDemoCotovelo = useCallback(async () => { setLoadingDemoCotovelo(true); try { const response = await fetch('/models/B126-COTOVELO.ifc'); const buffer = await response.arrayBuffer(); if (!ifcConvert) { const blob = new Blob([buffer], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); addModel({ fileName: 'B126-COTOVELO.ifc', fileSize: blob.size, url }); addRecentFile('B126-COTOVELO.ifc', blob).catch(() => {}); toast.success('Modelo demo "B126 Cotovelo" (IFC nativo) carregado!'); return; } const result = await convertIFCtoGLB(buffer, 'B126-COTOVELO.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success('Modelo demo "B126 Cotovelo" (IFC) carregado!'); } catch (err) { console.error(err); toast.error('Falha ao processar modelo demo Cotovelo'); } finally { setLoadingDemoCotovelo(false); } }, [addModel, ifcConvert]); const handleEnterViewer = () => { if (!model) { toast.error('Importe um modelo 3D primeiro'); return; } navigate('/viewer'); }; return (
{/* Logo area */}

TrackSteelXR

Inspeção de Qualidade Industrial

{/* Main card */}
{/* Import button */} {/* Toggle IFC Conversion */}

{ifcConvert ? 'Otimizado para AR (conversão automática)' : 'Modo nativo (carregamento bruto IFC)'}

{/* Demo buttons */}
{models.length > 0 && (

{models.length < maxModels ? `Você pode adicionar mais ${maxModels - models.length} peça(s) à cena` : `Limite de ${maxModels} peças simultâneas atingido`}

)} {/* Recent files card — últimos 6 modelos abertos (cache local) */}
{/* Enter viewer */} {/* XR Status */}
{xrSupported === null ? <> Verificando suporte WebXR… : xrSupported ? <> WebXR Compatível — Passthrough disponível : <> WebXR não disponível neste navegador }
{/* Footer */}

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)}
                    
)}
)) )}
)}
); }; export default Index;