322 lines
14 KiB
TypeScript
322 lines
14 KiB
TypeScript
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 Pé-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);
|
||
} |