diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx new file mode 100644 index 0000000..f25c910 --- /dev/null +++ b/src/components/CloudLoader.tsx @@ -0,0 +1,214 @@ +import { useState, useCallback } from 'react'; +import { Cloud, Loader2, Link2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useModelStore } from '@/stores/useModelStore'; +import { toast } from 'sonner'; +import { getSupportedExtension, convertToGLB } from '@/lib/convertToGLB'; +import { convertIFCtoGLB } from '@/lib/convertIFC'; + +const DEFAULT_URL = 'https://store.tracksteel.com.br/demo.ifc'; + +const PRESETS: { label: string; url: string }[] = [ + { label: 'Demo IFC (TrackSteel)', url: DEFAULT_URL }, +]; + +/** Normaliza URLs de Dropbox / OneDrive para apontar ao binário direto. */ +function normalizeCloudUrl(input: string): string { + try { + const u = new URL(input); + if (/(^|\.)dropbox\.com$/.test(u.hostname)) { + u.searchParams.set('dl', '1'); + return u.toString(); + } + if (/(^|\.)1drv\.ms$|onedrive\.live\.com$/.test(u.hostname)) { + u.searchParams.set('download', '1'); + return u.toString(); + } + return input; + } catch { + return input; + } +} + +/** Tenta obter um token JWT de uma instância FileBrowser NoAuth (POST /api/login vazio). */ +async function tryFileBrowserToken(origin: string): Promise { + try { + const r = await fetch(`${origin}/api/login`, { method: 'POST', mode: 'cors' }); + if (!r.ok) return null; + const txt = (await r.text()).trim(); + // FileBrowser devolve o JWT como texto puro + if (txt && txt.split('.').length === 3) return txt; + return null; + } catch { + return null; + } +} + +/** Faz fetch tentando vários endpoints típicos de hosts que servem SPA (FileBrowser etc.). */ +async function smartFetch(rawUrl: string): Promise { + const url = normalizeCloudUrl(rawUrl.trim()); + let res = await fetch(url, { mode: 'cors' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const ct = res.headers.get('content-type') || ''; + if (!ct.includes('text/html')) return res; + + // Servidor devolveu HTML — tentamos endpoints binários do FileBrowser + const u = new URL(url); + const path = u.pathname.startsWith('/api/raw/') ? u.pathname : `/api/raw${u.pathname}`; + + // 1ª tentativa: /api/raw/... cru + try { + const r = await fetch(`${u.origin}${path}${u.search}`, { mode: 'cors' }); + if (r.ok && !(r.headers.get('content-type') || '').includes('text/html')) return r; + if (r.status !== 401) { + // pode ser outro erro — segue para fluxo de token + } + } catch { /* continua */ } + + // 2ª tentativa: pega token JWT NoAuth e usa ?auth= + const token = await tryFileBrowserToken(u.origin); + if (token) { + const sep = u.search ? '&' : '?'; + const r = await fetch(`${u.origin}${path}${u.search}${sep}auth=${encodeURIComponent(token)}`, { mode: 'cors' }); + if (r.ok && !(r.headers.get('content-type') || '').includes('text/html')) return r; + } + + throw new Error( + 'O servidor devolveu HTML. Use um link DIRETO do arquivo (ex.: Dropbox ?dl=1, OneDrive ?download=1, ' + + 'ou habilite endpoint público no FileBrowser).' + ); +} + +interface CloudLoaderProps { + /** Renderiza como botão compacto (para HUD do AR) */ + compact?: boolean; + /** Variante visual do botão */ + variant?: 'default' | 'outline'; + className?: string; +} + +export function CloudLoader({ compact = false, variant = 'outline', className }: CloudLoaderProps) { + const setModel = useModelStore((s) => s.setModel); + const [open, setOpen] = useState(false); + const [url, setUrl] = useState(DEFAULT_URL); + const [loading, setLoading] = useState(false); + + const handleLoad = useCallback(async (rawUrl: string) => { + if (!rawUrl) return; + setLoading(true); + try { + const fileName = rawUrl.split('/').pop()?.split('?')[0] || 'modelo-cloud'; + const ext = getSupportedExtension(fileName); + if (!ext) { + toast.error('URL deve apontar para .GLB, .OBJ, .STL ou .IFC'); + return; + } + + const response = await smartFetch(rawUrl); + const buffer = await response.arrayBuffer(); + + let blob: Blob, outName = fileName, outSize = buffer.byteLength; + if (ext === 'glb') { + blob = new Blob([buffer], { type: 'model/gltf-binary' }); + } else if (ext === 'ifc') { + const r = await convertIFCtoGLB(buffer, fileName); + blob = r.blob; outName = r.fileName; outSize = r.fileSize; + } else { + const r = await convertToGLB(buffer, ext, fileName); + blob = r.blob; outName = r.fileName; outSize = r.fileSize; + } + const blobUrl = URL.createObjectURL(blob); + setModel({ fileName: outName, fileSize: outSize, url: blobUrl }); + toast.success(`"${outName}" carregado da nuvem`); + setOpen(false); + } catch (err) { + console.error(err); + toast.error(err instanceof Error ? err.message : 'Falha ao carregar da nuvem (verifique URL/CORS)'); + } finally { + setLoading(false); + } + }, [setModel]); + + return ( + + + {compact ? ( + + ) : ( + + )} + + + + + + Carregar modelo da nuvem + + + URL direta para .GLB, .OBJ, .STL ou .IFC. Dropbox use ?dl=1, OneDrive ?download=1. + FileBrowser é detectado automaticamente. + + + +
+
+ +
+ + setUrl(e.target.value)} + placeholder="https://exemplo.com/modelo.glb" + disabled={loading} + className="font-mono text-xs" + /> +
+
+ + {PRESETS.length > 0 && ( +
+ +
+ {PRESETS.map((p) => ( + + ))} +
+
+ )} +
+ + + + + +
+
+ ); +} diff --git a/src/components/XRHud.tsx b/src/components/XRHud.tsx index 384919d..e7c9866 100644 --- a/src/components/XRHud.tsx +++ b/src/components/XRHud.tsx @@ -10,6 +10,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { useModelStore } from '@/stores/useModelStore'; import { InspectionChecklist } from '@/components/InspectionChecklist'; import { ScaleSelector } from '@/components/ScaleSelector'; +import { CloudLoader } from '@/components/CloudLoader'; import { toast } from 'sonner'; const POSITION_STEP = 0.001; @@ -314,6 +315,9 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog {/* Scale presets */} + {/* Cloud loader (acessível também no AR) */} + + + +

Modo XR Imersivo