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

322 lines
14 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 } from '@react-pdf/renderer';
import { useGalpaoStore } from '../store/galpaoStore';
import { useWindStore } from '../store/appStore';
import { useCaptureStore } from '../store/captureStore';
import {
getColumnLinearLoads,
getRoofLinearLoads,
getAllPillarBaseReactions,
getDragForce,
} from './line-loads';
import { getWallCpeOfficial, getRoofCpeOfficial } from './coefficients';
import { appendKgf } from './export-generic-pdf';
const styles = StyleSheet.create({
page: {
flexDirection: 'column',
padding: 40,
fontSize: 10,
fontFamily: 'Helvetica',
color: '#333',
},
header: {
marginBottom: 15,
borderBottom: '2pt solid #4f46e5',
paddingBottom: 10,
},
title: { fontSize: 22, fontWeight: 'extrabold', color: '#1e1b4b' },
subtitle: { fontSize: 11, color: '#4338ca', marginTop: 4, fontWeight: 'bold' },
section: { marginTop: 15, marginBottom: 10 },
sectionTitle: { fontSize: 14, fontWeight: 'bold', marginBottom: 8, color: '#111' },
row: { flexDirection: 'row', marginBottom: 4 },
label: { width: 200, fontWeight: 'bold' },
value: { flex: 1 },
table: {
display: 'flex',
flexDirection: 'column',
marginTop: 10,
borderTop: '1pt solid #ccc',
borderLeft: '1pt solid #ccc',
},
tableRow: { flexDirection: 'row' },
tableHeader: { backgroundColor: '#f3f4f6', fontWeight: 'bold' },
tableCell: {
flex: 1,
padding: 5,
borderRight: '1pt solid #ccc',
borderBottom: '1pt solid #ccc',
textAlign: 'center',
},
tableCellFirst: {
flex: 1,
padding: 5,
borderRight: '1pt solid #ccc',
borderBottom: '1pt solid #ccc',
textAlign: 'left',
},
footer: {
position: 'absolute',
bottom: 30,
left: 40,
right: 40,
textAlign: 'center',
color: '#999',
fontSize: 8,
borderTop: '1pt solid #eaeaea',
paddingTop: 10,
},
sceneImage: {
width: 480,
height: 270,
objectFit: 'contain',
marginVertical: 8,
border: '1pt solid #ddd',
},
sceneCaption: {
fontSize: 8,
color: '#666',
fontStyle: 'italic',
textAlign: 'center',
marginBottom: 8,
},
});
interface ReportProps {
galpao: ReturnType<typeof useGalpaoStore.getState>;
wind: ReturnType<typeof useWindStore.getState>;
sceneImage?: string | null;
}
const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
const pressure = (cpe: number) => (wind.q * (cpe - wind.cpi)).toFixed(3);
const cpi = wind.cpi.toFixed(2);
const fmtSigned = (v: number, p = 3) => (v >= 0 ? `+${v.toFixed(p)}` : v.toFixed(p));
const FRAME_SPACING = 6.0;
const PURLIN_SPACING = 1.5;
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<Text style={styles.title}>BrainWind v1.0 Memória de Cálculo</Text>
<Text style={styles.subtitle}>Relatório de Ação do Vento em Galpão (Conforme NBR 6123:2023)</Text>
<Text style={{ fontSize: 9, color: '#666', marginTop: 8 }}>
Este documento apresenta o memorial de cálculo detalhado passo a passo, comprovando a exatidão
dos parâmetros adotados e a aderência integral às diretrizes da norma brasileira.
</Text>
</View>
<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}>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={{ marginTop: 10, padding: 8, backgroundColor: '#f9fafb', borderLeft: '3pt solid #4f46e5' }}>
<Text style={{ fontSize: 11, fontWeight: 'bold', marginBottom: 4 }}>Memória de Cálculo (Sec 4.2 e 4.3):</Text>
<Text style={{ fontSize: 10, fontFamily: 'Courier', marginBottom: 4 }}>
Vₖ = V × S × S × S
</Text>
<Text style={{ fontSize: 10, fontFamily: 'Courier', marginBottom: 8, color: '#4b5563' }}>
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
</Text>
<Text style={{ fontSize: 10, fontFamily: 'Courier', marginBottom: 4 }}>
q = 0,613 × (Vₖ)²
</Text>
<Text style={{ fontSize: 10, fontFamily: 'Courier', color: '#4b5563' }}>
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)}
</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>2. Geometria do Galpão (Tabelas 4 e 5)</Text>
<View style={styles.row}>
<Text style={styles.label}>Largura (b):</Text>
<Text style={styles.value}>{galpao.width} m</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Comprimento (a):</Text>
<Text style={styles.value}>{galpao.length} m</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Altura do -direito (h):</Text>
<Text style={styles.value}>{galpao.height} m</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Inclinação do Telhado (θ):</Text>
<Text style={styles.value}>{galpao.roofPitch}°</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Direção do Vento Analisada:</Text>
<Text style={styles.value}>{wind.windAngle}°</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>3. Pressão Interna (Sec. 6.3, Figura 2)</Text>
<View style={styles.row}>
<Text style={styles.label}>Caso de Permeabilidade:</Text>
<Text style={styles.value}>{wind.permeabilityCase}</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Coeficiente Cpi:</Text>
<Text style={styles.value}>{cpi}</Text>
</View>
</View>
{[0, 90].map((angle, index) => {
const wCpe = getWallCpeOfficial(galpao.length, galpao.width, galpao.height, angle as 0 | 90);
const rCpe = getRoofCpeOfficial(galpao.length, galpao.width, galpao.height, galpao.roofPitch, angle as 0 | 90);
const colLoads = getColumnLinearLoads(wind.cpi, wind.q, wCpe, FRAME_SPACING, angle as 0 | 90);
const rLoads = getRoofLinearLoads(wind.cpi, wind.q, rCpe, PURLIN_SPACING, galpao.roofPitch);
const rxns = getAllPillarBaseReactions(colLoads, galpao.height);
const dForce = getDragForce(wCpe, rCpe, wind.q, galpao.length, galpao.width, galpao.height, galpao.roofPitch, angle as 0 | 90);
const secBase = index === 0 ? 4 : 6;
return (
<View wrap={false} key={`angle-${angle}`}>
<Text style={{ fontSize: 16, fontWeight: 'bold', color: '#1e1b4b', marginTop: 20, marginBottom: 10, borderBottom: '1pt solid #ddd', paddingBottom: 5 }}>
Cenário: Vento a {angle}°
</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>{secBase}. Coeficientes e Pressões (q × (Cpe Cpi))</Text>
<View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={styles.tableCellFirst}>Elemento / Região</Text>
<Text style={styles.tableCell}>Cpe</Text>
<Text style={styles.tableCell}>Cpi</Text>
<Text style={styles.tableCell}>p [kN/m² (kgf/m²)]</Text>
</View>
{Object.entries(wCpe).map(([face, cpeVal]) => (
<View style={styles.tableRow} key={`wall-${face}`}>
<Text style={styles.tableCellFirst}>Parede {face}</Text>
<Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text>
<Text style={styles.tableCell}>{cpi}</Text>
<Text style={styles.tableCell}>{pressure(cpeVal as number)} ({(parseFloat(pressure(cpeVal as number)) * 101.9716).toFixed(1)})</Text>
</View>
))}
{Object.entries(rCpe).map(([face, cpeVal]) => (
<View style={styles.tableRow} key={`roof-${face}`}>
<Text style={styles.tableCellFirst}>Telhado {face}</Text>
<Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text>
<Text style={styles.tableCell}>{cpi}</Text>
<Text style={styles.tableCell}>{pressure(cpeVal as number)} ({(parseFloat(pressure(cpeVal as number)) * 101.9716).toFixed(1)})</Text>
</View>
))}
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>
{secBase + 1}. Cargas Lineares (kN/m (kgf/m))
</Text>
<Text style={{ fontSize: 9, marginBottom: 6 }}>
Pórticos: {FRAME_SPACING} m | Terças: {PURLIN_SPACING} m
</Text>
<View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={styles.tableCellFirst}>Pilar</Text>
<Text style={styles.tableCell}>Cpe</Text>
<Text style={styles.tableCell}>w [kN/m (kgf/m)]</Text>
</View>
{([
['Barlavento', angle === 0 ? wCpe.C : wCpe.A, colLoads.windward],
['Sotavento', angle === 0 ? wCpe.D : wCpe.B, colLoads.leeward],
['Lateral 1', angle === 0 ? wCpe.A : wCpe.C, colLoads.sideA],
['Lateral 2', angle === 0 ? wCpe.B : wCpe.D, colLoads.sideB],
] as const).map(([label, cpeVal, w]) => (
<View style={styles.tableRow} key={`col-${label}`}>
<Text style={styles.tableCellFirst}>{label}</Text>
<Text style={styles.tableCell}>{cpeVal.toFixed(2)}</Text>
<Text style={styles.tableCell}>{fmtSigned(w)} ({fmtSigned(w * 101.9716, 1)})</Text>
</View>
))}
</View>
<View style={[styles.table, { marginTop: 10 }]}>
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={styles.tableCellFirst}>Terça (Zona)</Text>
<Text style={styles.tableCell}>Cpe</Text>
<Text style={styles.tableCell}>w [kN/m (kgf/m)]</Text>
</View>
{(['E', 'F', 'G', 'H', 'I', 'J'] as const).map((z) => (
<View style={styles.tableRow} key={`roof-${z}`}>
<Text style={styles.tableCellFirst}>{z}</Text>
<Text style={styles.tableCell}>{rCpe[z].toFixed(2)}</Text>
<Text style={styles.tableCell}>{fmtSigned(rLoads[z])} ({fmtSigned(rLoads[z] * 101.9716, 1)})</Text>
</View>
))}
</View>
<View style={{ marginTop: 6, fontSize: 9 }}>
<Text>Reação global na base: <Text style={{ fontWeight: 'bold' }}>{fmtSigned(rxns.total, 3)} kN ({fmtSigned(rxns.total * 101.9716, 1)} kgf)</Text></Text>
<Text>Força de arrasto global (Cₐ): <Text style={{ fontWeight: 'bold' }}>{dForce.forceKN.toFixed(3)} kN ({(dForce.forceKN * 101.9716).toFixed(1)} kgf)</Text></Text>
</View>
</View>
</View>
);
})}
{sceneImage && (
<View style={styles.section} wrap={false}>
<Text style={styles.sectionTitle}>8. Modelo 3D (Captura de Cena)</Text>
<PdfImage src={sceneImage} style={styles.sceneImage} />
<Text style={styles.sceneCaption}>
Vista isométrica capturada em tempo real pelo projetista na interface web. Cores indicam intensidade de pressão (azul:
empuxo, vermelho: sucção).
</Text>
</View>
)}
<View style={{ marginTop: 20, padding: 10, backgroundColor: '#f3f4f6', borderRadius: 4 }}>
<Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 4, color: '#111' }}>Declaração de Conformidade:</Text>
<Text style={{ fontSize: 9, color: '#444' }}>
Todos os coeficientes de forma (Cpe), coeficientes de pressão interna (Cpi) e pressões dinâmicas calculados neste relatório
estão em conformidade estrita com os procedimentos, tabelas e ábacos definidos pela norma ABNT NBR 6123:2023.
Este memorial serve como prova matemática e pode ser anexado aos documentos do projeto estrutural.
</Text>
</View>
<Text style={styles.footer}>
Gerado por BrainWind v1.0 Tecnologia Auditada em Engenharia Estrutural (NBR 6123:2023)
</Text>
</Page>
</Document>
);
};
export async function exportGalpaoToPDF() {
const galpao = useGalpaoStore.getState();
const wind = useWindStore.getState();
const sceneImage = useCaptureStore.getState().capturedImage;
const blob = await pdf(
<ReportDocument galpao={galpao} wind={wind} sceneImage={sceneImage} />,
).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'memoria_calculo_vento.pdf');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}