🚀 Auto-deploy: BrainWind atualizado em 15/07/2026 11:12:30
This commit is contained in:
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
@@ -23,6 +23,7 @@ import { ThemeProvider, useTheme } from '@/lib/theme';
|
||||
import { useI18n } from './store/i18nStore';
|
||||
import { GlobalWindSettingsModal } from './components/GlobalWindSettingsModal';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { TermsOfUseDialog } from './components/TermsOfUseDialog';
|
||||
// Ícones Customizados de Engenharia Premium
|
||||
const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
@@ -97,7 +98,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -201,12 +202,14 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
<BookOpen className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Glossário</span>}
|
||||
</Link>
|
||||
|
||||
<TermsOfUseDialog isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FileText, Table, Box } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
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 { exportGalpaoToPDF } from '../lib/export-pdf';
|
||||
import { exportGalpaoToFtool } from '../lib/export-ftool';
|
||||
@@ -20,7 +29,36 @@ 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)
|
||||
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 (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -35,7 +73,7 @@ const ExportMenu: React.FC<ExportMenuProps> = ({ onExportCSV, onExportPDF, onExp
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePDF}
|
||||
onClick={openPdfDialog}
|
||||
className="text-purple-600 border-purple-200 hover:bg-purple-50 hover:text-purple-700"
|
||||
title={t('export_pdf')}
|
||||
>
|
||||
@@ -55,6 +93,67 @@ const ExportMenu: React.FC<ExportMenuProps> = ({ onExportCSV, onExportPDF, onExp
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 nº 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>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,14 @@ export function exportGalpaoToCSV() {
|
||||
['Direção do Vento (graus)', wind.windAngle.toString()],
|
||||
[],
|
||||
['--- 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)],
|
||||
[],
|
||||
['--- Coeficientes de Pressão (Cpe), Cpi e Pressão Líquida (kN/m2) ---'],
|
||||
|
||||
@@ -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 { useCaptureStore } from '../store/captureStore';
|
||||
|
||||
@@ -6,9 +6,9 @@ import { useCaptureStore } from '../store/captureStore';
|
||||
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' },
|
||||
{ src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf' },
|
||||
{ src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf', fontWeight: 'bold' },
|
||||
{ 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 },
|
||||
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 },
|
||||
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 {
|
||||
if (typeof val === 'number') return String(val);
|
||||
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 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>
|
||||
</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 */}
|
||||
<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>
|
||||
<Text style={styles.value}>{wind.locality || 'V0 generalizado'}</Text>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>Fator de Rugosidade (S₂):</Text>
|
||||
<Text style={styles.value}>{wind.s2.toFixed(3)} (Categoria {wind.terrainCategory}, Classe {wind.structureClass}, conforme Tabela 2)</Text>
|
||||
<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>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
@@ -169,17 +199,17 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
<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 style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
|
||||
Fórmula Base: Vk = V0 × S1 × S2 × S3
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 6, color: '#475569' }}>
|
||||
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
|
||||
<Text style={{ fontSize: 8, color: '#475569', marginBottom: 6 }}>
|
||||
Vk = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
|
||||
q = 0,613 × (Vₖ)²
|
||||
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
|
||||
Pressão Dinâmica: q = 0,613 × (Vk)²
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', color: '#475569' }}>
|
||||
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)}
|
||||
<Text style={{ fontSize: 8, color: '#475569' }}>
|
||||
q = 0,613 × ({wind.vk.toFixed(2)})² = {(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -208,7 +238,9 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
<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={{ fontWeight: 'bold', color: '#475569' }}>
|
||||
{item.label.replace('⊥', 'perpendicular à').replace('∥', 'paralelo à')}:
|
||||
</Text>
|
||||
<Text style={{ color: '#1e293b' }}>{appendKgf(item.value)}</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -227,7 +259,7 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
{sec.tableRows.map((tr, rIdx) => (
|
||||
<View style={styles.tableRow} key={rIdx}>
|
||||
{tr.map((tc, cIdx) => (
|
||||
<Text key={cIdx} style={cIdx === 0 ? styles.tableCellFirst : styles.tableCell}>
|
||||
<Text key={cIdx} style={[cIdx === 0 ? styles.tableCellFirst : styles.tableCell, cIdx > 0 ? { color: getEffortColor(tc) } : {}]}>
|
||||
{appendKgf(tc)}
|
||||
</Text>
|
||||
))}
|
||||
@@ -238,7 +270,6 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 6. Parecer Técnico e Considerações */}
|
||||
<View style={styles.section} wrap={false}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{sceneImage ? sections.length + 3 : sections.length + 2}. Parecer Técnico e Observações
|
||||
@@ -250,28 +281,38 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
||||
</Text>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* 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
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: 6, color: '#94a3b8' }}>
|
||||
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>
|
||||
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'center', marginHorizontal: 10 }} render={({ pageNumber, totalPages }) => (
|
||||
</View>
|
||||
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'right', flex: 0.2, paddingRight: 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>
|
||||
<PdfImage src="/logo_brainwind.png" style={{ width: 25, height: 'auto', objectFit: 'contain' }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
+135
-45
@@ -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 { useWindStore } from '../store/appStore';
|
||||
import { useCaptureStore } from '../store/captureStore';
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
getDragForce,
|
||||
} from './line-loads';
|
||||
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.)
|
||||
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' },
|
||||
{ src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf' },
|
||||
{ src: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf', fontWeight: 'bold' },
|
||||
{ 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 },
|
||||
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 },
|
||||
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 {
|
||||
@@ -53,21 +56,48 @@ interface ReportProps {
|
||||
function generateGalpaoRecommendations(galpao: any, wind: any): string[] {
|
||||
const recs: string[] = [];
|
||||
|
||||
// Analisar sucção no telhado
|
||||
// Cpi normalmente gera sucção extra se for positivo
|
||||
let maxSuction = 0;
|
||||
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) {
|
||||
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
|
||||
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("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 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>
|
||||
</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}>
|
||||
<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>
|
||||
<Text style={styles.value}>{wind.locality || 'V0 generalizado'}</Text>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
@@ -130,22 +178,21 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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 style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
|
||||
Fórmula Base: Vk = V0 × S1 × S2 × S3
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 6, color: '#475569' }}>
|
||||
Vₖ = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
|
||||
<Text style={{ fontSize: 8, color: '#475569', marginBottom: 6 }}>
|
||||
Vk = {wind.v0} × {wind.s1} × {wind.s2.toFixed(3)} × {wind.s3.toFixed(2)} = {wind.vk.toFixed(2)} m/s
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', marginBottom: 2 }}>
|
||||
q = 0,613 × (Vₖ)²
|
||||
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e1b4b', marginBottom: 2 }}>
|
||||
Pressão Dinâmica: q = 0,613 × (Vk)²
|
||||
</Text>
|
||||
<Text style={{ fontSize: 9, fontFamily: 'Courier', color: '#475569' }}>
|
||||
{appendKgf(`q = 0,613 × (${wind.vk.toFixed(2)})² = ${(0.613 * Math.pow(wind.vk, 2) / 1000).toFixed(4)} kN/m²`)}
|
||||
<Text style={{ fontSize: 8, color: '#475569' }}>
|
||||
q = 0,613 × ({wind.vk.toFixed(2)})² = {wind.q.toFixed(3)} 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={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
@@ -172,12 +219,20 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
</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}>
|
||||
<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 style={styles.row}>
|
||||
<Text style={styles.label}>Coeficiente Cpi:</Text>
|
||||
@@ -185,7 +240,6 @@ 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);
|
||||
@@ -202,10 +256,21 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
</Text>
|
||||
|
||||
<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.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²]</Text>
|
||||
@@ -215,7 +280,9 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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}>{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>
|
||||
))}
|
||||
{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.tableCell}>{(cpeVal as number).toFixed(2)}</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>
|
||||
<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 style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{secBase + 1}. Cargas Lineares por Pórtico e Terças
|
||||
</Text>
|
||||
<Text style={{ fontSize: 8, color: '#64748b', marginBottom: 4 }}>
|
||||
|
||||
<View style={[styles.mathCard, { marginBottom: 6, borderLeftColor: '#0ea5e9', backgroundColor: '#f0f9ff' }]}>
|
||||
<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.tableRow, styles.tableHeader]}>
|
||||
@@ -252,7 +330,9 @@ 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}>{appendKgf(`${fmtSigned(w)} kN/m`)}</Text>
|
||||
<Text style={[styles.tableCell, { color: getEffortColor(w) }]}>
|
||||
{appendKgf(`${fmtSigned(w)} kN/m`)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -267,7 +347,9 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
<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}>{appendKgf(`${fmtSigned(rLoads[z])} kN/m`)}</Text>
|
||||
<Text style={[styles.tableCell, { color: getEffortColor(rLoads[z]) }]}>
|
||||
{appendKgf(`${fmtSigned(rLoads[z])} kN/m`)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -291,7 +373,6 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 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}>
|
||||
@@ -301,24 +382,33 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
||||
</Text>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* 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
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: 6, color: '#94a3b8' }}>
|
||||
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>
|
||||
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'center', marginHorizontal: 10 }} render={({ pageNumber, totalPages }) => (
|
||||
</View>
|
||||
<Text style={{ fontSize: 7, color: '#64748b', textAlign: 'right', flex: 0.2, paddingRight: 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>
|
||||
<PdfImage src="/logo_brainwind.png" style={{ width: 25, height: 'auto', objectFit: 'contain' }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -44,6 +44,13 @@ export interface GlobalWindState {
|
||||
setCpiManual: (c: number) => void;
|
||||
companyLogo: string | null;
|
||||
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 {
|
||||
@@ -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)),
|
||||
|
||||
setV0: (val) => {
|
||||
|
||||
@@ -2,6 +2,25 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
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/
|
||||
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 |
Reference in New Issue
Block a user