import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font } from '@react-pdf/renderer'; import { useWindStore } from '../store/appStore'; import { useCaptureStore } from '../store/captureStore'; // Registrar fonte Roboto com suporte Unicode completo (subscripts, grego, etc.) Font.register({ family: 'Roboto', fonts: [ { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf', fontWeight: 'bold' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Italic.ttf', fontStyle: 'italic' }, ], }); const styles = StyleSheet.create({ page: { flexDirection: 'column', padding: 40, fontSize: 9, fontFamily: 'Roboto', color: '#334155' }, header: { marginBottom: 15, borderBottom: '1.5pt solid #6b21a8', paddingBottom: 8, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, title: { fontSize: 16, fontWeight: 'bold', color: '#6b21a8', textAlign: 'right' }, subtitle: { fontSize: 9, color: '#64748b', marginTop: 3, textAlign: 'right' }, section: { marginTop: 12, marginBottom: 8 }, sectionTitle: { fontSize: 11, fontWeight: 'bold', marginBottom: 6, color: '#4c1d95', borderBottom: '0.5pt solid #e2e8f0', paddingBottom: 2 }, row: { flexDirection: 'row', marginBottom: 3 }, label: { width: 180, fontWeight: 'bold', color: '#475569' }, value: { flex: 1, color: '#1e293b' }, text: { fontSize: 9, marginBottom: 3, lineHeight: 1.3, color: '#334155' }, table: { display: 'flex', flexDirection: 'column', marginTop: 8, borderTop: '0.5pt solid #cbd5e1', borderLeft: '0.5pt solid #cbd5e1' }, tableRow: { flexDirection: 'row' }, tableHeader: { backgroundColor: '#f8fafc', fontWeight: 'bold' }, tableCell: { flex: 1, padding: 4, borderRight: '0.5pt solid #cbd5e1', borderBottom: '0.5pt solid #cbd5e1', textAlign: 'center', fontSize: 8 }, tableCellFirst: { flex: 1, padding: 4, borderRight: '0.5pt solid #cbd5e1', borderBottom: '0.5pt solid #cbd5e1', textAlign: 'left', fontSize: 8 }, footer: { position: 'absolute', bottom: 30, left: 40, right: 40, borderTop: '0.5pt solid #cbd5e1', paddingTop: 6 }, sceneImage: { width: 480, height: 250, objectFit: 'contain', marginVertical: 6, border: '0.5pt solid #e2e8f0', borderRadius: 4 }, sceneCaption: { fontSize: 7, color: '#64748b', fontStyle: 'italic', textAlign: 'center', marginBottom: 6 }, mathCard: { marginTop: 8, padding: 8, backgroundColor: '#f8fafc', borderLeft: '2.5pt solid #6b21a8', borderRadius: 2 }, recCard: { marginTop: 8, padding: 8, backgroundColor: '#faf5ff', borderLeft: '2.5pt solid #a855f7', borderRadius: 2 }, signatureBlock: { marginTop: 40, flexDirection: 'row', justifyContent: 'space-around', paddingTop: 30 }, signatureLine: { width: 200, borderTop: '0.75pt solid #334155', paddingTop: 4, alignItems: 'center' }, signatureText: { fontSize: 9, color: '#334155' }, }); export function getEffortColor(val: string | number): string { const num = typeof val === 'number' ? val : parseFloat(val.toString().replace(',', '.').replace(/[^\d.-]/g, '')); if (isNaN(num) || num === 0) return '#1e293b'; return num < 0 ? '#ea580c' : '#2563eb'; // orange for negative (suction), blue for positive (compression) } export function appendKgf(val: string | number): string { if (typeof val === 'number') return String(val); return val.replace(/(-?\d+(?:[.,]\d+)?)\s*(kN(?:\/m²|\/m2|\/m|·m|\.m|m)?)/g, (match, numStr, unit) => { const num = parseFloat(numStr.replace(',', '.')); if (isNaN(num)) return match; const kgf = num * 101.9716; let kgfUnit = 'kgf'; if (unit.includes('/m²') || unit.includes('/m2')) kgfUnit = 'kgf/m²'; else if (unit.includes('/m')) kgfUnit = 'kgf/m'; else if (unit.includes('·m') || unit.includes('.m') || unit === 'kNm') kgfUnit = 'kgf·m'; const kgfStr = kgf.toFixed(1); const kgfFormatted = numStr.includes(',') ? kgfStr.replace('.', ',') : kgfStr; return `${match} (${kgfFormatted} ${kgfUnit})`; }); } export interface GenericPDFSection { title: string; type: 'table' | 'text' | 'grid'; content?: string; tableHeaders?: string[]; tableRows?: (string | number)[][]; gridItems?: { label: string; value: string | number }[]; } export interface GenericPDFProps { moduleName: string; sections: GenericPDFSection[]; wind: ReturnType; sceneImage?: string | null; } function generateRecommendations(moduleName: string, sections: GenericPDFSection[], wind: any): string[] { const recs: string[] = []; let hasSuction = false; let forceValue = 0; for (const sec of sections) { if (sec.gridItems) { for (const item of sec.gridItems) { const labelLower = item.label.toLowerCase(); if (labelLower.includes('vertical') || labelLower.includes('fy') || labelLower.includes('força vertical')) { const valStr = String(item.value); const valNum = parseFloat(valStr.replace(/[^\d.-]/g, '')); if (!isNaN(valNum)) { if (valNum > 0 || labelLower.includes('arrancamento') || labelLower.includes('sucção')) { hasSuction = true; forceValue = Math.max(forceValue, Math.abs(valNum)); } } } } } } const nameLower = moduleName.toLowerCase(); if (nameLower.includes('abrigo') || nameLower.includes('pórtico') || nameLower.includes('marquise')) { recs.push("Em abrigos e pórticos em balanço, o arrancamento vertical (Fz) na cobertura é intensificado quando a parede de fundo é fechada devido ao aprisionamento de pressão (+Cpi)."); recs.push("Verifique criteriosamente as ligações pilar-viga e a ancoragem das telhas na região da testeira, onde as tensões de arrancamento atingem picos críticos."); if (hasSuction && forceValue > 15) { recs.push(`A força de arrancamento calculada (${forceValue.toFixed(1)} kN) requer verificação da fundação dos pilares contra arrancamento e tombamento.`); } } else if (nameLower.includes('cobertura') || nameLower.includes('isolada')) { recs.push("Para coberturas isoladas, o vento gera sucções severas de arrancamento nas bordas. Verifique criteriosamente o dimensionamento e ancoragem das terças."); if (hasSuction && forceValue > 20) { recs.push(`A força de arrancamento vertical calculada (${forceValue.toFixed(1)} kN) exige detalhamento especial de fundação resistente a tração.`); } } else if (nameLower.includes('torre') || nameLower.includes('reticulada')) { recs.push("Torres reticuladas sofrem grandes forças de arrasto. Certifique-se de que a esbeltez local atende aos limites regulamentares e verifique a fadiga nas conexões."); } else if (nameLower.includes('muro') || nameLower.includes('placa') || nameLower.includes('sign')) { recs.push("Em painéis e muros, o momento de tombamento na fundação é crítico. Recomenda-se adotar coeficiente de segurança mínimo de 1,5 contra tombamento."); } else if (nameLower.includes('ponte') || nameLower.includes('tabuleiro')) { recs.push("Pontes de grandes vãos são susceptíveis a oscilações aeroelásticas (Flutter/Galloping). Caso a velocidade crítica de flutter seja próxima à de projeto, execute ensaios aerodinâmicos complementares."); } else if (nameLower.includes('dinâ') || nameLower.includes('dinamica') || nameLower.includes('vórtice')) { recs.push("Estruturas esbeltas com frequência natural inferior a 1,0 Hz devem obrigatoriamente ser analisadas sob efeito de oscilação dinâmica."); } else if (nameLower.includes('cilindro') || nameLower.includes('chaminé')) { recs.push("Cilindros e reservatórios estão expostos a picos locais de pressão e flambagem da chapa. Anéis de rigidez superiores são altamente sugeridos."); } else if (nameLower.includes('abóbada') || nameLower.includes('cúpula') || nameLower.includes('vault') || nameLower.includes('dome')) { recs.push("Estruturas curvas apresentam zonas de transição com forte gradiente de pressões. Cuidado redobrado no cálculo das pressões tangenciais locais."); } if (wind.vk > 45) { recs.push(`A velocidade característica encontrada (${wind.vk.toFixed(1)} m/s) é elevada. É sugerido rígido controle de deslocamentos laterais na estrutura.`); } recs.push("O memorial serve como suporte ao projetista estrutural, cabendo ao Engenheiro Responsável Técnico (RT) a aprovação final dos esforços de dimensionamento."); return recs; } const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: GenericPDFProps) => { const recommendations = generateRecommendations(moduleName, sections, wind); return ( {/* Cabeçalho com Logotipo customizado ou placeholder */} {wind.companyLogo ? ( ) : ( Logotipo Empresa )} {wind.projectName || 'BrainWind v1.0 — Memória de Cálculo'} Módulo: {moduleName} — ABNT NBR 6123:2023 Cliente {wind.clientName || '-'} Autor {wind.authorName || '-'} Revisão {wind.projectRev || '-'} Data {new Date().toLocaleDateString('pt-BR')} {/* 1. Parâmetros Globais do Vento */} 1. Parâmetros Globais do Vento e Pressão Dinâmica Localidade de Projeto: {wind.locality || 'V0 generalizado'} Velocidade Básica (V0): {wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123) Fator Topográfico (S1): {wind.s1} (conforme Seção 5.2) Fator de Rugosidade (S2): {wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2) Fator Estatístico (S3): {wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4) Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3): Fórmula Base: Vk = V0 × S1 × S2 × S3 Vk = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s Pressão Dinâmica: q = 0,613 × (Vk)² q = 0,613 × ({wind.vk.toFixed(2)})² = {(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m² {sceneImage && ( 2. Modelo 3D (Captura de Cena) Vista isométrica em tempo real capturada pelo usuário na interface. )} {sections.map((sec, idx) => ( {sceneImage ? idx + 3 : idx + 2}. {sec.title} {sec.type === 'text' && sec.content && ( {appendKgf(sec.content)} )} {sec.type === 'grid' && sec.gridItems && ( {sec.gridItems.map((item, i) => ( {item.label.replace('⊥', 'perpendicular à').replace('∥', 'paralelo à')}: {appendKgf(item.value)} ))} )} {sec.type === 'table' && sec.tableHeaders && sec.tableRows && ( {sec.tableHeaders.map((th, i) => ( {th} ))} {sec.tableRows.map((tr, rIdx) => ( {tr.map((tc, cIdx) => ( 0 ? { color: getEffortColor(tc) } : {}]}> {appendKgf(tc)} ))} ))} )} ))} {sceneImage ? sections.length + 3 : sections.length + 2}. Parecer Técnico e Observações {recommendations.map((rec, rIdx) => ( • {rec} ))} {wind.authorName || 'Engenheiro Responsável'} CREA / CAU: Isenção de Responsabilidade do Software: Os resultados e relatórios gerados pelo BrainWind são de responsabilidade exclusiva e integral do usuário. O aplicativo atua como ferramenta de auxílio e não substitui o julgamento técnico, a análise detalhada das normas e o dimensionamento final por profissional habilitado. Legendas: V0: Vel. básica | S1, S2, S3: Fatores de vento | Vk: Vel. de projeto | q: Pressão dinâmica | Cpe: Pressão externa | Cpi: Pressão interna ( `Pág. ${pageNumber} de ${totalPages}` )} /> ); }; export async function exportGenericToPDF(moduleName: string, sections: GenericPDFSection[]) { const wind = useWindStore.getState(); const sceneImage = useCaptureStore.getState().capturedImage; const blob = await pdf( ).toBlob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `memoria_calculo_${moduleName.toLowerCase().replace(/\s+/g, '_')}.pdf`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }