🚀 Auto-deploy: BrainWind atualizado em 14/07/2026 18:03:29
This commit is contained in:
@@ -40,6 +40,8 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
||||
setS3,
|
||||
setS3Group,
|
||||
setTerrainCategory,
|
||||
locality,
|
||||
setLocality,
|
||||
} = useWindStore();
|
||||
|
||||
const [stationQuery, setStationQuery] = useState('');
|
||||
@@ -74,7 +76,7 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
||||
<Tabs defaultValue="norma" className="w-full mt-2 min-w-0">
|
||||
<TabsList className="flex w-full mb-4 bg-muted p-1 text-xs min-w-0">
|
||||
<TabsTrigger value="norma" className="text-xs py-1 px-1.5 flex-1">NBR 6123</TabsTrigger>
|
||||
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Local</TabsTrigger>
|
||||
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Localidade</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="norma" className="space-y-4 min-w-0">
|
||||
@@ -186,22 +188,30 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
||||
</div>
|
||||
|
||||
<div className="max-h-[200px] overflow-y-auto rounded-md border divide-y">
|
||||
{filteredStations.map((s) => (
|
||||
{filteredStations.map((s) => {
|
||||
const isSelected = locality === s.nome;
|
||||
return (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setV0(s.v0)}
|
||||
className="w-full text-left px-2 py-1.5 hover:bg-muted/60 transition-colors"
|
||||
onClick={() => {
|
||||
setV0(s.v0);
|
||||
setLocality(s.nome);
|
||||
}}
|
||||
className={`w-full text-left px-2 py-1.5 transition-colors ${
|
||||
isSelected ? 'bg-primary/10 border-l-2 border-primary' : 'hover:bg-muted/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-xs truncate max-w-[150px]">{s.nome}</span>
|
||||
<Badge variant="outline" className="font-mono text-[10px] py-0 px-1 font-semibold">V₀ = {s.v0} m/s</Badge>
|
||||
<span className={`font-medium text-xs truncate max-w-[150px] ${isSelected ? 'text-primary' : ''}`}>{s.nome}</span>
|
||||
<Badge variant={isSelected ? "default" : "outline"} className="font-mono text-[10px] py-0 px-1 font-semibold">V₀ = {s.v0} m/s</Badge>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{s.latitude} · {s.longitude} · {s.altitude} m</div>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] leading-tight text-muted-foreground">
|
||||
Selecionar uma estação ajusta V₀. Você pode sobrescrever manualmente na aba NBR.
|
||||
<p className="text-[10px] leading-tight text-muted-foreground mt-2">
|
||||
Selecionar uma estação define a localidade e ajusta V₀. Você pode sobrescrever V₀ manualmente na aba NBR.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useProjects } from '../lib/hooks/useProjects';
|
||||
import { useWindStore } from '../store/appStore';
|
||||
|
||||
interface SaveModuleDialogProps {
|
||||
moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower';
|
||||
inputs: Record<string, unknown>;
|
||||
triggerButton?: React.ReactNode;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export const SaveModuleDialog: React.FC<SaveModuleDialogProps> = ({ moduleType,
|
||||
terrainCategory: windStore.terrainCategory,
|
||||
largestDimension: windStore.largestDimension,
|
||||
heightZ: windStore.heightZ,
|
||||
locality: windStore.locality,
|
||||
} : undefined;
|
||||
|
||||
await save({
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function SceneCanvas({ fallback, style, className, moduleId, ...r
|
||||
}`}
|
||||
aria-label={isLocked ? 'Destravar visualização 3D' : 'Travar visualização 3D'}
|
||||
>
|
||||
{isLocked ? <Lock className="w-5 h-5 text-destructive-foreground" /> : <Unlock className="w-5 h-5 text-muted-foreground" />}
|
||||
{isLocked ? <Lock className="w-5 h-5 text-black" /> : <Unlock className="w-5 h-5 text-muted-foreground" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { MapPin, Wind } from 'lucide-react';
|
||||
import { GlobalWindSettingsModal } from './GlobalWindSettingsModal';
|
||||
|
||||
export const WindParametersSummary: React.FC = () => {
|
||||
const { v0, s1, s2, s3, vk, q, locality } = useWindStore();
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/40 p-3 space-y-2 mb-6">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h4 className="text-xs font-semibold flex items-center gap-1.5 text-muted-foreground">
|
||||
<Wind className="w-3.5 h-3.5" />
|
||||
Vento Base
|
||||
</h4>
|
||||
<GlobalWindSettingsModal trigger={<button className="text-[10px] underline text-primary cursor-pointer">Editar</button>} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs items-center">
|
||||
<span className="flex items-center gap-1 text-muted-foreground"><MapPin className="w-3 h-3"/> Localidade:</span>
|
||||
<span className="font-medium text-foreground truncate max-w-[150px]" title={locality || 'Não selecionada'}>{locality || 'Não selecionada'}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[11px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">V₀:</span>
|
||||
<span className="font-mono font-medium">{v0} m/s</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Vₖ:</span>
|
||||
<span className="font-mono font-medium">{vk.toFixed(2)} m/s</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">S₁×S₂×S₃:</span>
|
||||
<span className="font-mono">{(s1 * s2 * s3).toFixed(3)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">q:</span>
|
||||
<span className="font-mono font-medium">{q.toFixed(3)} kN/m²</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,38 @@
|
||||
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf } from '@react-pdf/renderer';
|
||||
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: 10, fontFamily: 'Helvetica', color: '#333' },
|
||||
header: { marginBottom: 20, borderBottom: '2pt solid #6b21a8', paddingBottom: 10 },
|
||||
title: { fontSize: 20, fontWeight: 'bold', color: '#6b21a8' },
|
||||
subtitle: { fontSize: 10, color: '#666', marginTop: 4 },
|
||||
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 },
|
||||
text: { fontSize: 10, marginBottom: 4, lineHeight: 1.4 },
|
||||
table: { display: 'flex', flexDirection: 'column', marginTop: 10, borderTop: '1pt solid #ccc', borderLeft: '1pt solid #ccc' },
|
||||
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: '#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 },
|
||||
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 {
|
||||
@@ -57,17 +69,85 @@ export interface GenericPDFProps {
|
||||
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}>Cargas de Vento: {moduleName} — NBR 6123:2023</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>
|
||||
@@ -85,19 +165,20 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
<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 #6b21a8' }}>
|
||||
<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 }}>
|
||||
<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: 10, fontFamily: 'Courier', marginBottom: 8, color: '#4b5563' }}>
|
||||
<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: 10, fontFamily: 'Courier', marginBottom: 4 }}>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
|
||||
q = 0,613 × (Vₖ)²
|
||||
</Text>
|
||||
<Text style={{ fontSize: 10, fontFamily: 'Courier', color: '#4b5563' }}>
|
||||
<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>
|
||||
@@ -108,7 +189,7 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
<Text style={styles.sectionTitle}>2. Modelo 3D (Captura de Cena)</Text>
|
||||
<PdfImage src={sceneImage} style={styles.sceneImage} />
|
||||
<Text style={styles.sceneCaption}>
|
||||
Vista isométrica capturada em tempo real pelo usuário.
|
||||
Vista isométrica em tempo real capturada pelo usuário na interface.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -124,12 +205,14 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
)}
|
||||
|
||||
{sec.type === 'grid' && sec.gridItems && (
|
||||
sec.gridItems.map((item, i) => (
|
||||
<View style={styles.row} key={i}>
|
||||
<Text style={styles.label}>{item.label}:</Text>
|
||||
<Text style={styles.value}>{appendKgf(item.value)}</Text>
|
||||
<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 && (
|
||||
@@ -155,9 +238,39 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
</View>
|
||||
))}
|
||||
|
||||
<Text style={styles.footer}>
|
||||
Gerado por BrainWind v1.0 — Ferramenta de Auxílio ao Cálculo Estrutural (NBR 6123:2023)
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
+147
-123
@@ -1,4 +1,4 @@
|
||||
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf } from '@react-pdf/renderer';
|
||||
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font, Svg, Path } from '@react-pdf/renderer';
|
||||
import { useGalpaoStore } from '../store/galpaoStore';
|
||||
import { useWindStore } from '../store/appStore';
|
||||
import { useCaptureStore } from '../store/captureStore';
|
||||
@@ -11,74 +11,37 @@ import {
|
||||
import { getWallCpeOfficial, getRoofCpeOfficial } from './coefficients';
|
||||
import { appendKgf } from './export-generic-pdf';
|
||||
|
||||
// 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: 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',
|
||||
},
|
||||
page: { flexDirection: 'column', padding: 40, fontSize: 9, fontFamily: 'Roboto', color: '#334155' },
|
||||
header: { marginBottom: 15, borderBottom: '1.5pt solid #4f46e5', paddingBottom: 8, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
title: { fontSize: 16, fontWeight: 'bold', color: '#1e1b4b', textAlign: 'right' },
|
||||
subtitle: { fontSize: 9, color: '#4338ca', marginTop: 3, textAlign: 'right' },
|
||||
section: { marginTop: 12, marginBottom: 8 },
|
||||
sectionTitle: { fontSize: 11, fontWeight: 'bold', marginBottom: 6, color: '#111', borderBottom: '0.5pt solid #cbd5e1', 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: '#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,
|
||||
},
|
||||
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 #4f46e5', borderRadius: 2 },
|
||||
recCard: { marginTop: 8, padding: 8, backgroundColor: '#eff6ff', borderLeft: '2.5pt solid #3b82f6', borderRadius: 2 },
|
||||
});
|
||||
|
||||
interface ReportProps {
|
||||
@@ -87,6 +50,30 @@ interface ReportProps {
|
||||
sceneImage?: string | null;
|
||||
}
|
||||
|
||||
function generateGalpaoRecommendations(galpao: any, wind: any): string[] {
|
||||
const recs: string[] = [];
|
||||
|
||||
// Analisar sucção no telhado
|
||||
// Cpi normalmente gera sucção extra se for positivo
|
||||
if (wind.cpi > 0) {
|
||||
recs.push(`Coeficiente de pressão interna positivo (Cpi = +${wind.cpi.toFixed(2)}) indica pressurização interna. Isso amplifica os esforços de arrancamento (sucção) na cobertura.`);
|
||||
} else if (wind.cpi < 0) {
|
||||
recs.push(`Coeficiente de pressão interna negativo (Cpi = ${wind.cpi.toFixed(2)}) indica sucção interna, o que diminui a sucção no telhado mas amplifica as pressões nas paredes barlavento.`);
|
||||
}
|
||||
|
||||
// Verificar esbeltez h/b
|
||||
const ratio = galpao.height / Math.min(galpao.width, galpao.length);
|
||||
if (ratio > 1.5) {
|
||||
recs.push("A relação pé-direito / largura é alta (estrutura esbelta). Verifique atentamente os deslocamentos no topo dos pórticos e garanta contraventamentos eficientes nas paredes.");
|
||||
}
|
||||
|
||||
// Declaração geral
|
||||
recs.push("Verifique as ligações das terças de cobertura (presilhas de fixação) contra arrancamento devido à ação local do vento nas zonas de borda (zonas F, G, J).");
|
||||
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 ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
const pressure = (cpe: number) => (wind.q * (cpe - wind.cpi)).toFixed(3);
|
||||
const cpi = wind.cpi.toFixed(2);
|
||||
@@ -95,20 +82,33 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
const FRAME_SPACING = 6.0;
|
||||
const PURLIN_SPACING = 1.5;
|
||||
|
||||
const recommendations = generateGalpaoRecommendations(galpao, 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}>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>
|
||||
<Text style={styles.subtitle}>Galpão Retangular — 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>
|
||||
@@ -126,48 +126,53 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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 }}>
|
||||
<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: 10, fontFamily: 'Courier', marginBottom: 8, color: '#4b5563' }}>
|
||||
<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: 10, fontFamily: 'Courier', marginBottom: 4 }}>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
|
||||
q = 0,613 × (Vₖ)²
|
||||
</Text>
|
||||
<Text style={{ fontSize: 10, fontFamily: 'Courier', color: '#4b5563' }}>
|
||||
<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>
|
||||
|
||||
{/* 2. Geometria do Galpão */}
|
||||
<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 style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
<View style={{ width: '50%', marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 'bold', color: '#475569' }}>Largura (b):</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{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 style={{ width: '50%', marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 'bold', color: '#475569' }}>Comprimento (a):</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{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 style={{ width: '50%', marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 'bold', color: '#475569' }}>Altura do Pé-direito (h):</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{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 style={{ width: '50%', marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 'bold', color: '#475569' }}>Inclinação do Telhado (θ):</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{galpao.roofPitch}°</Text>
|
||||
</View>
|
||||
<View style={{ width: '50%', marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 'bold', color: '#475569' }}>Direção do Vento Selecionada:</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{wind.windAngle}°</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>
|
||||
|
||||
{/* 3. Pressão Interna */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>3. Pressão Interna (Sec. 6.3, Figura 2)</Text>
|
||||
<View style={styles.row}>
|
||||
@@ -180,6 +185,7 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 4. Cenários de Vento */}
|
||||
{[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);
|
||||
@@ -190,26 +196,26 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
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 }}>
|
||||
<View wrap={false} key={`angle-${angle}`} style={{ marginTop: 15 }}>
|
||||
<Text style={{ fontSize: 13, fontWeight: 'bold', color: '#1e1b4b', marginTop: 10, marginBottom: 6, borderBottom: '1pt solid #cbd5e1', paddingBottom: 3 }}>
|
||||
Cenário: Vento a {angle}°
|
||||
</Text>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{secBase}. Coeficientes e Pressões (q × (Cpe − Cpi))</Text>
|
||||
<Text style={styles.sectionTitle}>{secBase}. Coeficientes e Pressões Localizadas (p = 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>
|
||||
<Text style={styles.tableCell}>p [kN/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>
|
||||
<Text style={styles.tableCell}>{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}</Text>
|
||||
</View>
|
||||
))}
|
||||
{Object.entries(rCpe).map(([face, cpeVal]) => (
|
||||
@@ -217,7 +223,7 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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>
|
||||
<Text style={styles.tableCell}>{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -225,17 +231,17 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{secBase + 1}. Cargas Lineares (kN/m (kgf/m))
|
||||
{secBase + 1}. Cargas Lineares por Pórtico e Terças
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, marginBottom: 6 }}>
|
||||
Pórticos: {FRAME_SPACING} m | Terças: {PURLIN_SPACING} m
|
||||
<Text style={{ fontSize: 8, color: '#64748b', marginBottom: 4 }}>
|
||||
Espaçamento dos 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.tableCellFirst}>Pilar principal do pórtico</Text>
|
||||
<Text style={styles.tableCell}>Cpe</Text>
|
||||
<Text style={styles.tableCell}>w [kN/m (kgf/m)]</Text>
|
||||
<Text style={styles.tableCell}>Carga w [kN/m]</Text>
|
||||
</View>
|
||||
{([
|
||||
['Barlavento', angle === 0 ? wCpe.C : wCpe.A, colLoads.windward],
|
||||
@@ -246,29 +252,29 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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>
|
||||
<Text style={styles.tableCell}>{appendKgf(`${fmtSigned(w)} kN/m`)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={[styles.table, { marginTop: 10 }]}>
|
||||
<View style={[styles.table, { marginTop: 8 }]}>
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
<Text style={styles.tableCellFirst}>Terça (Zona)</Text>
|
||||
<Text style={styles.tableCellFirst}>Terça de Cobertura (Zonas A-J)</Text>
|
||||
<Text style={styles.tableCell}>Cpe</Text>
|
||||
<Text style={styles.tableCell}>w [kN/m (kgf/m)]</Text>
|
||||
<Text style={styles.tableCell}>Carga w [kN/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>
|
||||
<Text style={styles.tableCell}>{appendKgf(`${fmtSigned(rLoads[z])} kN/m`)}</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 style={{ marginTop: 6, fontSize: 8.5, color: '#1e293b' }}>
|
||||
<Text>• Reação total na base do pilar: <Text style={{ fontWeight: 'bold' }}>{appendKgf(`${fmtSigned(rxns.total, 3)} kN`)}</Text></Text>
|
||||
<Text>• Força de arrasto global do galpão: <Text style={{ fontWeight: 'bold' }}>{appendKgf(`${dForce.forceKN.toFixed(3)} kN`)}</Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -280,24 +286,42 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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).
|
||||
Vista isométrica 3D do galpão renderizado com distribuição tridimensional de pressões.
|
||||
</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.
|
||||
{/* Parecer Técnico */}
|
||||
<View style={styles.section} wrap={false}>
|
||||
<Text style={styles.sectionTitle}>9. Parecer Técnico e Recomendações</Text>
|
||||
<View style={styles.recCard}>
|
||||
{recommendations.map((rec, rIdx) => (
|
||||
<Text style={{ fontSize: 9, marginBottom: 4, lineHeight: 1.3, color: '#1e3a8a' }} key={rIdx}>
|
||||
• {rec}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.footer}>
|
||||
Gerado por BrainWind v1.0 — Tecnologia Auditada em Engenharia Estrutural (NBR 6123:2023)
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface SavedProject {
|
||||
id?: number;
|
||||
name: string;
|
||||
type: 'projeto' | 'template';
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower';
|
||||
inputs: Record<string, unknown>;
|
||||
windData?: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
|
||||
@@ -10,7 +10,9 @@ import { calculateCircleBarForce, reynoldsBar } from '@/lib/nbr-tables/table-27'
|
||||
import Bar3DViewer from '@/components/three/Bar3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const BarSelectorModule: React.FC = () => {
|
||||
@@ -88,11 +90,18 @@ const BarSelectorModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Barra Prismática</CardTitle>
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Barras Individuais</CardTitle>
|
||||
<CardDescription>Sec. 8.1 — faces planas (Tab. 26) ou circulares (Tab. 27).</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="bar"
|
||||
inputs={{ barType, section, alpha, width, length, diameter }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Barra</label>
|
||||
<Select value={barType} onValueChange={(v) => setBarType(v as 'flat' | 'circular')}>
|
||||
|
||||
@@ -13,6 +13,7 @@ import Bridge3DViewer from '@/components/three/Bridge3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
|
||||
@@ -115,11 +116,18 @@ const BridgeModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Tabuleiro de Ponte</CardTitle>
|
||||
<CardDescription>Sec. 11.2 / 11.3 — análise estática.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="bridge"
|
||||
inputs={{ lp, width, deckHeight, heg, mass, fv, vk, alpha }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Maior vão (Lₚ)</label><span className="font-mono text-sm">{lp} m</span></div>
|
||||
<Slider min={20} max={500} step={5} value={[lp]} onValueChange={(v) => setLp(v[0])} />
|
||||
@@ -158,10 +166,6 @@ const BridgeModule: React.FC = () => {
|
||||
<label className="text-sm font-medium">Velocidade Crítica (V_F) aerodinâmica (m/s)</label>
|
||||
<Input type="number" value={vf} onChange={(e) => setVf(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>Categoria:</span><span className="font-mono">{terrainCategory}</span></div>
|
||||
<div className="flex justify-between"><span>V₀:</span><span className="font-mono">{v0} m/s</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -271,10 +275,6 @@ const BridgeModule: React.FC = () => {
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="bridge"
|
||||
inputs={{ lp, width, deckHeight, heg, mass, fv, vk, alpha }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,12 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const CylinderModule: React.FC = () => {
|
||||
const { v0, terrainCategory, structureClass, s2, vk, q, cpi: globalCpi } = useWindStore();
|
||||
const { vk, cpi: globalCpi } = useWindStore();
|
||||
|
||||
const [diameter, setDiameter] = React.useState(8);
|
||||
const [height, setHeight] = React.useState(20);
|
||||
@@ -78,11 +80,18 @@ const CylinderModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Cilindro Vertical</CardTitle>
|
||||
<CardDescription>Sec. 6.2.1 — silos, reservatórios, chaminés.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="cilindro"
|
||||
inputs={{ diameter, height, surface, endType }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label className="text-sm font-medium">Diâmetro (d)</label>
|
||||
@@ -121,10 +130,6 @@ const CylinderModule: React.FC = () => {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>V₀:</span><span className="font-mono">{v0} m/s</span></div>
|
||||
<div className="flex justify-between"><span>S₂ (cat. {terrainCategory}, cl. {structureClass}):</span><span className="font-mono">{s2.toFixed(3)}</span></div>
|
||||
<div className="flex justify-between"><span>Vₖ:</span><span className="font-mono">{vk.toFixed(2)} m/s</span></div>
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
<div className="flex justify-between"><span>Reynolds Re:</span><span className="font-mono">{result.re.toLocaleString('pt-BR')}</span></div>
|
||||
<div className="flex justify-between"><span>h/d:</span><span className="font-mono">{result.hOverD.toFixed(2)}</span></div>
|
||||
<div className="flex justify-between"><span>Regime:</span><span className="font-mono">{result.supercritical ? 'supercrítico' : 'subcrítico'}</span></div>
|
||||
|
||||
@@ -12,6 +12,8 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const DomeModule: React.FC = () => {
|
||||
@@ -71,11 +73,18 @@ const DomeModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Cúpula</CardTitle>
|
||||
<CardDescription>Sec. 6.2.4 — silos, reservatórios, ginásios.</CardDescription>
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Cúpulas</CardTitle>
|
||||
<CardDescription>Sec. 6.2.5 — coberturas esféricas.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="dome"
|
||||
inputs={{ diameter, rise, wallHeight, type }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as DomeType)}>
|
||||
|
||||
@@ -29,6 +29,8 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
|
||||
const DynamicsModule: React.FC = () => {
|
||||
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
|
||||
@@ -134,11 +136,18 @@ const DynamicsModule: React.FC = () => {
|
||||
|
||||
<TabsContent value="edificio" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Resposta Dinâmica</CardTitle>
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Análise Dinâmica Contínua</CardTitle>
|
||||
<CardDescription>Sec. 9.3 / Tab. 31 e 32</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="dynamics"
|
||||
inputs={{ structureType, height, freq, uMax, useType, sectionShape, sectionRatio, sectionSize, massEq }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Estrutura</label>
|
||||
<Select value={structureType} onValueChange={(v) => setStructureType(v as StructureDynamicType)}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
|
||||
const GalpaoModule: React.FC = () => {
|
||||
@@ -55,11 +56,18 @@ const GalpaoModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Geometria</CardTitle>
|
||||
<CardDescription>Ajuste as dimensões do galpão.</CardDescription>
|
||||
<CardDescription>Tabelas 4 e 5 — NBR 6123.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="galpao"
|
||||
inputs={{ width, length, height, roofPitch }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<Tooltip>
|
||||
@@ -180,10 +188,6 @@ const GalpaoModule: React.FC = () => {
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="galpao"
|
||||
inputs={{ width, length, height, roofPitch }}
|
||||
/>
|
||||
<ExportMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const IsolatedRoofModule: React.FC = () => {
|
||||
@@ -116,11 +118,18 @@ const IsolatedRoofModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Cobertura Isolada</CardTitle>
|
||||
<CardDescription>Sec. 7.2 — sobre suportes de reduzidas dimensões.</CardDescription>
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Coberturas Isoladas</CardTitle>
|
||||
<CardDescription>Sec. 6.2.2 — coberturas de uma ou duas águas.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="isolated-roof"
|
||||
inputs={{ type, theta, height, width, depth }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
@@ -11,11 +11,12 @@ import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack';
|
||||
import { Piperack3D } from '@/components/three/Piperack3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
import { Plus, Trash2, Layers, Info } from 'lucide-react';
|
||||
import { Plus, Trash2, Info } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
|
||||
const PiperackModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
@@ -110,23 +111,23 @@ const PiperackModule: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-3.5rem)] md:h-screen bg-background">
|
||||
<header className="shrink-0 flex items-center px-6 py-4 border-b bg-card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Layers className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold tracking-tight">Pipe-Racks Industriais</h1>
|
||||
<p className="text-sm text-muted-foreground">Vento transversal (Sec. 7 - NBR 6123)</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col lg:flex-row p-4 gap-4 overflow-hidden bg-muted/30">
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
|
||||
{/* Coluna Esquerda: Entradas */}
|
||||
<div className="w-full lg:w-80 shrink-0 overflow-y-auto bg-card rounded-xl border border-border shadow-sm p-5 space-y-6">
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Pipe-Racks Industriais</CardTitle>
|
||||
<CardDescription>Vento transversal (Sec. 7 - NBR 6123)</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="piperack"
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div>
|
||||
<h3 className="font-medium mb-4">Geometria da Estrutura</h3>
|
||||
<div className="space-y-5">
|
||||
@@ -239,6 +240,8 @@ const PiperackModule: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
@@ -422,10 +425,6 @@ const PiperackModule: React.FC = () => {
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="piperack"
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,7 +432,6 @@ const PiperackModule: React.FC = () => {
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ const SettingsModule: React.FC = () => {
|
||||
const setPendingLoad = useHydrationStore((s) => s.setPendingLoad);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const logoInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const { companyLogo, setCompanyLogo } = useWindStore();
|
||||
|
||||
const exportSnapshot = () => {
|
||||
const state = useWindStore.getState();
|
||||
@@ -148,6 +150,58 @@ const SettingsModule: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Logotipo da Empresa</CardTitle>
|
||||
<CardDescription>Insira o logotipo da sua empresa para aparecer no cabeçalho esquerdo dos relatórios PDF exportados.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-6">
|
||||
<div className="w-40 h-24 border-2 border-dashed border-border rounded-lg flex items-center justify-center bg-muted/20 overflow-hidden relative">
|
||||
{companyLogo ? (
|
||||
<img src={companyLogo} alt="Logo da Empresa" className="w-full h-full object-contain p-2" />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground text-center p-2">Sem logotipo</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full sm:w-auto">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => logoInputRef.current?.click()} className="text-xs">
|
||||
<Upload className="w-3.5 h-3.5 mr-1.5" />
|
||||
Carregar Logo
|
||||
</Button>
|
||||
{companyLogo && (
|
||||
<Button variant="destructive" size="sm" onClick={() => setCompanyLogo(null)} className="text-xs">
|
||||
<Trash2 className="w-3.5 h-3.5 mr-1.5" />
|
||||
Excluir
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Formatos aceitos: PNG, JPG, SVG. Tamanho recomendado: proporção retangular (ex: 4:3 ou 16:9).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref={logoInputRef}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (typeof event.target?.result === 'string') {
|
||||
setCompanyLogo(event.target.result);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_about')}</CardTitle>
|
||||
|
||||
@@ -11,6 +11,8 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const SignModule: React.FC = () => {
|
||||
@@ -68,11 +70,18 @@ const SignModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Muros e Placas</CardTitle>
|
||||
<CardDescription>Sec. 7.1 — vento perpendicular à face.</CardDescription>
|
||||
<CardDescription>Sec. 6.3 — retangulares ao nível do solo.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="sign"
|
||||
inputs={{ length, height, alpha, hasEndPlates, groundClearance }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Comprimento (ℓ)</label><span className="font-mono text-sm">{length} m</span></div>
|
||||
<Slider min={2} max={30} step={0.5} value={[length]} onValueChange={(v) => setLength(v[0])} />
|
||||
|
||||
@@ -9,6 +9,8 @@ import Tower3DViewer from '@/components/three/Tower3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const TowerModule: React.FC = () => {
|
||||
@@ -77,11 +79,18 @@ const TowerModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Torre Reticulada</CardTitle>
|
||||
<CardDescription>Sec. 8.5 — faces planas ou circulares.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="tower"
|
||||
inputs={{ section, barType, baseWidth, height, panels, phi, alphaWind }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Seção</label>
|
||||
<Select value={section} onValueChange={(v) => setSection(v as TowerSection)}>
|
||||
|
||||
@@ -12,6 +12,8 @@ import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const VaultModule: React.FC = () => {
|
||||
@@ -70,11 +72,18 @@ const VaultModule: React.FC = () => {
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Abóbada Cilíndrica</CardTitle>
|
||||
<CardDescription>Sec. 6.2.3 — galpões curvos, igrejas.</CardDescription>
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Abóbadas</CardTitle>
|
||||
<CardDescription>Sec. 6.2.4 — coberturas cilíndricas.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="vault"
|
||||
inputs={{ span, length, rise, regime, localCpi, viewDirection }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<WindParametersSummary />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Vão (ℓ)</label><span className="font-mono text-sm">{span} m</span></div>
|
||||
<Slider min={5} max={60} step={1} value={[span]} onValueChange={(v) => setSpan(v[0])} />
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface GlobalWindState {
|
||||
terrainCategory: TerrainCategory;
|
||||
largestDimension: number;
|
||||
heightZ: number;
|
||||
locality: string | null;
|
||||
|
||||
// Saídas (Calculados)
|
||||
structureClass: StructureClass;
|
||||
@@ -31,6 +32,7 @@ export interface GlobalWindState {
|
||||
// Ações
|
||||
updateCalculations: () => void;
|
||||
setV0: (val: number) => void;
|
||||
setLocality: (loc: string | null) => void;
|
||||
setS1: (val: number) => void;
|
||||
setS3: (val: number) => void;
|
||||
setS3Group: (group: 1 | 2 | 3 | 4 | 5) => void;
|
||||
@@ -40,6 +42,8 @@ export interface GlobalWindState {
|
||||
setPermeabilityCase: (c: PermeabilityCase) => void;
|
||||
setCpiRatio: (r: number) => void;
|
||||
setCpiManual: (c: number) => void;
|
||||
companyLogo: string | null;
|
||||
setCompanyLogo: (logo: string | null) => void;
|
||||
}
|
||||
|
||||
function calcCpi(permeabilityCase: PermeabilityCase, ratio: number, windAngle: 0 | 90): number {
|
||||
@@ -79,6 +83,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
||||
terrainCategory: 'II',
|
||||
largestDimension: 30,
|
||||
heightZ: 10,
|
||||
locality: null,
|
||||
|
||||
structureClass: 'B',
|
||||
s2: 1.06,
|
||||
@@ -90,12 +95,25 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
||||
cpiRatio: 1.0,
|
||||
cpi: calcCpi('four-equally-permeable', 1.0, 0),
|
||||
|
||||
companyLogo: typeof window !== 'undefined' ? localStorage.getItem('ventoapp-company-logo') : null,
|
||||
setCompanyLogo: (logo) => {
|
||||
set({ companyLogo: logo });
|
||||
if (logo) {
|
||||
localStorage.setItem('ventoapp-company-logo', logo);
|
||||
} else {
|
||||
localStorage.removeItem('ventoapp-company-logo');
|
||||
}
|
||||
},
|
||||
|
||||
updateCalculations: () => set((state) => recalc(state)),
|
||||
|
||||
setV0: (val) => {
|
||||
set({ v0: val });
|
||||
set({ v0: val, locality: null });
|
||||
get().updateCalculations();
|
||||
},
|
||||
setLocality: (loc) => {
|
||||
set({ locality: loc });
|
||||
},
|
||||
setS1: (val) => {
|
||||
set({ s1: val });
|
||||
get().updateCalculations();
|
||||
|
||||
Reference in New Issue
Block a user