🚀 Auto-deploy: BrainWind atualizado em 26/08/2026 10:57:31

This commit is contained in:
2026-08-26 10:57:31 +00:00
parent ddcb56d78b
commit b842d6009f
7 changed files with 204 additions and 9 deletions
@@ -42,6 +42,8 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
setTerrainCategory,
locality,
setLocality,
fviz,
setFviz,
} = useWindStore();
const [stationQuery, setStationQuery] = useState('');
@@ -77,6 +79,7 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
<TabsList className="flex w-full mb-4 bg-muted p-1 text-xs min-w-0">
<TabsTrigger value="norma" className="text-xs py-1 px-1.5 flex-1">NBR 6123</TabsTrigger>
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Localidade</TabsTrigger>
<TabsTrigger value="vizinhanca" className="text-xs py-1 px-1.5 flex-1">Vizinhança 3D</TabsTrigger>
</TabsList>
<TabsContent value="norma" className="space-y-4 min-w-0">
@@ -215,6 +218,21 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
</p>
</div>
</TabsContent>
<TabsContent value="vizinhanca" className="space-y-4 min-w-0">
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-foreground">Fator de Vizinhança (Fviz)</label>
<span className="text-xs text-muted-foreground font-mono">{fviz.toFixed(2)}</span>
</div>
<Slider min={0.7} max={1.3} step={0.01} value={[fviz]} onValueChange={(vals) => setFviz(vals[0])} className="py-1 cursor-pointer" />
<p className="text-[10px] text-muted-foreground">
Modifica diretamente a pressão dinâmica (q).<br/>
<b>&lt; 1.0</b>: Efeito de sombreamento (edifícios ao redor protegem a estrutura).<br/>
<b>&gt; 1.0</b>: Efeito funil ou fenda (edifícios ao redor aceleram o vento).
</p>
</div>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
+61 -1
View File
@@ -9,6 +9,8 @@ import {
getPillarBaseMoment,
getDragForce,
} from '../lib/line-loads';
import { calcELU, calcELS } from '../lib/combinations';
import type { LoadInputs } from '../lib/combinations';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
@@ -25,6 +27,10 @@ const LinearLoadsTable: React.FC = () => {
const [purlinSpacing, setPurlinSpacing] = useState<number>(1.5);
// Gravitational Loads (kN/m²)
const [pp, setPp] = useState<number>(0.15); // Peso Próprio típico
const [sc, setSc] = useState<number>(0.25); // Sobrecarga típica de cobertura
const columnLoads = useMemo(
() => getColumnLinearLoads(cpi, q, wallCpe, frameSpacing, windAngle),
[cpi, q, wallCpe, frameSpacing, windAngle],
@@ -92,10 +98,11 @@ const LinearLoadsTable: React.FC = () => {
<Separator />
<Tabs defaultValue="pilares" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="pilares">{t('linear_loads_tab_pillars')}</TabsTrigger>
<TabsTrigger value="tercas">{t('linear_loads_tab_purlins')}</TabsTrigger>
<TabsTrigger value="reacoes">{t('linear_loads_tab_reactions')}</TabsTrigger>
<TabsTrigger value="combinacoes" className="text-xs">Combinações</TabsTrigger>
</TabsList>
<TabsContent value="pilares" className="space-y-3">
@@ -202,6 +209,59 @@ const LinearLoadsTable: React.FC = () => {
{t('linear_loads_warning_simplified')}
</p>
</TabsContent>
<TabsContent value="combinacoes" className="space-y-4">
<div className="grid grid-cols-2 gap-4 bg-muted/20 p-3 rounded-md border">
<div className="space-y-1">
<label className="text-xs font-medium text-foreground">Peso Próprio (kN/m²)</label>
<Input type="number" step={0.05} value={pp} onChange={e => setPp(Number(e.target.value))} className="h-8" />
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-foreground">Sobrecarga (kN/m²)</label>
<Input type="number" step={0.05} value={sc} onChange={e => setSc(Number(e.target.value))} className="h-8" />
</div>
</div>
<div className="rounded-md border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/40">
<tr>
<th className="px-3 py-2 text-left font-medium">Elemento (Têrça)</th>
<th className="px-3 py-2 text-right font-medium">Vento (kN/m)</th>
<th className="px-3 py-2 text-right font-medium">ELU (kN/m)</th>
<th className="px-3 py-2 text-right font-medium text-muted-foreground">ELS (kN/m)</th>
</tr>
</thead>
<tbody>
{[
{ zona: 'E', w: roofLoads.E },
{ zona: 'F', w: roofLoads.F },
{ zona: 'G', w: roofLoads.G },
{ zona: 'H', w: roofLoads.H },
{ zona: 'I', w: roofLoads.I },
{ zona: 'J', w: roofLoads.J },
].map((row) => {
const inputs: LoadInputs = {
pp: pp * purlinSpacing, // linearizando a carga de área
sc: sc * purlinSpacing,
w: row.w
};
return (
<tr key={row.zona} className="border-t">
<td className="px-3 py-2 font-mono font-semibold">Zona {row.zona}</td>
<td className="px-3 py-2 text-right font-mono">{fmtSigned(row.w)}</td>
<td className="px-3 py-2 text-right font-mono font-bold text-primary">{fmtSigned(calcELU(inputs))}</td>
<td className="px-3 py-2 text-right font-mono text-muted-foreground">{fmtSigned(calcELS(inputs))}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
* Combinações simplificadas para NBR 8681. Assumido vento principal ou sobrecarga principal (pega o pior caso ELU Normal).
</p>
</TabsContent>
</Tabs>
<Separator />
+51
View File
@@ -0,0 +1,51 @@
/**
* Módulo de Combinações de Ações (NBR 8681 / NBR 6118)
* Simplificado para uso prático com o vento.
*/
export interface LoadInputs {
/** Peso Próprio (Permanente) em kN/m ou kN/m² */
pp: number;
/** Sobrecarga (Acidental) em kN/m ou kN/m² */
sc: number;
/** Carga de Vento (Acidental) em kN/m ou kN/m² */
w: number;
}
/**
* Combinação ELU (Estado Limite Último) Normal
* Vento Principal: 1.4*PP + 1.4*W + 1.4*0.5*SC
* Sobrecarga Principal: 1.4*PP + 1.4*SC + 1.4*0.6*W
*/
export function calcELU(loads: LoadInputs): number {
const { pp, sc, w } = loads;
const g = 1.4 * pp;
// Vento como ação variável principal
const comb1 = g + (1.4 * w) + (1.4 * 0.5 * sc);
// Sobrecarga como ação variável principal
const comb2 = g + (1.4 * sc) + (1.4 * 0.6 * w);
return Math.max(Math.abs(comb1), Math.abs(comb2)) * Math.sign(w || pp || 1); // preserva direção da dominante
}
/**
* Combinação ELU Fiel (Arrancamento)
* Peso Próprio é favorável (reduz a chance de voar).
* Vento é desfavorável (puxa para cima, negativo).
* PP = 1.0, W = 1.4
*/
export function calcELUArrancamento(pp: number, w: number): number {
// pp é positivo para baixo. w é negativo para cima (sucção).
return (1.0 * pp) + (1.4 * w);
}
/**
* Combinação ELS (Estado Limite de Serviço) - Frequente
* PP + 0.4*SC + 0.3*W
*/
export function calcELS(loads: LoadInputs): number {
const { pp, sc, w } = loads;
return pp + (0.4 * sc) + (0.3 * w);
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Utilitários para Análise Dinâmica Simplificada (NBR 6123 - Anexo Dinâmico)
*/
/**
* Retorna verdadeiro se a frequência natural (Hz) indica que a estrutura
* é excessivamente flexível e exige cálculo pelo modelo dinâmico rigoroso.
* Uma regra de ouro da engenharia de vento brasileira é que f1 < 1Hz
* requer verificação atenciosa aos efeitos dinâmicos (rajadas e vórtices).
*/
export function isResonantRisk(f1: number): boolean {
return f1 > 0 && f1 < 1.0;
}
+32 -2
View File
@@ -8,13 +8,14 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useWindStore } from '@/store/appStore';
import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack';
import { isResonantRisk } from '@/lib/dynamics';
import { Piperack3D } from '@/components/three/Piperack3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { WindParametersSummary } from '@/components/WindParametersSummary';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { Plus, Trash2, Info } from 'lucide-react';
import { Plus, Trash2, Info, AlertTriangle } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
import { Input } from '@/components/ui/input';
@@ -26,6 +27,7 @@ const PiperackModule: React.FC = () => {
const [spacing, setSpacing] = useState(2);
const [numFrames, setNumFrames] = useState(2);
const [phiStruct, setPhiStruct] = useState(0.5);
const [f1, setF1] = useState(1.5); // Frequência natural (Hz)
const [pipes, setPipes] = useState<PipeDef[]>([]);
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
@@ -41,6 +43,7 @@ const PiperackModule: React.FC = () => {
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
if (p.pipes !== undefined) setPipes(p.pipes);
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
if (p.f1 !== undefined) setF1(p.f1);
clearPendingLoad();
}
}, [pendingLoad, clearPendingLoad]);
@@ -157,7 +160,7 @@ const PiperackModule: React.FC = () => {
</div>
<SaveModuleDialog
moduleType="piperack"
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct, f1 }}
/>
</CardHeader>
<CardContent className="space-y-6">
@@ -219,6 +222,33 @@ const PiperackModule: React.FC = () => {
</div>
<Slider value={[phiStruct]} min={0.1} max={1.0} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Freq. Natural (f)</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p>Frequência fundamental no sentido do vento (Hz). Valores abaixo de 1 Hz indicam risco dinâmico elevado.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<span className="text-sm text-muted-foreground">{f1.toFixed(1)} Hz</span>
</div>
<Slider value={[f1]} min={0.1} max={5.0} step={0.1} onValueChange={v => setF1(v[0])} />
{isResonantRisk(f1) && (
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive flex gap-2 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<p>Estrutura flexível (f &lt; 1 Hz). O vento produzirá amplificação dinâmica (C.A.D) considerável. É exigida análise pela NBR 6123 Capítulo 9.</p>
</div>
)}
</div>
</div>
</div>
+21 -2
View File
@@ -19,7 +19,7 @@ import { EducationalManual } from '@/components/EducationalManual';
import { WindParametersSummary } from '@/components/WindParametersSummary';
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind } from 'lucide-react';
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
const conditionLabels: Record<ShelterCondition, string> = {
@@ -41,6 +41,7 @@ const ShelterModule: React.FC = () => {
const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
const [leftClosure, setLeftClosure] = useState(100);
const [rightClosure, setRightClosure] = useState(100);
const [surfaceMass, setSurfaceMass] = useState(15); // kg/m² para lona/telha leve
const [viewMode, setViewMode] = useState<'3d' | 'elevation' | 'airflow'>('3d');
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
@@ -101,6 +102,9 @@ const ShelterModule: React.FC = () => {
const effAreaBack = (width * height) * (backClosure / 100);
const effAreaSideTotal = (depth * height) * ((leftClosure + rightClosure) / 100);
const gravityForce = (surfaceMass * effAreaRoof * 9.81) / 1000; // in kN
const isUpliftCritical = envelope.criticalUp.value > gravityForce;
const handleExportPDF = () => {
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
const sections: GenericPDFSection[] = [
@@ -351,7 +355,7 @@ const ShelterModule: React.FC = () => {
</div>
<SaveModuleDialog
moduleType="shelter"
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle }}
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle, surfaceMass }}
/>
</CardHeader>
<CardContent className="space-y-5">
@@ -476,6 +480,21 @@ const ShelterModule: React.FC = () => {
</div>
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
</div>
<div className="space-y-2 pt-2 border-t">
<div className="flex justify-between text-xs">
<span>Massa Superficial (kg/m²):</span>
<span className="font-mono font-medium">{surfaceMass} kg/m²</span>
</div>
<Slider min={2} max={100} step={1} value={[surfaceMass]} onValueChange={(v) => setSurfaceMass(v[0])} />
<p className="text-[10px] text-muted-foreground mt-1">Lonas ~3kg/m², Telhas Metálicas ~10-15kg/m²</p>
{isUpliftCritical && (
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive flex gap-2 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<p>Alerta de Arrancamento: Força () {envelope.criticalUp.value.toFixed(1)} kN &gt; Peso ({gravityForce.toFixed(1)} kN). Exige ancoragem/estaiamento.</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
+8 -4
View File
@@ -16,6 +16,7 @@ export interface GlobalWindState {
largestDimension: number;
heightZ: number;
locality: string | null;
fviz: number; // Fator de vizinhança 3D (0.7 a 1.3)
// Saídas (Calculados)
structureClass: StructureClass;
@@ -42,6 +43,7 @@ export interface GlobalWindState {
setPermeabilityCase: (c: PermeabilityCase) => void;
setCpiRatio: (r: number) => void;
setCpiManual: (c: number) => void;
setFviz: (f: number) => void;
companyLogo: string | null;
setCompanyLogo: (logo: string | null) => void;
@@ -81,7 +83,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
structureClass: calc.structClass,
s2: calc.s2,
vk: calc.vk,
q: calc.q,
q: calc.q * (state.fviz ?? 1.0),
cpi: calcCpi(state.permeabilityCase, state.cpiRatio, state.windAngle),
};
};
@@ -95,6 +97,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
largestDimension: 30,
heightZ: 10,
locality: null,
fviz: 1.0,
structureClass: 'B',
s2: 1.06,
@@ -169,9 +172,10 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
},
setCpiManual: (c) => {
set({ cpi: clampCpi(c) });
// Not calling updateCalculations to avoid overriding it immediately if permeabilityCase applies,
// but if the user overrides manually we might want to switch permeabilityCase to 'custom' ?
// Wait, there is no 'custom'. We just set the value.
},
setFviz: (f) => {
set({ fviz: f });
get().updateCalculations();
},
};
});