Files
BrainWind/app/src/lib/export-generic-pdf.tsx
T

295 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font, Svg, Path } 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://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.ttf' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu4mxMyIFYW8YR8w.ttf', fontWeight: 'bold' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzc.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 },
});
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<typeof useWindStore.getState>;
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('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 (
<Document>
<Page size="A4" style={styles.page}>
{/* Cabeçalho com Logotipo customizado ou placeholder */}
<View style={styles.header}>
{wind.companyLogo ? (
<PdfImage src={wind.companyLogo} style={{ width: 110, height: 40, objectFit: 'contain' }} />
) : (
<View style={{ width: 110, height: 40, border: '0.75pt dashed #cbd5e1', borderRadius: 2, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 7, color: '#94a3b8' }}>Logotipo Empresa</Text>
</View>
)}
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={styles.title}>BrainWind v1.0 Memória de Cálculo</Text>
<Text style={styles.subtitle}>Módulo: {moduleName} ABNT NBR 6123:2023</Text>
</View>
</View>
{/* 1. Parâmetros Globais do Vento */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>1. Parâmetros Globais do Vento e Pressão Dinâmica</Text>
<View style={styles.row}>
<Text style={styles.label}>Localidade de Projeto:</Text>
<Text style={styles.value}>{wind.locality || 'V₀ generalizado'}</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Velocidade Básica (V):</Text>
<Text style={styles.value}>{wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123)</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Fator Topográfico (S):</Text>
<Text style={styles.value}>{wind.s1} (conforme Seção 5.2)</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Fator de Rugosidade (S):</Text>
<Text style={styles.value}>{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Fator Estatístico (S):</Text>
<Text style={styles.value}>{wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4)</Text>
</View>
<View style={styles.mathCard}>
<Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 3, color: '#1e1b4b' }}>
Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3):
</Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
Vₖ = V × S × S × S
</Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 6, color: '#475569' }}>
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
</Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
q = 0,613 × (Vₖ)²
</Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', color: '#475569' }}>
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)}
</Text>
</View>
</View>
{sceneImage && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>2. Modelo 3D (Captura de Cena)</Text>
<PdfImage src={sceneImage} style={styles.sceneImage} />
<Text style={styles.sceneCaption}>
Vista isométrica em tempo real capturada pelo usuário na interface.
</Text>
</View>
)}
{sections.map((sec, idx) => (
<View style={styles.section} key={idx} wrap={false}>
<Text style={styles.sectionTitle}>
{sceneImage ? idx + 3 : idx + 2}. {sec.title}
</Text>
{sec.type === 'text' && sec.content && (
<Text style={styles.text}>{appendKgf(sec.content)}</Text>
)}
{sec.type === 'grid' && sec.gridItems && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{sec.gridItems.map((item, i) => (
<View style={{ width: '50%', marginBottom: 4 }} key={i}>
<Text style={{ fontWeight: 'bold', color: '#475569' }}>{item.label}:</Text>
<Text style={{ color: '#1e293b' }}>{appendKgf(item.value)}</Text>
</View>
))}
</View>
)}
{sec.type === 'table' && sec.tableHeaders && sec.tableRows && (
<View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}>
{sec.tableHeaders.map((th, i) => (
<Text key={i} style={i === 0 ? styles.tableCellFirst : styles.tableCell}>
{th}
</Text>
))}
</View>
{sec.tableRows.map((tr, rIdx) => (
<View style={styles.tableRow} key={rIdx}>
{tr.map((tc, cIdx) => (
<Text key={cIdx} style={cIdx === 0 ? styles.tableCellFirst : styles.tableCell}>
{appendKgf(tc)}
</Text>
))}
</View>
))}
</View>
)}
</View>
))}
{/* 6. Parecer Técnico e Considerações */}
<View style={styles.section} wrap={false}>
<Text style={styles.sectionTitle}>
{sceneImage ? sections.length + 3 : sections.length + 2}. Parecer Técnico e Observações
</Text>
<View style={styles.recCard}>
{recommendations.map((rec, rIdx) => (
<Text style={{ fontSize: 9, marginBottom: 4, lineHeight: 1.3, color: '#3b0764' }} key={rIdx}>
{rec}
</Text>
))}
</View>
</View>
{/* Rodapé fixo */}
<View style={styles.footer} fixed>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 6.5, color: '#94a3b8', flex: 1, textAlign: 'left' }}>
Legendas: V: Vel. básica | S, S, S: Fatores de vento | Vₖ: Vel. de projeto | q: Pressão dinâmica | Cpe: Pressão externa | Cpi: Pressão interna
</Text>
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'center', marginHorizontal: 10 }} render={({ pageNumber, totalPages }) => (
`Pág. ${pageNumber} de ${totalPages}`
)} />
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', flex: 0.3 }}>
<Svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="#6b21a8" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 2 }}>
<Path d="M12.8 19.6A2 2 0 1 0 14 16H2" />
<Path d="M17.5 8a2.5 2.5 0 1 1 2 4H2" />
<Path d="M9.8 4.4A2 2 0 1 1 11 8H2" />
</Svg>
<Text style={{ fontSize: 7, fontWeight: 'bold', color: '#6b21a8' }}>BrainWind</Text>
</View>
</View>
</View>
</Page>
</Document>
);
};
export async function exportGenericToPDF(moduleName: string, sections: GenericPDFSection[]) {
const wind = useWindStore.getState();
const sceneImage = useCaptureStore.getState().capturedImage;
const blob = await pdf(
<GenericReportDocument moduleName={moduleName} sections={sections} wind={wind} sceneImage={sceneImage} />
).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);
}