🚀 Auto-deploy: BrainWind atualizado em 15/07/2026 11:12:30

This commit is contained in:
2026-07-15 11:12:30 +00:00
parent 0755a56702
commit 212dde0e5d
13 changed files with 1889 additions and 87 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

+5 -2
View File
@@ -23,6 +23,7 @@ import { ThemeProvider, useTheme } from '@/lib/theme';
import { useI18n } from './store/i18nStore'; import { useI18n } from './store/i18nStore';
import { GlobalWindSettingsModal } from './components/GlobalWindSettingsModal'; import { GlobalWindSettingsModal } from './components/GlobalWindSettingsModal';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { TermsOfUseDialog } from './components/TermsOfUseDialog';
// Ícones Customizados de Engenharia Premium // Ícones Customizados de Engenharia Premium
const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => ( const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg <svg
@@ -97,7 +98,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
)} )}
> >
<div className="h-14 flex items-center justify-between px-4 border-b"> <div className="h-14 flex items-center justify-between px-4 border-b">
{!isCollapsed && <span className="font-bold text-primary truncate tracking-tight">BrainWind</span>} {!isCollapsed && <img src={effectiveTheme === 'dark' ? '/logo_brainwind_dark.svg' : '/logo_brainwind.svg'} alt="BrainWind" className="h-7 w-auto object-contain" />}
<div className="flex items-center gap-1 shrink-0 ml-auto"> <div className="flex items-center gap-1 shrink-0 ml-auto">
<Button <Button
variant="ghost" variant="ghost"
@@ -201,12 +202,14 @@ function AppLayout({ children }: { children: React.ReactNode }) {
<BookOpen className="w-5 h-5 shrink-0" /> <BookOpen className="w-5 h-5 shrink-0" />
{!isCollapsed && <span>Glossário</span>} {!isCollapsed && <span>Glossário</span>}
</Link> </Link>
<TermsOfUseDialog isCollapsed={isCollapsed} />
</div> </div>
</aside> </aside>
<main className="flex-1 flex flex-col h-full overflow-hidden pb-16 md:pb-0"> <main className="flex-1 flex flex-col h-full overflow-hidden pb-16 md:pb-0">
<header className="h-14 border-b bg-card flex items-center justify-between px-4 md:hidden"> <header className="h-14 border-b bg-card flex items-center justify-between px-4 md:hidden">
<span className="font-bold text-primary tracking-tight">BrainWind</span> <img src={effectiveTheme === 'dark' ? '/logo_brainwind_dark.svg' : '/logo_brainwind.svg'} alt="BrainWind" className="h-6 w-auto object-contain" />
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Button <Button
variant="ghost" variant="ghost"
+102 -3
View File
@@ -1,7 +1,16 @@
import React from 'react'; import React, { useState } from 'react';
import { FileText, Table, Box } from 'lucide-react'; import { FileText, Table, Box } from 'lucide-react';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { useI18n } from '../store/i18nStore'; import { useI18n } from '../store/i18nStore';
import { useWindStore } from '../store/appStore';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from './ui/dialog';
import { Input } from './ui/input';
import { exportGalpaoToCSV } from '../lib/export-csv'; import { exportGalpaoToCSV } from '../lib/export-csv';
import { exportGalpaoToPDF } from '../lib/export-pdf'; import { exportGalpaoToPDF } from '../lib/export-pdf';
import { exportGalpaoToFtool } from '../lib/export-ftool'; import { exportGalpaoToFtool } from '../lib/export-ftool';
@@ -20,8 +29,37 @@ const ExportMenu: React.FC<ExportMenuProps> = ({ onExportCSV, onExportPDF, onExp
// Exibir o Ftool apenas se explicitamente fornecido, ou se for a configuração padrão (Galpão) // Exibir o Ftool apenas se explicitamente fornecido, ou se for a configuração padrão (Galpão)
const isGalpao = !onExportCSV && !onExportPDF && !onExportFtool; const isGalpao = !onExportCSV && !onExportPDF && !onExportFtool;
const [isPdfDialogOpen, setIsPdfDialogOpen] = useState(false);
const windStore = useWindStore();
const [localMeta, setLocalMeta] = useState({
projectName: windStore.projectName,
clientName: windStore.clientName,
authorName: windStore.authorName,
projectRev: windStore.projectRev,
});
const openPdfDialog = () => {
setLocalMeta({
projectName: windStore.projectName,
clientName: windStore.clientName,
authorName: windStore.authorName,
projectRev: windStore.projectRev,
});
setIsPdfDialogOpen(true);
};
const confirmPdfExport = () => {
windStore.setProjectMetadata(localMeta);
setIsPdfDialogOpen(false);
// Give a small delay to ensure state updates before PDF rendering
setTimeout(() => {
handlePDF();
}, 100);
};
return ( return (
<div className="flex gap-2"> <>
<div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -35,7 +73,7 @@ const ExportMenu: React.FC<ExportMenuProps> = ({ onExportCSV, onExportPDF, onExp
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handlePDF} onClick={openPdfDialog}
className="text-purple-600 border-purple-200 hover:bg-purple-50 hover:text-purple-700" className="text-purple-600 border-purple-200 hover:bg-purple-50 hover:text-purple-700"
title={t('export_pdf')} title={t('export_pdf')}
> >
@@ -55,6 +93,67 @@ const ExportMenu: React.FC<ExportMenuProps> = ({ onExportCSV, onExportPDF, onExp
</Button> </Button>
)} )}
</div> </div>
<Dialog open={isPdfDialogOpen} onOpenChange={setIsPdfDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Gerar Relatório PDF</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="projectName" className="text-right text-sm font-medium">
Projeto
</label>
<Input
id="projectName"
value={localMeta.projectName}
onChange={(e) => setLocalMeta({ ...localMeta, projectName: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="clientName" className="text-right text-sm font-medium">
Cliente
</label>
<Input
id="clientName"
value={localMeta.clientName}
onChange={(e) => setLocalMeta({ ...localMeta, clientName: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="authorName" className="text-right text-sm font-medium">
Autor
</label>
<Input
id="authorName"
value={localMeta.authorName}
onChange={(e) => setLocalMeta({ ...localMeta, authorName: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="projectRev" className="text-right text-sm font-medium">
Revisão
</label>
<Input
id="projectRev"
value={localMeta.projectRev}
onChange={(e) => setLocalMeta({ ...localMeta, projectRev: e.target.value })}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button onClick={confirmPdfExport} className="bg-purple-600 hover:bg-purple-700 text-white">
<FileText className="w-4 h-4 mr-2" />
Exportar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
); );
}; };
+120
View File
@@ -0,0 +1,120 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Info, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useState } from 'react';
interface TermsOfUseDialogProps {
isCollapsed?: boolean;
}
export function TermsOfUseDialog({ isCollapsed = false }: TermsOfUseDialogProps) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<button
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
isCollapsed && 'justify-center px-0'
)}
title={isCollapsed ? 'Termos de Uso e Responsabilidade' : undefined}
>
<Info className="w-5 h-5 shrink-0" />
{!isCollapsed && <span>Termos de Uso</span>}
</button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col">
<DialogHeader className="px-6 py-4 border-b">
<DialogTitle className="text-xl flex items-center gap-2">
<Info className="w-5 h-5 text-primary" />
Termos de Uso e Responsabilidade
</DialogTitle>
</DialogHeader>
<div className="flex-1 px-6 py-4 overflow-y-auto">
<div className="space-y-6 text-sm leading-relaxed text-muted-foreground pb-6">
<div className="bg-primary/5 p-4 rounded-lg border border-primary/10">
<p className="font-medium text-foreground mb-1">Norma de Referência</p>
<p>
Os cálculos estruturais e parâmetros de vento gerados pelo BrainWind seguem as
diretrizes estabelecidas pela <strong>ABNT NBR 6123:2023</strong>
<em>Forças devidas ao vento em edificações</em>.
</p>
</div>
<p>
O <strong>BrainWind</strong> é uma ferramenta computacional projetada para auxiliar projetistas
no cálculo e na análise dos efeitos do vento em edificações. Por ser um aplicativo
de suporte técnico, sua utilização está estritamente sujeita às seguintes condições:
</p>
<div className="bg-destructive/10 p-4 rounded-lg border border-destructive/20 text-destructive-foreground">
<div className="flex gap-2 items-center font-bold mb-2 text-destructive">
<AlertTriangle className="w-5 h-5" />
Aviso de Responsabilidade Técnica
</div>
<p className="text-sm">
A checagem, validação e verificação de todos os resultados, relatórios, dados de
entrada e parâmetros gerados pelo BrainWind são de <strong>responsabilidade exclusiva
e integral do usuário (Engenheiro Responsável)</strong>.
<br /><br />
O aplicativo é estritamente uma ferramenta de auxílio e não substitui, sob
nenhuma circunstância, o julgamento técnico, a análise detalhada das normas e o
dimensionamento final da estrutura por parte de um profissional de engenharia
legalmente habilitado (com registro ativo no CREA/CAU).
</p>
</div>
<ul className="space-y-4 list-disc pl-5">
<li>
<strong className="text-foreground">Licenciamento e Uso Autorizado:</strong> O uso do
BrainWind é restrito aos detentores de licença válida e ativa, emitida diretamente
pelos desenvolvedores ou por seus distribuidores autorizados. É terminantemente
proibida a distribuição, reprodução, engenharia reversa, compartilhamento de credenciais
ou qualquer forma de redistribuição não autorizada do software ou de suas licenças.
</li>
<li>
<strong className="text-foreground">Privacidade de Dados (LGPD):</strong> Os dados pessoais,
credenciais de acesso e projetos informados ao BrainWind são armazenados em servidores
seguros e tratados em rigorosa conformidade com a Lei Geral de Proteção de Dados
(Lei 13.709/2018). Detalhes adicionais sobre coleta, retenção e direitos do titular
estão disponíveis em nossa Política de Privacidade integral.
</li>
<li>
<strong className="text-foreground">Isenção de Responsabilidade e Danos:</strong> Os
desenvolvedores, distribuidores e afiliados do BrainWind não se responsabilizam por
quaisquer danos materiais, morais, perdas financeiras, interrupção de negócios ou
falhas estruturais decorrentes de decisões de projeto tomadas com base nas
informações ou cálculos fornecidos pelo aplicativo.
</li>
<li>
<strong className="text-foreground">Suporte e Aprimoramento Contínuo:</strong> Como o
sistema passa por atualizações e revisões constantes para melhor alinhamento
normativo, solicitamos que dúvidas, sugestões de melhoria e eventuais reportes
de inconsistências algorítmicas sejam enviados diretamente para nossa equipe
técnica através do canal: <strong>suporte@brainwind.com.br</strong>.
</li>
</ul>
<div className="pt-4 mt-6 border-t text-xs text-muted-foreground/70">
<p>Última atualização: julho de 2026. Esta versão substitui todas as anteriores.</p>
</div>
</div>
</div>
<div className="p-4 border-t bg-muted/20 flex justify-end">
<Button onClick={() => setOpen(false)}>
Ciente e de Acordo
</Button>
</div>
</DialogContent>
</Dialog>
);
}
+8 -1
View File
@@ -33,7 +33,14 @@ export function exportGalpaoToCSV() {
['Direção do Vento (graus)', wind.windAngle.toString()], ['Direção do Vento (graus)', wind.windAngle.toString()],
[], [],
['--- Pressão Interna ---'], ['--- Pressão Interna ---'],
['Caso de Permeabilidade', wind.permeabilityCase], ['Caso de Permeabilidade', {
'two-opposite-permeable': 'Duas faces opostas permeáveis',
'four-equally-permeable': 'Quatro faces igualmente permeáveis',
'dominant-windward': 'Abertura dominante — barlavento',
'dominant-leeward': 'Abertura dominante — sotavento',
'dominant-lateral': 'Abertura dominante — lateral',
'airtight': 'Edificação estanque'
}[wind.permeabilityCase] || wind.permeabilityCase],
['Coeficiente Cpi', cpi.toFixed(2)], ['Coeficiente Cpi', cpi.toFixed(2)],
[], [],
['--- Coeficientes de Pressão (Cpe), Cpi e Pressão Líquida (kN/m2) ---'], ['--- Coeficientes de Pressão (Cpe), Cpi e Pressão Líquida (kN/m2) ---'],
+74 -33
View File
@@ -1,4 +1,4 @@
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font, Svg, Path } from '@react-pdf/renderer'; import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font } from '@react-pdf/renderer';
import { useWindStore } from '../store/appStore'; import { useWindStore } from '../store/appStore';
import { useCaptureStore } from '../store/captureStore'; import { useCaptureStore } from '../store/captureStore';
@@ -6,9 +6,9 @@ import { useCaptureStore } from '../store/captureStore';
Font.register({ Font.register({
family: 'Roboto', family: 'Roboto',
fonts: [ fonts: [
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.ttf' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu4mxMyIFYW8YR8w.ttf', fontWeight: 'bold' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf', fontWeight: 'bold' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzc.ttf', fontStyle: 'italic' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Italic.ttf', fontStyle: 'italic' },
], ],
}); });
@@ -33,8 +33,17 @@ const styles = StyleSheet.create({
sceneCaption: { fontSize: 7, color: '#64748b', fontStyle: 'italic', textAlign: 'center', marginBottom: 6 }, 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 }, 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 }, recCard: { marginTop: 8, padding: 8, backgroundColor: '#faf5ff', borderLeft: '2.5pt solid #a855f7', borderRadius: 2 },
signatureBlock: { marginTop: 40, flexDirection: 'row', justifyContent: 'space-around', paddingTop: 30 },
signatureLine: { width: 200, borderTop: '0.75pt solid #334155', paddingTop: 4, alignItems: 'center' },
signatureText: { fontSize: 9, color: '#334155' },
}); });
export function getEffortColor(val: string | number): string {
const num = typeof val === 'number' ? val : parseFloat(val.toString().replace(',', '.').replace(/[^\d.-]/g, ''));
if (isNaN(num) || num === 0) return '#1e293b';
return num < 0 ? '#ea580c' : '#2563eb'; // orange for negative (suction), blue for positive (compression)
}
export function appendKgf(val: string | number): string { export function appendKgf(val: string | number): string {
if (typeof val === 'number') return String(val); if (typeof val === 'number') return String(val);
return val.replace(/(-?\d+(?:[.,]\d+)?)\s*(kN(?:\/m²|\/m2|\/m|·m|\.m|m)?)/g, (match, numStr, unit) => { return val.replace(/(-?\d+(?:[.,]\d+)?)\s*(kN(?:\/m²|\/m2|\/m|·m|\.m|m)?)/g, (match, numStr, unit) => {
@@ -136,32 +145,53 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
</View> </View>
)} )}
<View style={{ flex: 1, marginLeft: 15 }}> <View style={{ flex: 1, marginLeft: 15 }}>
<Text style={styles.title}>BrainWind v1.0 Memória de Cálculo</Text> <Text style={styles.title}>{wind.projectName || 'BrainWind v1.0 — Memória de Cálculo'}</Text>
<Text style={styles.subtitle}>Módulo: {moduleName} ABNT NBR 6123:2023</Text> <Text style={styles.subtitle}>Módulo: {moduleName} ABNT NBR 6123:2023</Text>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 12, backgroundColor: '#f8fafc', padding: 6, borderRadius: 2 }}>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Cliente</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.clientName || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Autor</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.authorName || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Revisão</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.projectRev || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Data</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{new Date().toLocaleDateString('pt-BR')}</Text>
</View>
</View>
{/* 1. Parâmetros Globais do Vento */} {/* 1. Parâmetros Globais do Vento */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>1. Parâmetros Globais do Vento e Pressão Dinâmica</Text> <Text style={styles.sectionTitle}>1. Parâmetros Globais do Vento e Pressão Dinâmica</Text>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Localidade de Projeto:</Text> <Text style={styles.label}>Localidade de Projeto:</Text>
<Text style={styles.value}>{wind.locality || 'V generalizado'}</Text> <Text style={styles.value}>{wind.locality || 'V0 generalizado'}</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Velocidade Básica (V):</Text> <Text style={styles.label}>Velocidade Básica (V0):</Text>
<Text style={styles.value}>{wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123)</Text> <Text style={styles.value}>{wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123)</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator Topográfico (S):</Text> <Text style={styles.label}>Fator Topográfico (S1):</Text>
<Text style={styles.value}>{wind.s1} (conforme Seção 5.2)</Text> <Text style={styles.value}>{wind.s1} (conforme Seção 5.2)</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator de Rugosidade (S):</Text> <Text style={styles.label}>Fator de Rugosidade (S2):</Text>
<Text style={styles.value}>{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)</Text> <Text style={styles.value}>
{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)
</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator Estatístico (S):</Text> <Text style={styles.label}>Fator Estatístico (S3):</Text>
<Text style={styles.value}>{wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4)</Text> <Text style={styles.value}>{wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4)</Text>
</View> </View>
@@ -169,17 +199,17 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
<Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 3, color: '#1e1b4b' }}> <Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 3, color: '#1e1b4b' }}>
Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3): Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3):
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}> <Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
Vₖ = V × S × S × S Fórmula Base: Vk = V0 × S1 × S2 × S3
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 6, color: '#475569' }}> <Text style={{ fontSize: 8, color: '#475569', marginBottom: 6 }}>
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s Vk = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}> <Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
q = 0,613 × (Vₖ)² Pressão Dinâmica: q = 0,613 × (Vk)²
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', color: '#475569' }}> <Text style={{ fontSize: 8, color: '#475569' }}>
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)} q = 0,613 × ({wind.vk.toFixed(2)})² = {(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²
</Text> </Text>
</View> </View>
</View> </View>
@@ -208,7 +238,9 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}> <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{sec.gridItems.map((item, i) => ( {sec.gridItems.map((item, i) => (
<View style={{ width: '50%', marginBottom: 4 }} key={i}> <View style={{ width: '50%', marginBottom: 4 }} key={i}>
<Text style={{ fontWeight: 'bold', color: '#475569' }}>{item.label}:</Text> <Text style={{ fontWeight: 'bold', color: '#475569' }}>
{item.label.replace('⊥', 'perpendicular à').replace('∥', 'paralelo à')}:
</Text>
<Text style={{ color: '#1e293b' }}>{appendKgf(item.value)}</Text> <Text style={{ color: '#1e293b' }}>{appendKgf(item.value)}</Text>
</View> </View>
))} ))}
@@ -227,7 +259,7 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
{sec.tableRows.map((tr, rIdx) => ( {sec.tableRows.map((tr, rIdx) => (
<View style={styles.tableRow} key={rIdx}> <View style={styles.tableRow} key={rIdx}>
{tr.map((tc, cIdx) => ( {tr.map((tc, cIdx) => (
<Text key={cIdx} style={cIdx === 0 ? styles.tableCellFirst : styles.tableCell}> <Text key={cIdx} style={[cIdx === 0 ? styles.tableCellFirst : styles.tableCell, cIdx > 0 ? { color: getEffortColor(tc) } : {}]}>
{appendKgf(tc)} {appendKgf(tc)}
</Text> </Text>
))} ))}
@@ -238,7 +270,6 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
</View> </View>
))} ))}
{/* 6. Parecer Técnico e Considerações */}
<View style={styles.section} wrap={false}> <View style={styles.section} wrap={false}>
<Text style={styles.sectionTitle}> <Text style={styles.sectionTitle}>
{sceneImage ? sections.length + 3 : sections.length + 2}. Parecer Técnico e Observações {sceneImage ? sections.length + 3 : sections.length + 2}. Parecer Técnico e Observações
@@ -250,28 +281,38 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
</Text> </Text>
))} ))}
</View> </View>
<View style={styles.signatureBlock}>
<View style={styles.signatureLine}>
<Text style={styles.signatureText}>{wind.authorName || 'Engenheiro Responsável'}</Text>
<Text style={{ fontSize: 7, color: '#64748b', marginTop: 2 }}>CREA / CAU:</Text>
</View>
</View>
<View style={{ marginTop: 20, paddingTop: 10, borderTop: '0.5pt solid #e2e8f0' }}>
<Text style={{ fontSize: 6, color: '#94a3b8', textAlign: 'justify', lineHeight: 1.3 }}>
Isenção de Responsabilidade do Software: Os resultados e relatórios gerados pelo BrainWind são de responsabilidade exclusiva e integral do usuário. O aplicativo atua como ferramenta de auxílio e não substitui o julgamento técnico, a análise detalhada das normas e o dimensionamento final por profissional habilitado.
</Text>
</View>
</View> </View>
{/* Rodapé fixo */}
<View style={styles.footer} fixed> <View style={styles.footer} fixed>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 6.5, color: '#94a3b8', flex: 1, textAlign: 'left' }}> <View style={{ flex: 1 }}>
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 style={{ fontSize: 6, color: '#94a3b8' }}>
</Text> Legendas: V0: Vel. básica | S1, S2, S3: Fatores de vento | Vk: Vel. de projeto | q: Pressão dinâmica | Cpe: Pressão externa | Cpi: Pressão interna
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'center', marginHorizontal: 10 }} render={({ pageNumber, totalPages }) => ( </Text>
</View>
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'right', flex: 0.2, paddingRight: 10 }} render={({ pageNumber, totalPages }) => (
`Pág. ${pageNumber} de ${totalPages}` `Pág. ${pageNumber} de ${totalPages}`
)} /> )} />
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', flex: 0.3 }}> <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 }}> <PdfImage src="/logo_brainwind.png" style={{ width: 25, height: 'auto', objectFit: 'contain' }} />
<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> </View>
</View> </View>
</Page> </Page>
</Document> </Document>
); );
}; };
+138 -48
View File
@@ -1,4 +1,4 @@
import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font, Svg, Path } from '@react-pdf/renderer'; import { Document, Page, Text, View, StyleSheet, Image as PdfImage, pdf, Font } from '@react-pdf/renderer';
import { useGalpaoStore } from '../store/galpaoStore'; import { useGalpaoStore } from '../store/galpaoStore';
import { useWindStore } from '../store/appStore'; import { useWindStore } from '../store/appStore';
import { useCaptureStore } from '../store/captureStore'; import { useCaptureStore } from '../store/captureStore';
@@ -9,15 +9,15 @@ import {
getDragForce, getDragForce,
} from './line-loads'; } from './line-loads';
import { getWallCpeOfficial, getRoofCpeOfficial } from './coefficients'; import { getWallCpeOfficial, getRoofCpeOfficial } from './coefficients';
import { appendKgf } from './export-generic-pdf'; import { appendKgf, getEffortColor } from './export-generic-pdf';
// Registrar fonte Roboto com suporte Unicode completo (subscripts, grego, etc.) // Registrar fonte Roboto com suporte Unicode completo (subscripts, grego, etc.)
Font.register({ Font.register({
family: 'Roboto', family: 'Roboto',
fonts: [ fonts: [
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.ttf' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu4mxMyIFYW8YR8w.ttf', fontWeight: 'bold' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf', fontWeight: 'bold' },
{ src: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzc.ttf', fontStyle: 'italic' }, { src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Italic.ttf', fontStyle: 'italic' },
], ],
}); });
@@ -42,6 +42,9 @@ const styles = StyleSheet.create({
sceneCaption: { fontSize: 7, color: '#64748b', fontStyle: 'italic', textAlign: 'center', marginBottom: 6 }, 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 }, 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 }, recCard: { marginTop: 8, padding: 8, backgroundColor: '#eff6ff', borderLeft: '2.5pt solid #3b82f6', borderRadius: 2 },
signatureBlock: { marginTop: 40, flexDirection: 'row', justifyContent: 'space-around', paddingTop: 30 },
signatureLine: { width: 200, borderTop: '0.75pt solid #334155', paddingTop: 4, alignItems: 'center' },
signatureText: { fontSize: 9, color: '#334155' },
}); });
interface ReportProps { interface ReportProps {
@@ -53,21 +56,48 @@ interface ReportProps {
function generateGalpaoRecommendations(galpao: any, wind: any): string[] { function generateGalpaoRecommendations(galpao: any, wind: any): string[] {
const recs: string[] = []; const recs: string[] = [];
// Analisar sucção no telhado let maxSuction = 0;
// Cpi normalmente gera sucção extra se for positivo let maxSuctionZone = '';
let maxCompression = 0;
let maxCompressionZone = '';
const pressure = (cpe: number) => (wind.q * (cpe - wind.cpi));
[0, 90].forEach(angle => {
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);
Object.entries(wCpe).forEach(([zone, cpe]) => {
const p = pressure(cpe as number);
if (p < maxSuction) { maxSuction = p; maxSuctionZone = `Parede ${zone} (${angle}°)`; }
if (p > maxCompression) { maxCompression = p; maxCompressionZone = `Parede ${zone} (${angle}°)`; }
});
Object.entries(rCpe).forEach(([zone, cpe]) => {
const p = pressure(cpe as number);
if (p < maxSuction) { maxSuction = p; maxSuctionZone = `Telhado ${zone} (${angle}°)`; }
if (p > maxCompression) { maxCompression = p; maxCompressionZone = `Telhado ${zone} (${angle}°)`; }
});
});
if (wind.cpi > 0) { 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.`); 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) { } 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.`); 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); const ratio = galpao.height / Math.min(galpao.width, galpao.length);
if (ratio > 1.5) { 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."); 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 if (maxSuction < 0) {
recs.push(`Para este projeto, a pressão crítica de sucção (arrancamento) ocorre na zona ${maxSuctionZone} com valor de ${maxSuction.toFixed(3)} kN/m². Atenção especial à ancoragem e fixação das telhas nesta região.`);
}
if (maxCompression > 0) {
recs.push(`A pressão máxima de compressão ocorre na zona ${maxCompressionZone} com valor de ${maxCompression.toFixed(3)} kN/m².`);
}
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("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."); 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.");
@@ -97,32 +127,50 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</View> </View>
)} )}
<View style={{ flex: 1, marginLeft: 15 }}> <View style={{ flex: 1, marginLeft: 15 }}>
<Text style={styles.title}>BrainWind v1.0 Memória de Cálculo</Text> <Text style={styles.title}>{wind.projectName || 'BrainWind v1.0 — Memória de Cálculo'}</Text>
<Text style={styles.subtitle}>Galpão Retangular ABNT NBR 6123:2023</Text> <Text style={styles.subtitle}>Galpão Retangular ABNT NBR 6123:2023</Text>
</View> </View>
</View> </View>
{/* 1. Parâmetros Globais do Vento */} <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 12, backgroundColor: '#f8fafc', padding: 6, borderRadius: 2 }}>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Cliente</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.clientName || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Autor</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.authorName || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Revisão</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{wind.projectRev || '-'}</Text>
</View>
<View>
<Text style={{ fontSize: 8, color: '#64748b' }}>Data</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{new Date().toLocaleDateString('pt-BR')}</Text>
</View>
</View>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>1. Parâmetros Globais do Vento e Pressão Dinâmica</Text> <Text style={styles.sectionTitle}>1. Parâmetros Globais do Vento e Pressão Dinâmica</Text>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Localidade de Projeto:</Text> <Text style={styles.label}>Localidade de Projeto:</Text>
<Text style={styles.value}>{wind.locality || 'V generalizado'}</Text> <Text style={styles.value}>{wind.locality || 'V0 generalizado'}</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Velocidade Básica (V):</Text> <Text style={styles.label}>Velocidade Básica (V0):</Text>
<Text style={styles.value}>{wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123)</Text> <Text style={styles.value}>{wind.v0} m/s (conforme Figura 1 e Anexo C da NBR 6123)</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator Topográfico (S):</Text> <Text style={styles.label}>Fator Topográfico (S1):</Text>
<Text style={styles.value}>{wind.s1} (conforme Seção 5.2)</Text> <Text style={styles.value}>{wind.s1} (conforme Seção 5.2)</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator de Rugosidade (S):</Text> <Text style={styles.label}>Fator de Rugosidade (S2):</Text>
<Text style={styles.value}>{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)</Text> <Text style={styles.value}>{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Fator Estatístico (S):</Text> <Text style={styles.label}>Fator Estatístico (S3):</Text>
<Text style={styles.value}>{wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4)</Text> <Text style={styles.value}>{wind.s3.toFixed(2)} (Grupo {wind.s3Group}, conforme Tabela 4)</Text>
</View> </View>
@@ -130,22 +178,21 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
<Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 3, color: '#1e1b4b' }}> <Text style={{ fontSize: 10, fontWeight: 'bold', marginBottom: 3, color: '#1e1b4b' }}>
Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3): Formulações de Velocidade e Pressão (Sec. 4.2 e 4.3):
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}> <Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
Vₖ = V × S × S × S Fórmula Base: Vk = V0 × S1 × S2 × S3
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 6, color: '#475569' }}> <Text style={{ fontSize: 8, color: '#475569', marginBottom: 6 }}>
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s Vk = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}> <Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
q = 0,613 × (Vₖ)² Pressão Dinâmica: q = 0,613 × (Vk)²
</Text> </Text>
<Text style={{ fontSize: 9, fontFamily: 'Courier', color: '#475569' }}> <Text style={{ fontSize: 8, color: '#475569' }}>
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)} q = 0,613 × ({wind.vk.toFixed(2)})² = {wind.q.toFixed(3)} kN/m²
</Text> </Text>
</View> </View>
</View> </View>
{/* 2. Geometria do Galpão */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>2. Geometria do Galpão (Tabelas 4 e 5)</Text> <Text style={styles.sectionTitle}>2. Geometria do Galpão (Tabelas 4 e 5)</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}> <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
@@ -172,12 +219,20 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</View> </View>
</View> </View>
{/* 3. Pressão Interna */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>3. Pressão Interna (Sec. 6.3, Figura 2)</Text> <Text style={styles.sectionTitle}>3. Pressão Interna (Sec. 6.3, Figura 2)</Text>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Caso de Permeabilidade:</Text> <Text style={styles.label}>Caso de Permeabilidade:</Text>
<Text style={styles.value}>{wind.permeabilityCase}</Text> <Text style={styles.value}>
{{
'two-opposite-permeable': 'Duas faces opostas permeáveis',
'four-equally-permeable': 'Quatro faces igualmente permeáveis',
'dominant-windward': 'Abertura dominante — barlavento',
'dominant-leeward': 'Abertura dominante — sotavento',
'dominant-lateral': 'Abertura dominante — lateral',
'airtight': 'Edificação estanque'
}[wind.permeabilityCase] || wind.permeabilityCase}
</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Coeficiente Cpi:</Text> <Text style={styles.label}>Coeficiente Cpi:</Text>
@@ -185,7 +240,6 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</View> </View>
</View> </View>
{/* 4. Cenários de Vento */}
{[0, 90].map((angle, index) => { {[0, 90].map((angle, index) => {
const wCpe = getWallCpeOfficial(galpao.length, galpao.width, galpao.height, angle as 0 | 90); 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 rCpe = getRoofCpeOfficial(galpao.length, galpao.width, galpao.height, galpao.roofPitch, angle as 0 | 90);
@@ -202,10 +256,21 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</Text> </Text>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>{secBase}. Coeficientes e Pressões Localizadas (p = q × (Cpe Cpi))</Text> <Text style={styles.sectionTitle}>{secBase}. Coeficientes e Pressões Localizadas</Text>
<View style={[styles.mathCard, { marginBottom: 6 }]}>
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
Fórmula de Pressão Efetiva: p = q × (Cpe Cpi)
</Text>
<Text style={{ fontSize: 8, color: '#475569' }}>
Valores de Cpe extraídos da Tabela 4 (Paredes) e Tabela 5 (Telhado Duas Águas) da NBR 6123:2023.
</Text>
</View>
<View style={styles.table}> <View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}> <View style={[styles.tableRow, styles.tableHeader]}>
<Text style={styles.tableCellFirst}>Elemento / Região</Text> <Text style={styles.tableCellFirst}>Elemento / Região</Text>
<Text style={styles.tableCell}>Cpe</Text> <Text style={styles.tableCell}>Cpe</Text>
<Text style={styles.tableCell}>Cpi</Text> <Text style={styles.tableCell}>Cpi</Text>
<Text style={styles.tableCell}>p [kN/m²]</Text> <Text style={styles.tableCell}>p [kN/m²]</Text>
@@ -215,7 +280,9 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
<Text style={styles.tableCellFirst}>Parede {face}</Text> <Text style={styles.tableCellFirst}>Parede {face}</Text>
<Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text> <Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text>
<Text style={styles.tableCell}>{cpi}</Text> <Text style={styles.tableCell}>{cpi}</Text>
<Text style={styles.tableCell}>{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}</Text> <Text style={[styles.tableCell, { color: getEffortColor(pressure(cpeVal as number)) }]}>
{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}
</Text>
</View> </View>
))} ))}
{Object.entries(rCpe).map(([face, cpeVal]) => ( {Object.entries(rCpe).map(([face, cpeVal]) => (
@@ -223,19 +290,30 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
<Text style={styles.tableCellFirst}>Telhado {face}</Text> <Text style={styles.tableCellFirst}>Telhado {face}</Text>
<Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text> <Text style={styles.tableCell}>{(cpeVal as number).toFixed(2)}</Text>
<Text style={styles.tableCell}>{cpi}</Text> <Text style={styles.tableCell}>{cpi}</Text>
<Text style={styles.tableCell}>{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}</Text> <Text style={[styles.tableCell, { color: getEffortColor(pressure(cpeVal as number)) }]}>
{appendKgf(`${pressure(cpeVal as number)} kN/m²`)}
</Text>
</View> </View>
))} ))}
</View> </View>
<Text style={{ fontSize: 7, color: '#64748b', marginTop: 4, fontStyle: 'italic' }}>
Nota: Valores avermelhados indicam sucção (pressão apontando para fora da superfície). Valores azuis indicam compressão (pressão empurrando a superfície).
</Text>
</View> </View>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> <Text style={styles.sectionTitle}>
{secBase + 1}. Cargas Lineares por Pórtico e Terças {secBase + 1}. Cargas Lineares por Pórtico e Terças
</Text> </Text>
<Text style={{ fontSize: 8, color: '#64748b', marginBottom: 4 }}>
Espaçamento dos Pórticos: {FRAME_SPACING} m | Terças: {PURLIN_SPACING} m <View style={[styles.mathCard, { marginBottom: 6, borderLeftColor: '#0ea5e9', backgroundColor: '#f0f9ff' }]}>
</Text> <Text style={{ fontSize: 9, fontWeight: 'bold', color: '#0c4a6e', marginBottom: 2 }}>
Fórmula de Carga Linear: w = p × espaçamento
</Text>
<Text style={{ fontSize: 8, color: '#0369a1' }}>
Espaçamento dos Pórticos: {FRAME_SPACING} m | Terças: {PURLIN_SPACING} m
</Text>
</View>
<View style={styles.table}> <View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}> <View style={[styles.tableRow, styles.tableHeader]}>
@@ -252,7 +330,9 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
<View style={styles.tableRow} key={`col-${label}`}> <View style={styles.tableRow} key={`col-${label}`}>
<Text style={styles.tableCellFirst}>{label}</Text> <Text style={styles.tableCellFirst}>{label}</Text>
<Text style={styles.tableCell}>{cpeVal.toFixed(2)}</Text> <Text style={styles.tableCell}>{cpeVal.toFixed(2)}</Text>
<Text style={styles.tableCell}>{appendKgf(`${fmtSigned(w)} kN/m`)}</Text> <Text style={[styles.tableCell, { color: getEffortColor(w) }]}>
{appendKgf(`${fmtSigned(w)} kN/m`)}
</Text>
</View> </View>
))} ))}
</View> </View>
@@ -267,7 +347,9 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
<View style={styles.tableRow} key={`roof-${z}`}> <View style={styles.tableRow} key={`roof-${z}`}>
<Text style={styles.tableCellFirst}>{z}</Text> <Text style={styles.tableCellFirst}>{z}</Text>
<Text style={styles.tableCell}>{rCpe[z].toFixed(2)}</Text> <Text style={styles.tableCell}>{rCpe[z].toFixed(2)}</Text>
<Text style={styles.tableCell}>{appendKgf(`${fmtSigned(rLoads[z])} kN/m`)}</Text> <Text style={[styles.tableCell, { color: getEffortColor(rLoads[z]) }]}>
{appendKgf(`${fmtSigned(rLoads[z])} kN/m`)}
</Text>
</View> </View>
))} ))}
</View> </View>
@@ -291,7 +373,6 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</View> </View>
)} )}
{/* Parecer Técnico */}
<View style={styles.section} wrap={false}> <View style={styles.section} wrap={false}>
<Text style={styles.sectionTitle}>9. Parecer Técnico e Recomendações</Text> <Text style={styles.sectionTitle}>9. Parecer Técnico e Recomendações</Text>
<View style={styles.recCard}> <View style={styles.recCard}>
@@ -301,24 +382,33 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
</Text> </Text>
))} ))}
</View> </View>
<View style={styles.signatureBlock}>
<View style={styles.signatureLine}>
<Text style={styles.signatureText}>{wind.authorName || 'Engenheiro Responsável'}</Text>
<Text style={{ fontSize: 7, color: '#64748b', marginTop: 2 }}>CREA / CAU:</Text>
</View>
</View>
<View style={{ marginTop: 20, paddingTop: 10, borderTop: '0.5pt solid #e2e8f0' }}>
<Text style={{ fontSize: 6, color: '#94a3b8', textAlign: 'justify', lineHeight: 1.3 }}>
Isenção de Responsabilidade do Software: Os resultados e relatórios gerados pelo BrainWind são de responsabilidade exclusiva e integral do usuário. O aplicativo atua como ferramenta de auxílio e não substitui o julgamento técnico, a análise detalhada das normas e o dimensionamento final por profissional habilitado.
</Text>
</View>
</View> </View>
{/* Rodapé fixo */}
<View style={styles.footer} fixed> <View style={styles.footer} fixed>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 6.5, color: '#94a3b8', flex: 1, textAlign: 'left' }}> <View style={{ flex: 1 }}>
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 style={{ fontSize: 6, color: '#94a3b8' }}>
</Text> Legendas: V0: Vel. básica | S1, S2, S3: Fatores de vento | Vk: Vel. de projeto | q: Pressão dinâmica | Cpe: Pressão externa | Cpi: Pressão interna
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'center', marginHorizontal: 10 }} render={({ pageNumber, totalPages }) => ( </Text>
</View>
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'right', flex: 0.2, paddingRight: 10 }} render={({ pageNumber, totalPages }) => (
`Pág. ${pageNumber} de ${totalPages}` `Pág. ${pageNumber} de ${totalPages}`
)} /> )} />
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', flex: 0.3 }}> <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 }}> <PdfImage src="/logo_brainwind.png" style={{ width: 25, height: 'auto', objectFit: 'contain' }} />
<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> </View>
</View> </View>
+13
View File
@@ -44,6 +44,13 @@ export interface GlobalWindState {
setCpiManual: (c: number) => void; setCpiManual: (c: number) => void;
companyLogo: string | null; companyLogo: string | null;
setCompanyLogo: (logo: string | null) => void; setCompanyLogo: (logo: string | null) => void;
// Metadados do Projeto
projectName: string;
clientName: string;
authorName: string;
projectRev: string;
setProjectMetadata: (data: Partial<{projectName: string, clientName: string, authorName: string, projectRev: string}>) => void;
} }
function calcCpi(permeabilityCase: PermeabilityCase, ratio: number, windAngle: 0 | 90): number { function calcCpi(permeabilityCase: PermeabilityCase, ratio: number, windAngle: 0 | 90): number {
@@ -105,6 +112,12 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
} }
}, },
projectName: 'Projeto BrainWind',
clientName: 'Cliente Padrão',
authorName: 'Eng. Responsável',
projectRev: 'R0',
setProjectMetadata: (data) => set((state) => ({ ...state, ...data })),
updateCalculations: () => set((state) => recalc(state)), updateCalculations: () => set((state) => recalc(state)),
setV0: (val) => { setV0: (val) => {
+19
View File
@@ -2,6 +2,25 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import path from 'path' import path from 'path'
import fs from 'fs'
try {
const svgPath = path.resolve(__dirname, '../logotipo_brainwind color original3.svg')
if (fs.existsSync(svgPath)) {
let text = fs.readFileSync(svgPath, 'utf8')
text = text.replace(/([0-9]+\.[0-9]+)/g, (m) => parseFloat(m).toFixed(1).replace(/\.0$/, ''))
text = text.replace(/\s+/g, ' ')
fs.mkdirSync(path.resolve(__dirname, 'public'), { recursive: true })
fs.writeFileSync(path.resolve(__dirname, 'public/logo_brainwind.svg'), text)
fs.writeFileSync(path.resolve(__dirname, 'public/logo_brainwind_dark.svg'), text.replace(/#06315A/gi, '#E2E8F0'))
}
const pngPath = path.resolve(__dirname, '../logotipo_brainwind color original.png')
if (fs.existsSync(pngPath)) {
fs.mkdirSync(path.resolve(__dirname, 'public'), { recursive: true })
fs.copyFileSync(pngPath, path.resolve(__dirname, 'public/logo_brainwind.png'))
}
} catch(e) {}
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 507 KiB