import { useNavigate } from "react-router-dom"; import { ArrowLeft, Glasses, Box, Ruler, FileText, Upload, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useModelStore } from "@/stores/useModelStore"; import { ModelViewerCanvas } from "@/components/three/ModelViewer"; import { ViewerControls } from "@/components/ViewerControls"; import { InspectionChecklist } from "@/components/InspectionChecklist"; import { FineTuningControls } from "@/components/FineTuningControls"; import { MeasurementsList } from "@/components/MeasurementsList"; import { ScreenshotGallery } from "@/components/ScreenshotGallery"; import { ShareButton } from "@/components/ShareButton"; import { CloudLoader } from "@/components/CloudLoader"; import { SceneModelList } from "@/components/SceneModelList"; import { useEffect, useRef, useCallback, useState } from "react"; import { toast } from "sonner"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from "@/lib/convertToGLB"; import { validateModelFile } from "@/lib/validateModelFile"; import { convertIFCtoGLB } from "@/lib/convertIFC"; const Viewer = () => { const navigate = useNavigate(); const { model, xrSupported, addModel, models, maxModels, scaleRatio } = useModelStore(); const fileInputRef = useRef(null); const [converting, setConverting] = useState(false); useEffect(() => { if (!model) { navigate("/"); } }, [model, navigate]); const handleFileUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; if (models.length >= maxModels) { toast.error(`Limite de ${maxModels} peças simultâneas atingido`); return; } const ext = getSupportedExtension(file.name); if (!ext) { toast.error('Formato inválido. Selecione .GLB, .OBJ, .STL ou .IFC'); return; } const sizeCheck = validateModelFile(file.name, file.size, ext); if (!sizeCheck.ok) { toast.error(sizeCheck.reason!); return; } if (ext === 'glb') { try { const head = await file.slice(0, 16).arrayBuffer(); const sig = validateModelFile(file.name, file.size, ext, head); if (!sig.ok) { toast.error(sig.reason!); return; } } catch (err) { console.error('[upload] header read failed', err); } const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); toast.success(`"${file.name}" adicionado à cena`); return; } setConverting(true); try { const buffer = await file.arrayBuffer(); const sig = validateModelFile(file.name, file.size, ext, buffer); if (!sig.ok) { toast.error(sig.reason!); if (sig.detail) console.warn('[validateModelFile]', sig.detail); return; } const result = ext === 'ifc' ? await convertIFCtoGLB(buffer, file.name) : await convertToGLB(buffer, ext, file.name); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); toast.success(`"${file.name}" convertido e adicionado!`); } 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, models.length, maxModels]); if (!model) return null; const handleEnterXR = async () => { if (!xrSupported) { toast.error("WebXR não é suportado neste navegador. Use o navegador do Meta Quest 3."); return; } navigate("/xr"); }; const canAddMore = models.length < maxModels; return (
{/* Top bar */}

TrackSteelXR

{/* 3D Canvas with floating controls */}
{/* Side panel */}
); }; function InfoItem({ icon: Icon, label, value }: { icon: any; label: string; value: string }) { return (

{label}

{value}

); } export default Viewer;