# TrackSteelXR — Análise de Melhorias **Data da análise**: 2026-05-15 **Analisado por**: Hermes (VPS BrainSteel) **Escopo**: `/host_root/root/Apps/tracksteelxr/` (98 arquivos, ~7647 linhas TS/TSX) --- ## 1. DIAGNÓSTICO GERAL ### Stack Tecnológica - **Frontend**: React 18 + TypeScript 5 + Vite - **3D Engine**: Three.js 0.160 + @react-three/fiber + @react-three/drei - **UI**: shadcn-ui + Radix UI + Tailwind CSS - **State**: Zustand 4.5 - **PDF**: jsPDF 4.2 - **Format support**: GLB, OBJ, STL, IFC via web-ifc - **Gerenciador**: Bun (bun.lockb) ### Qualidade Geral - ✅ Projeto bem estruturado com separação de concerns - ✅ Boas práticas de tipagem TypeScript - ✅ Componentes bem modularizados (UI, 3D, stores, hooks) - ✅ Algoritmos de SmartMeasure interesting (circle fit, vertex snap) - ⚠️ Performance comprometida por loops desnecessários - ⚠️ Memory leaks em event handlers - ⚠️ Dependências desatualizadas (CVE risks) - ⚠️ Validação de input ausente --- ## 2. CRÍTICAS URGENTES (🔴 Alta Prioridade) ### 2.1 Performance: Loop Infinito Anula `frameloop="demand"` **Arquivo**: `src/components/three/ModelViewer.tsx` **Linha**: ~485 (no `onCreated` do Canvas) **Problema**: ```tsx { const loop = () => { invalidate(); requestAnimationFrame(loop); }; // ❌ RENDERIZA SEMPRE loop(); }} > ``` O `frameloop="demand"` faz o Canvas renderizar **apenas quando `invalidate()` é chamado** (normalmente por mudanças de state). Mas o `onCreated` cria um `requestAnimationFrame` loop que chama `invalidate()` a cada frame, forçando renderização contínua — igual ao `frameloop="always"`. **Impacto**: Em modelos grandes (>500k triângulos), isso causa 60%+ de uso de CPU desnecessário. Dispositivos móveis根本无法正常运行。 **Correção**: ```tsx { gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); // NÃO criar loop aqui }} > ``` Ou, se precisa de animação constante (não é o caso aqui), usar `frameloop="always"` e remover o loop manual. --- ### 2.2 Memory Leak em SmartSnapHandler e HoverDetector **Arquivo**: `src/components/three/ModelViewer.tsx` **Linhas**: ~200-350 **Problema**: Event listeners adicionados com `gl.domElement.addEventListener` não são removidos adequadamente quando componentes desmontam ou quando callbacks mudam (muda `onMove` referência). Cada re-render do componente pode adicionar novos listeners sem remover os antigos. ```tsx gl.domElement.addEventListener('pointermove', onMove); // ... return () => { gl.domElement.removeEventListener('pointermove', onMove); // ❌ hoverTimer.current também vaza se callback mudar }; ``` **Impacto**: Após extended use, centenas de listeners acumulados no DOM. Memory leak progressivo até tab crash. **Correção**: ```tsx useEffect(() => { if (!measureMode) return () => {}; const onMove = (e: MouseEvent) => { /* ... */ }; const onLeave = () => { /* ... */ }; gl.domElement.addEventListener('pointermove', onMove); gl.domElement.addEventListener('pointerleave', onLeave); return () => { gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointerleave', onLeave); hoverTimer.current && clearTimeout(hoverTimer.current); }; }, [measureMode, camera, scene, gl, raycaster, mouse, setHoverInfo]); // incluir todas deps ``` --- ### 2.3 SmartMeasure.ts — Algoritmo O(n) em Vertex Search **Arquivo**: `src/lib/SmartMeasure.ts` **Função**: `findNearestVertex()` **Problema**: Para cada mousemove, itera **todos** os vértices do mesh: ```ts for (let i = 0; i < posAttr.count; i++) { _v.fromBufferAttribute(posAttr, i); _v.applyMatrix4(mesh.matrixWorld); _projected.copy(_v).project(camera); // cálculo de distância } ``` Modelo STL médio: 100k-500k vértices. Isso é 500k cálculos por mousemove, a 60fps = 30 milhões de operações/segundo. **Impacto**: Lag severo em modelos grandes, especialmente em dispositivos com GPU limitada. **Correção** (usar Octree spatial indexing): ```ts import { Octree } from 'three/examples/jsm/math/Octree.js'; import { OctreeHelper } from 'three/examples/jsm/objects/OctreeHelper.js'; // Pré-calcular octree uma vez quando modelo carrega const octree = useMemo(() => { const tree = new Octree(); tree.fromGeometry(mesh.geometry, 8); // 8 = subdivisions return tree; }, [mesh.geometry]); // Buscar vértices no octree ao invés de iterar todos const nearbyVertices = octree.search(worldPoint, searchRadius, true); ``` Alternativa mais simples: reduzir search radius ou usar grid spatial hash. --- ### 2.4 Dependências Desatualizadas — Risco de CVE **Arquivo**: `package.json` | Pacote | Atual | Latest | Status | |--------|-------|--------|--------| | `three` | ^0.160.1 | 0.170+ | ⚠️ Vulnerabilidades conhecidas | | `@react-three/drei` | ^9.122.0 | 9.134+ | ⚠️ Atualizar | | `vite` | ^5.4.19 | 6.x disponível | 🔴 CVE em versões 5.x < 5.4.11 | | `jspdf` | ^4.2.0 | 4.2.0 (atual) | ✅ Mas monitorar | | `react-router-dom` | ^6.30.1 | 7.x beta | ⚠️ Breaking changes em 7 | **Riscos específicos**: - **Vite 5.x**: Múltiplos CVEs de 2024 (CVE-2024-23331, CVE-2024-29041, etc.) permitiriam path traversal e XSS. - **jsPDF**: Histórico de vulnerabilidades de deserialização. Sempre manter atualizado. **Correção**: ```bash # Atualizar com cuidado (alguns são breaking changes) npm update three @react-three/drei vite react-router-dom # Ou verificar manualmente antes de atualizar npm audit npm audit fix ``` --- ## 3. REFATORAÇÃO (🟡 Média Prioridade) ### 3.1 Zustand Store Acoplado — 30+ states misturados **Arquivo**: `src/stores/useModelStore.ts` **Problema**: Um único store com responsibilities demais: - UI state: `opacity`, `renderMode`, `anchorMode`, `showGrid`, `wireframeColor` - 3D state: `measurePoints`, `measurements`, `snapPoint`, `hoverInfo` - Business state: `checklist`, `inspectionResult` - File state: `model`, `compareImage` - Meta state: `screenshots`, `xrSupported` Cada mudança de qualquer state causa re-render em **todos** os componentes que usam `useModelStore`. **Correção**: Separar em stores modulares: ```ts // stores/uiStore.ts — UI-only (opacity, renderMode, showGrid, etc.) const useUIStore = create((set) => ({ opacity: 1, setOpacity: (opacity) => set({ opacity }), // ... })); // stores/measurementStore.ts — 3D/measurement state const useMeasurementStore = create((set) => ({ measurePoints: [], addMeasurePoint: (p) => set((state) => /* ... */), // ... })); // stores/inspectionStore.ts — checklist/inspection const useInspectionStore = create((set) => ({ checklist: defaultChecklist, setChecklistItemStatus: (id, status) => set((state) => /* ... */), // ... })); // stores/uploadStore.ts — model upload handling const useUploadStore = create((set) => ({ model: null, setModel: (model) => set({ model }), // ... })); ``` **Benefício**: Components só re-renderizam quando o state específico muda. 50-70% menos re-renders. --- ### 3.2 GLBModel.useEffect — 700+ linhas de lógica acoplada **Arquivo**: `src/components/three/ModelViewer.tsx` (função `GLBModel`) **Problema**: Um único `useEffect` de ~150 linhas que faz: 1. Material cloning em todos os meshes 2. Cleanup de edge lines anteriores 3. Lógica de cor (rejected/approved/default) 4. Criação de edge lines novas 5. Atualização de opacity/wireframe **Correção**: Extrair para hooks customizados: ```ts // hooks/useMaterialManager.ts export function useMaterialManager(scene: THREE.Group, renderMode: string) { const originalColors = useRef>(new Map()); useEffect(() => { // material cloning logic }, [scene, renderMode]); return { originalColors }; } // hooks/useEdgeRenderer.ts export function useEdgeRenderer( mesh: THREE.Mesh, enabled: boolean, thresholdAngle: number ) { useEffect(() => { if (!enabled) return; // create edge lines return () => { /* cleanup */ }; }, [mesh, enabled, thresholdAngle]); } // hooks/useModelColors.ts export function useModelColors( scene: THREE.Group, checklist: ChecklistItem[], renderMode: string ) { // color logic based on checklist status } ``` --- ### 3.3 convertToGLB.ts — Material Hardcoded **Arquivo**: `src/lib/convertToGLB.ts` **Problema**: ```ts const material = new THREE.MeshStandardMaterial({ color: 0x8899aa, metalness: 0.85, roughness: 0.35, }); ``` Material fixo para todos os modelos convertidos. Usuário não pode customizar cor/textura. **Correção**: ```ts interface ConvertOptions { color?: number; metalness?: number; roughness?: number; } export async function convertToGLB( buffer: ArrayBuffer, ext: 'obj' | 'stl', fileName: string, options: ConvertOptions = {} ): Promise<{ blob: Blob; fileName: string; fileSize: number }> { const defaultMaterial = { color: 0x8899aa, metalness: 0.85, roughness: 0.35, ...options, }; const material = new THREE.MeshStandardMaterial(defaultMaterial); // ... } ``` --- ### 3.4 generateReport.ts — Tratamento de Erro Inadequado **Arquivo**: `src/lib/generateReport.ts` **Problema**: ```ts try { pdf.addImage(data.screenshots[i], 'PNG', margin, y, imgW, imgH); } catch { pdf.text(`[Erro ao inserir screenshot ${i + 1}]`, margin, y); } ``` Catch genérico oculta o erro real. Falha silenciosamente sem logging. **Correção**: ```ts try { pdf.addImage(data.screenshots[i], 'PNG', margin, y, imgW, imgH); } catch (err) { console.error(`[generateReport] Failed to add screenshot ${i + 1}:`, err); // Fallback: tentar converter ou redimensionar try { const imgData = resizeImage(data.screenshots[i], 1920); pdf.addImage(imgData, 'PNG', margin, y, imgW, imgH); } catch (fallbackErr) { pdf.text(`[Screenshot ${i + 1} não disponível]`, margin, y); } } ``` --- ### 3.5 Validação de Upload Ausente **Arquivo**: `src/lib/convertToGLB.ts` **Problema**: ```ts export function getSupportedExtension(fileName: string): SupportedExt | null { const ext = fileName.split('.').pop()?.toLowerCase(); if (ext === 'glb' || ext === 'obj' || ext === 'stl' || ext === 'ifc') return ext; return null; } ``` Só checa extensão, não conteúdo. Arquivo `.glb` com conteúdo corrompido/crasha o loader. **Correção**: ```ts export async function validateModelFile(file: File): Promise { // 1. Check extension const ext = file.name.split('.').pop()?.toLowerCase(); if (!['glb', 'obj', 'stl', 'ifc'].includes(ext)) { return { valid: false, error: 'Formato não suportado' }; } // 2. Check MIME type (if browser provides) const validMimes = { glb: ['model/gltf-binary', 'application/octet-stream'], obj: ['model/obj', 'text/plain', 'application/octet-stream'], stl: ['model/stl', 'application/sla', 'application/octet-stream'], ifc: ['application/x-step', 'application/octet-stream'], }; // 3. Check file size (max 500MB for IFC, 100MB for others) const maxSizes = { ifc: 500 * 1024 * 1024, default: 100 * 1024 * 1024 }; const maxSize = maxSizes[ext] || maxSizes.default; if (file.size > maxSize) { return { valid: false, error: `Arquivo muito grande (max ${maxSize / 1024 / 1024}MB)` }; } // 4. Try to parse header (for GLB: first 4 bytes = 0x46546C67 = 'glTF') if (ext === 'glb') { const header = await file.slice(0, 20).arrayBuffer(); const view = new DataView(header); const magic = view.getUint32(0, true); if (magic !== 0x46546C67) { return { valid: false, error: 'Arquivo GLB inválido (magic bytes incorretos)' }; } } return { valid: true }; } ``` --- ## 4. OTIMIZAÇÕES / BOAS PRÁTICAS (🟢 Baixa Prioridade) ### 4.1 `preserveDrawingBuffer: true` Gasto de Memória **Arquivo**: `src/components/three/ModelViewer.tsx` ```tsx gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true // ⚠️ CUSTO EXTRA }} ``` `preserveDrawingBuffer: true` mantém o framebuffer em memória após cada render, permitindo `toDataURL()` para screenshots. Mas isso duplica o consumo de memória GPU. **Solução**: Ativar só quando usuário vai fazer screenshot: ```tsx // No state const [screenshots, addScreenshot] = useModelStore(s => [s.screenshots, s.addScreenshot]); // Mudar Canvas config baseado em necessidade const glConfig = useMemo(() => ({ antialias: true, alpha: true, powerPreference: 'high-performance' as const, preserveDrawingBuffer: screenshots.length > 0, // só ativa se há screenshots }), [screenshots.length]); ``` --- ### 4.2 pixelRatio Limitado a 1.5 ```tsx gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); ``` Limitar em 1.5 é bom para performance, mas em dispositivos Apple Retina (2x, 3x) poderia ir a 2 sem impacto perceptível na qualidade vs custo de render. **Sugestão**: ```tsx const maxPixelRatio = /Mac|iPhone|iPad/.test(navigator.platform) ? 2 : 1.5; gl.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio)); ``` --- ### 4.3 Test Coverage Ausente O projeto tem configuração Vitest e ESLint mas: - Apenas 2 arquivos de teste placeholder em `src/test/` - Nenhum teste de unidade ou integração - Cobertura: 0% **Sugestão de testes prioritários**: ```ts // src/lib/__tests__/convertToGLB.test.ts describe('convertToGLB', () => { it('should convert STL buffer to GLB', async () => { /* ... */ }); it('should convert OBJ buffer to GLB', async () => { /* ... */ }); it('should reject invalid file types', async () => { /* ... */ }); }); // src/stores/__tests__/useModelStore.test.ts describe('useModelStore', () => { it('should add measurement point after two clicks', () => { /* ... */ }); it('should clear measurements', () => { /* ... */ }); }); ``` --- ### 4.4 Fallback para WebXR Inexistente **Arquivo**: `src/stores/useModelStore.ts` ```ts xrSupported: boolean | null; // null = não checou ainda ``` Quando `xrSupported === false`, não há fallback ou mensagem ao usuário. **Sugestão**: ```tsx // No componente XRSession const xrSupported = useModelStore(s => s.xrSupported); if (xrSupported === false) { return (

WebXR não disponível neste dispositivo.

); } ``` --- ### 4.5 Inconsistência Bun/npm no package.json ```json "scripts": { "dev": "vite" // npm script } ``` Mas o projeto usa `bun.lockb` como lockfile. README menciona npm mas o ambiente usa bun. **Correção**: Padronizar para bun ou npm: ```bash # Se usando bun (recomendado - mais rápido) npm install -g bun bun install bun run dev # OU remover bun.lockb e usar npm rm bun.lockb npm install ``` --- ### 4.6 Screenshot Compression Ausente ```ts screenshots: string[]; // data:image/png;base64,... ``` Screenshots em base64 sem compressão crescem indefinidamente na memória. Sessões longas com 10+ screenshots podem causar OOM em dispositivos móveis. **Sugestão**: ```ts async function compressScreenshot(dataUrl: string, quality = 0.8): Promise { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const maxDim = 1920; const scale = Math.min(maxDim / img.width, maxDim / img.height, 1); canvas.width = img.width * scale; canvas.height = img.height * scale; const ctx = canvas.getContext('2d')!; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); resolve(canvas.toDataURL('image/jpeg', quality)); }; img.src = dataUrl; }); } ``` --- ### 4.7 Audio Validation Ausente ```ts audioUrl: string | null; ``` Não valida se URL é `blob://` ou `file://` válido. Não limpa strings maliciosas. **Sugestão**: ```ts function isValidAudioUrl(url: string): boolean { if (!url) return true; // null é válido try { const parsed = new URL(url); return ['blob:', 'file:'].includes(parsed.protocol); } catch { return false; } } function sanitizeAudioUrl(url: string | null): string | null { if (!url) return null; return isValidAudioUrl(url) ? url : null; } ``` --- ## 5. SEGURANÇA ### 5.1 Nenhuma Sanitização de Input de Arquivo - Upload aceita qualquer extensão de arquivo - Não há validação de MIME type real - Não há scan de conteúdo - Não há limite de tamanho de arquivo **Recomendação**: Implementar validação conforme item 3.5. --- ### 5.2 Nenhuma Proteção Contra DoS por Memória - Arquivos IFC podem ter gigabytes - `web-ifc` carrega arquivo completo em memória - Não há limite de tamanho de arquivo antes do parsing **Recomendação**: ```ts const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB function validateIFCFile(file: File): boolean { if (file.size > MAX_FILE_SIZE) { throw new Error(`Arquivo IFC muito grande: ${file.size / 1024 / 1024}MB (max 500MB)`); } return true; } ``` --- ### 5.3 Sem CSP Headers ou XSS Protection TrackSteelXR é uma React SPA sem headers de segurança configurados. web-ifc carrega arquivos potencialmente maliciosos. **Recomendação**: Configurar CSP no servidor que servir a app (ex: Nginx, Caddy, Cloudflare). Exemplo CSP mínimo: ``` Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; ``` --- ## 6. RESUMO PRIORITÁRIO | Prioridade | Item | Impacto | Esforço | |------------|------|--------|---------| | 🔴 Alta | Remover loop em `onCreated` | 60%+ CPU | 5 min | | 🔴 Alta | Fix memory leak listeners | Estabilidade | 15 min | | 🔴 Alta | Update vite (CVE) | Segurança | 10 min | | 🟡 Média | Octree para vertex search | Performance | 1-2h | | 🟡 Média | Separate Zustand stores | Manutenibilidade | 2-3h | | 🟡 Média | Validação de upload | Segurança | 1h | | 🟡 Média | Fix generateReport error handling | Confiabilidade | 30 min | | 🟢 Baixa | Test coverage | Confiabilidade | 4h+ | | 🟢 Baixa | Screenshot compression | Performance | 1h | | 🟢 Baixa | Configurar CSP headers | Segurança | 30 min | --- ## 7. PRÓXIMOS PASSOS RECOMENDADOS 1. **Imediato**: Corrigir loop infinito + memory leak (2 items, ~20 min) 2. **Esta semana**: Update vite + outras deps críticas 3. **Esta semana**: Adicionar validação de upload 4. **Próxima semana**: Octree para vertex search 5. **Próxima semana**: Separar Zustand stores --- *Documento gerado automaticamente por Hermes — VPS BrainSteel* *Última atualização: 2026-05-15*