🚀 Auto-deploy: BrainWind atualizado em 27/07/2026 23:01:47

This commit is contained in:
2026-07-27 23:01:47 +00:00
parent 812a180bd8
commit 3e289b5dde
6 changed files with 429 additions and 65 deletions
+12 -4
View File
@@ -5,7 +5,7 @@ import { Text, Grid, Environment } from '@react-three/drei';
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
import SceneCanvas from '../SceneCanvas';
import { useCanvasTheme } from '@/lib/theme';
import type { ShelterCondition, OpeningPosition } from '@/lib/nbr-tables/table-shelters';
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
export interface Shelter3DProps {
condition: ShelterCondition;
@@ -18,6 +18,7 @@ export interface Shelter3DProps {
rightClosure: number;
openingPos: OpeningPosition;
backRange?: [number, number];
windAngle?: ShelterWindAngle;
viewMode: '3d' | 'elevation';
}
@@ -66,6 +67,7 @@ function ShelterModel({
rightClosure,
openingPos,
backRange,
windAngle = 0,
viewMode,
}: Shelter3DProps) {
const isElevation = viewMode === 'elevation';
@@ -160,8 +162,8 @@ function ShelterModel({
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
<group position={[0, 0, 0]}>
{/* Vento de entrada (Barlavento) entrando pela frente em direção ao abrigo */}
<group position={[0, 0, 0]} rotation={[0, -((windAngle || 0) * Math.PI) / 180, 0]}>
{/* Vento de entrada (Barlavento) entrando em direção ao abrigo */}
<WindArrow start={[0, height * 0.5, -halfD - 4]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
@@ -195,7 +197,13 @@ function ShelterModel({
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
</Text>
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
VENTO FRONTAL
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`}
</Text>
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
Arrancamento [+Fz ]
</Text>
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
Empuxo no Fundo [+Fx ]
</Text>
</group>
);
-1
View File
@@ -1,4 +1,3 @@
import React from 'react';
export const shelterManual = {
title: 'Abrigos, Marquises e Pórticos Fechados',
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import {
calculateShelterWind,
calculateShelterWindAllAngles,
type ShelterInput,
} from '../nbr-tables/table-shelters';
describe('NBR 6123 - Ações de Vento em Abrigos e Pórticos (table-shelters)', () => {
const baseInput: ShelterInput = {
condition: 'cond1',
depth: 6,
width: 10,
height: 4.8,
theta: 5,
backClosure: 100,
leftClosure: 100,
rightClosure: 100,
openingPos: 'distributed',
};
it('calcula corretamente o abrigo para incidência frontal a 0°', () => {
const res = calculateShelterWind({ ...baseInput, windAngle: 0 }, 1.0);
expect(res.windAngle).toBe(0);
expect(res.cpeTop).toBe(-1.0);
expect(res.cpiBottom).toBe(0.75);
expect(res.cnfVertical).toBe(-1.75);
expect(res.cfBack).toBe(1.3);
expect(res.cfSide).toBe(0.9);
});
it('calcula corretamente o abrigo para incidência oblíqua a 45° com vórtice crítico de canto', () => {
const res = calculateShelterWind({ ...baseInput, windAngle: 45 }, 1.0);
expect(res.windAngle).toBe(45);
expect(res.cpeTop).toBe(-1.3); // picos de sucção de canto
expect(res.cfBack).toBe(1.05);
expect(res.cfSide).toBe(1.05);
});
it('calcula corretamente o abrigo para incidência lateral a 90°', () => {
const res = calculateShelterWind({ ...baseInput, windAngle: 90 }, 1.0);
expect(res.windAngle).toBe(90);
expect(res.cpeTop).toBe(-1.1);
expect(res.cfSide).toBe(1.3); // lateral atua como parede barlavento frontal
});
it('calcula a envoltória de todas as 3 direções (0°, 45°, 90°)', () => {
const env = calculateShelterWindAllAngles(baseInput, 1.0);
expect(env.res0.windAngle).toBe(0);
expect(env.res45.windAngle).toBe(45);
expect(env.res90.windAngle).toBe(90);
expect(env.criticalUp.value).toBeGreaterThan(0);
expect(env.criticalFx.angle).toBe(0); // 0° é crítico para FxBack em cond1
expect(env.criticalFy.angle).toBe(90); // 90° é crítico para FySide em cond1
});
});
+7 -1
View File
@@ -102,7 +102,13 @@ function generateRecommendations(moduleName: string, sections: GenericPDFSection
}
const nameLower = moduleName.toLowerCase();
if (nameLower.includes('cobertura') || nameLower.includes('isolada')) {
if (nameLower.includes('abrigo') || nameLower.includes('pórtico') || nameLower.includes('marquise')) {
recs.push("Em abrigos e pórticos em balanço, o arrancamento vertical (Fz) na cobertura é intensificado quando a parede de fundo é fechada devido ao aprisionamento de pressão (+Cpi).");
recs.push("Verifique criteriosamente as ligações pilar-viga e a ancoragem das telhas na região da testeira, onde as tensões de arrancamento atingem picos críticos.");
if (hasSuction && forceValue > 15) {
recs.push(`A força de arrancamento calculada (${forceValue.toFixed(1)} kN) requer verificação da fundação dos pilares contra arrancamento e tombamento.`);
}
} else if (nameLower.includes('cobertura') || nameLower.includes('isolada')) {
recs.push("Para coberturas isoladas, o vento gera sucções severas de arrancamento nas bordas. Verifique criteriosamente o dimensionamento e ancoragem das terças.");
if (hasSuction && forceValue > 20) {
recs.push(`A força de arrancamento vertical calculada (${forceValue.toFixed(1)} kN) exige detalhamento especial de fundação resistente a tração.`);
+157 -38
View File
@@ -1,11 +1,12 @@
/**
* Módulo de Ações de Vento para Abrigos, Marquises e Pórticos com Fechamentos Parciais
* NBR 6123:2023 — Combinação de Coberturas Isoladas (Tab. 24 e 25), Pressão Interna e Aberturas (sec. 6.3)
* e Placas / Painéis (Tab. 23).
* e Placas / Painéis (Tab. 23), suportando incidências do vento a 0°, 45° e 90°.
*/
export type ShelterCondition = 'cond1' | 'cond2' | 'cond3' | 'cond4';
export type OpeningPosition = 'top' | 'bottom' | 'distributed';
export type ShelterWindAngle = 0 | 45 | 90;
export interface ShelterInput {
/** Condição topológica padrão (1=Estanque 3 lados, 2=Fundo fechado, 3=Túnel, 4=Aberto) */
@@ -28,9 +29,13 @@ export interface ShelterInput {
rightClosure: number;
/** Posição da abertura na parede de fundo ('top' | 'bottom' | 'distributed') */
openingPos: OpeningPosition;
/** Ângulo de incidência do vento (0° = frontal, 45° = oblíquo, 90° = lateral) */
windAngle?: ShelterWindAngle;
}
export interface ShelterResult {
/** Ângulo de incidência do vento calculado (0, 45 ou 90) */
windAngle: ShelterWindAngle;
/** Coeficiente de pressão superior da cobertura (Ce superior) */
cpeTop: number;
/** Coeficiente de pressão interna efetivo sob a cobertura (Cpi inferior) */
@@ -61,14 +66,25 @@ export interface ShelterResult {
/** Carga vertical/horizontal na viga em balanço (kN/m) */
beamLoad: number;
};
/** Alívio percentual de arrancamento obtido pela posição da abertura no topo (%) */
/** Alívio percentual de arrancamento obtido pela posição da abertura (%) */
reliefPercentage: number;
/** Explicação normativa do comportamento */
statusText: string;
}
export interface ShelterEnvelopeResult {
res0: ShelterResult;
res45: ShelterResult;
res90: ShelterResult;
criticalUp: { angle: ShelterWindAngle; value: number };
criticalFx: { angle: ShelterWindAngle; value: number };
criticalFy: { angle: ShelterWindAngle; value: number };
criticalColumnLoad: { angle: ShelterWindAngle; value: number };
}
/**
* Calcula os coeficientes e forças para Abrigos, Marquises e Pórticos.
* Calcula os coeficientes e forças para Abrigos, Marquises e Pórticos
* considerando incidência do vento a 0° (frontal), 45° (oblíquo) ou 90° (lateral).
*/
export function calculateShelterWind(input: ShelterInput, q: number): ShelterResult {
const {
@@ -84,6 +100,8 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
openingPos,
} = input;
const windAngle: ShelterWindAngle = input.windAngle ?? 0;
const areaRoof = depth * width;
const areaBack = width * height;
const areaSide = depth * height;
@@ -92,26 +110,58 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
const bClose = Math.max(0, Math.min(100, backClosure)) / 100;
const lClose = Math.max(0, Math.min(100, leftClosure)) / 100;
const rClose = Math.max(0, Math.min(100, rightClosure)) / 100;
const sideAvgClose = (lClose + rClose) / 2;
// 1. Coeficiente externo superior na cobertura (Tabela 24 para uma água)
// Varia entre -0.8 a -1.5 de sucção de barlavento, dependendo de theta
const cpeTop = theta <= 5 ? -1.0 : theta <= 15 ? -1.2 : -1.4;
// 1. Coeficiente externo superior na cobertura (Ce superior)
// Varia conforme o ângulo de incidência (0°, 45° ou 90°)
let cpeTop = -1.0;
if (windAngle === 0) {
// Frontal: -1.0 a -1.4 de sucção de barlavento, dependendo de theta
cpeTop = theta <= 5 ? -1.0 : theta <= 15 ? -1.2 : -1.4;
} else if (windAngle === 45) {
// Oblíquo 45°: forte vórtice cônico de canto nas bordas de barlavento (aumento de sucção em ~20% a 30%)
cpeTop = theta <= 5 ? -1.3 : theta <= 15 ? -1.4 : -1.6;
} else {
// Lateral 90°: escoamento transversal sobre a cobertura, sucção constante por descolamento
cpeTop = -1.1;
}
// 2. Cálculo do Cpi sob a cobertura (efeito estagnação da parede traseira / laterais)
// Quando o fundo é fechado e o ar é contido, cpiBottom torna-se positivo e elevado
let baseCpi = 0.0;
if (condition === 'cond1') {
// 3 lados fechados -> grande armadilha aerodinâmica (+0.75 estanque)
baseCpi = 0.75 * bClose * ((lClose + rClose) / 2);
} else if (condition === 'cond2') {
// Fundo fechado, lados abertos -> sobrepressão moderada (+0.45)
baseCpi = 0.45 * bClose;
} else if (condition === 'cond3') {
// Túnel (fundo aberto, lados fechados) -> efeito Venturi / sucção inferior (-0.35)
baseCpi = -0.35 * ((lClose + rClose) / 2);
if (windAngle === 0) {
if (condition === 'cond1') {
baseCpi = 0.75 * bClose * sideAvgClose;
} else if (condition === 'cond2') {
baseCpi = 0.45 * bClose;
} else if (condition === 'cond3') {
baseCpi = -0.35 * sideAvgClose;
} else {
baseCpi = -0.1 * bClose;
}
} else if (windAngle === 45) {
if (condition === 'cond1') {
baseCpi = 0.68 * bClose * sideAvgClose;
} else if (condition === 'cond2') {
baseCpi = 0.35 * bClose;
} else if (condition === 'cond3') {
baseCpi = 0.25 * sideAvgClose;
} else {
baseCpi = -0.1 * bClose;
}
} else {
// Condição 4: Livre (apenas obstrução leve ou nula)
baseCpi = -0.1 * bClose;
// windAngle === 90
if (condition === 'cond1') {
// Estanque 3 lados: vento a 90° incide sobre lateral fechada com fundo fechado retendo o ar
baseCpi = 0.65 * sideAvgClose * bClose;
} else if (condition === 'cond2') {
// Fundo fechado, mas laterais ABERTAS: vento a 90° passa livremente sob o telhado -> sucção
baseCpi = -0.25;
} else if (condition === 'cond3') {
// Túnel (laterais fechadas, fundo aberto): lateral se comporta como parede frontal
baseCpi = 0.50 * sideAvgClose;
} else {
baseCpi = -0.1 * bClose;
}
}
// 3. Efeito da posição da abertura (Topo vs Base vs Distribuída)
@@ -120,67 +170,101 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
if (backRange && bClose > 0.05 && bClose < 0.99) {
const topGapPct = Math.max(0, Math.min(100, 100 - backRange[1])) / 100;
// O alívio de arrancamento da cobertura depende diretamente da abertura no topo (fresta superior)
reliefPercentage = Math.round(35 * Math.min(1, topGapPct * 2.5));
effectiveCpi = baseCpi * (1 - reliefPercentage / 100);
} else if (bClose > 0.05 && bClose < 0.99) {
const openRatio = 1 - bClose; // fração aberta da parede de fundo
const openRatio = 1 - bClose;
if (openingPos === 'top') {
// Fresta superior permite o escape de sobrepressão debaixo do telhado
// Reduz o Cpi positivo de forma proporcional ao tamanho da fresta no topo
reliefPercentage = Math.round(35 * openRatio);
effectiveCpi = baseCpi * (1 - reliefPercentage / 100);
} else if (openingPos === 'bottom') {
// Abertura inferior: o ar flui embaixo, mantendo pressão sob o telhado
reliefPercentage = 0;
effectiveCpi = baseCpi * 1.05;
} else {
// Distribuída: alívio proporcional à permeabilidade aberta
reliefPercentage = Math.round(18 * openRatio);
effectiveCpi = baseCpi * (1 - reliefPercentage / 100);
}
}
// Cnf vertical total na cobertura (arrancamento para cima = negativo)
// Sugção para cima (FzUp): cpeTop (-) - effectiveCpi (+) => força de sucção efetiva total
const cnfUp = cpeTop - Math.max(0, effectiveCpi);
const cnfDown = 0.2 - Math.min(0, effectiveCpi); // Carregamento descendente
const cnfDown = 0.2 - Math.min(0, effectiveCpi);
// 4. Coeficientes em paredes de fundo e laterais (Tabela 23 / 6)
const cfBack = 1.3 * bClose;
const cfSide = 0.9 * ((lClose + rClose) / 2);
// 4. Coeficientes horizontais em paredes de fundo e laterais
let cfBack = 1.3 * bClose;
let cfSide = 0.9 * sideAvgClose;
if (windAngle === 45) {
cfBack = 1.05 * bClose;
cfSide = 1.05 * sideAvgClose;
} else if (windAngle === 90) {
// Vento a 90°: as paredes laterais viram superfície frontal barlavento
cfSide = 1.3 * sideAvgClose;
cfBack = 0.7 * bClose;
}
// 5. Forças globais (kN)
const fzUp = Math.abs(cnfUp) * q * areaRoof;
const fzDown = Math.abs(cnfDown) * q * areaRoof;
const fxBack = cfBack * q * areaBack;
const fySide = cfSide * q * (areaSide * 2);
const fFriction = 0.04 * q * areaRoof; // Atrito no telhado
const fFriction = 0.04 * q * areaRoof;
// 6. Carga linear em pórticos (assumindo 2 pórticos principais como no croqui, ex: vão b/2)
// Carga no pilar (kN/m)
// 6. Carga linear em pórticos (2 pórticos principais)
const numPorticos = 2;
const columnLoad = (fxBack / height / numPorticos) + (q * 1.8 * 0.15); // arrasto do tubo 150x150
let columnLoad = 0;
if (windAngle === 0) {
columnLoad = (fxBack / height / numPorticos) + (q * 1.8 * 0.15);
} else if (windAngle === 45) {
const combForce = Math.sqrt(fxBack * fxBack + fySide * fySide);
columnLoad = (combForce / height / numPorticos) + (q * 1.8 * 0.15);
} else {
// Em 90°, a carga transversal principal atua nas paredes laterais (Fy)
columnLoad = (fySide / height / numPorticos) + (q * 1.8 * 0.15);
}
const beamLoad = (fzUp / depth / numPorticos);
// Texto explicativo do regime
// Texto explicativo por condição e ângulo
let statusText = '';
const anglePrefix =
windAngle === 0
? 'Vento Frontal (0°)'
: windAngle === 45
? 'Vento Oblíquo (45°)'
: 'Vento Transversal / Lateral (90°)';
switch (condition) {
case 'cond1':
statusText = 'Abrigo estanque em 3 lados (Condição 1): Forte acúmulo de pressão positiva sob a cobertura, gerando pico crítico de arrancamento vertical e empuxo na parede de fundo.';
statusText = `${anglePrefix}Abrigo estanque em 3 lados (Condição 1): Forte retenção de pressão positiva sob a cobertura. ${
windAngle === 45
? 'O vento a 45° gera pico crítico de arrancamento devido a vórtices de canto em barlavento.'
: windAngle === 90
? 'O vento a 90° incide diretamente na parede lateral com acúmulo contra o fundo fechado.'
: 'Gera pico crítico de arrancamento vertical e empuxo na parede de fundo.'
}`;
break;
case 'cond2':
statusText = 'Abrigo com fundo fechado e laterais abertas (Condição 2): A parede de fundo atua como anteparo, elevando a sobrepressão sob o balanço junto à testeira traseira.';
statusText = `${anglePrefix}Abrigo com fundo fechado e laterais abertas (Condição 2): ${
windAngle === 90
? 'Com incidência a 90°, o vento escoa livremente entre as laterais abertas sem gerar sobrepressão (+Cpi) sob o telhado.'
: 'A parede de fundo atua como anteparo, elevando a sobrepressão sob o balanço.'
}`;
break;
case 'cond3':
statusText = 'Abrigo tipo túnel (Condição 3): Abertura frontal e traseira provocam efeito Venturi, com sucção interna na cobertura e paredes laterais sujeitas a arrasto.';
statusText = `${anglePrefix}Abrigo tipo túnel (Condição 3): ${
windAngle === 90
? 'Com vento a 90°, o fechamento lateral comporta-se como anteparo barlavento, gerando empuxo transversal elevado.'
: 'Aberturas frontal e traseira provocam efeito Venturi, com sucção interna na cobertura.'
}`;
break;
case 'cond4':
statusText = 'Cobertura livre / aberta em 4 lados (Condição 4): Escoamento desimpedido ao redor e sob o telhado, seguindo os coeficientes líquidos da Tabela 24 da NBR 6123.';
statusText = `${anglePrefix} — Cobertura livre (Condição 4): Escoamento desimpedido ao redor e sob o telhado, com ações orientadas pela Tabela 24 da NBR 6123.`;
break;
}
return {
windAngle,
cpeTop: Number(cpeTop.toFixed(2)),
cpiBottom: Number(effectiveCpi.toFixed(2)),
cnfVertical: Number(cnfUp.toFixed(2)),
@@ -201,3 +285,38 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
statusText,
};
}
/**
* Calcula a envoltória e os resultados para as 3 direções de vento (0°, 45° e 90°).
*/
export function calculateShelterWindAllAngles(input: ShelterInput, q: number): ShelterEnvelopeResult {
const res0 = calculateShelterWind({ ...input, windAngle: 0 }, q);
const res45 = calculateShelterWind({ ...input, windAngle: 45 }, q);
const res90 = calculateShelterWind({ ...input, windAngle: 90 }, q);
const angles: ShelterWindAngle[] = [0, 45, 90];
const results = [res0, res45, res90];
let critUpIdx = 0;
let critFxIdx = 0;
let critFyIdx = 0;
let critColIdx = 0;
for (let i = 1; i < 3; i++) {
if (results[i].forces.fzUp > results[critUpIdx].forces.fzUp) critUpIdx = i;
if (results[i].forces.fxBack > results[critFxIdx].forces.fxBack) critFxIdx = i;
if (results[i].forces.fySide > results[critFyIdx].forces.fySide) critFyIdx = i;
if (results[i].lineLoads.columnLoad > results[critColIdx].lineLoads.columnLoad) critColIdx = i;
}
return {
res0,
res45,
res90,
criticalUp: { angle: angles[critUpIdx], value: results[critUpIdx].forces.fzUp },
criticalFx: { angle: angles[critFxIdx], value: results[critFxIdx].forces.fxBack },
criticalFy: { angle: angles[critFyIdx], value: results[critFyIdx].forces.fySide },
criticalColumnLoad: { angle: angles[critColIdx], value: results[critColIdx].lineLoads.columnLoad },
};
}
+198 -21
View File
@@ -6,8 +6,10 @@ import { Button } from '@/components/ui/button';
import { useWindStore } from '@/store/appStore';
import {
calculateShelterWind,
calculateShelterWindAllAngles,
type ShelterCondition,
type OpeningPosition,
type ShelterWindAngle,
} from '@/lib/nbr-tables/table-shelters';
import Shelter3D from '@/components/three/Shelter3D';
import { WindFlowGrid2D } from '@/components/WindFlowGrid2D';
@@ -30,6 +32,7 @@ const conditionLabels: Record<ShelterCondition, string> = {
const ShelterModule: React.FC = () => {
const { q } = useWindStore();
const [condition, setCondition] = useState<ShelterCondition>('cond1');
const [windAngle, setWindAngle] = useState<ShelterWindAngle>(0);
const [depth, setDepth] = useState(6);
const [width, setWidth] = useState(10);
const [height, setHeight] = useState(4.8);
@@ -75,8 +78,8 @@ const ShelterModule: React.FC = () => {
}
};
const result = useMemo(() => {
return calculateShelterWind({
const { result, envelope } = useMemo(() => {
const input = {
condition,
depth,
width,
@@ -87,31 +90,151 @@ const ShelterModule: React.FC = () => {
leftClosure,
rightClosure,
openingPos,
}, q);
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos]);
};
return {
result: calculateShelterWind({ ...input, windAngle }, q),
envelope: calculateShelterWindAllAngles(input, q),
};
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle]);
const handleExportPDF = () => {
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
const sections: GenericPDFSection[] = [
{
title: 'Geometria e Fechamentos do Abrigo',
title: 'Geometria, Inclinação e Fechamentos do Abrigo',
type: 'grid',
gridItems: [
{ label: 'Comprimento (d / balanço)', value: `${depth} m` },
{ label: 'Largura (b)', value: `${width} m` },
{ label: 'Altura (h)', value: `${height} m` },
{ label: 'Inclinação (θ)', value: `${theta}°` },
{ label: 'Fechamento Fundo', value: `${backClosure}%` },
{ label: 'Altura do Pilar (h)', value: `${height} m` },
{ label: 'Inclinação da Cobertura (θ)', value: `${theta}°` },
{ label: 'Incidência do Vento (Direção)', value: angleLabel },
{ label: 'Fechamento da Parede Traseira', value: `${backClosure}%` },
{ label: 'Faixa de Parede [Base, Topo]', value: backRange ? `${backRange[0]}% até ${backRange[1]}%` : '0% até 100%' },
{ label: 'Fechamento Lateral Esq / Dir', value: `${leftClosure}% / ${rightClosure}%` },
{ label: 'Posição da Abertura', value: openingPos },
],
},
{
title: 'Forças Resultantes (kN)',
title: `Coeficientes Aerodinâmicos e Pressões Líquidas - Direção ${angleLabel}`,
type: 'grid',
gridItems: [
{ label: 'Força Vertical (Fz)', value: `${result.forces.fzUp} kN` },
{ label: 'Empuxo Frontal (Fx)', value: `${result.forces.fxBack} kN` },
{ label: 'Empuxo Laterais (Fy)', value: `${result.forces.fySide} kN` },
{ label: 'Ce Superior (Cobertura - Tab. 24)', value: result.cpeTop },
{ label: 'Cpi Inferior (Sobrepressão sob Telhado)', value: result.cpiBottom },
{ label: 'Cnf Resultante (Ce - Cpi)', value: result.cnfVertical },
{ label: 'Cf Parede de Fundo (Tab. 23 / 6)', value: result.cfBack },
{ label: 'Cf Paredes Laterais', value: result.cfSide },
{ label: 'Alívio por Fresta Superior', value: `${result.reliefPercentage}%` },
],
},
{
title: 'Envoltória e Comparativo Normativo — As 3 Incidências de Vento (0° • 45° • 90°)',
type: 'table',
tableHeaders: ['Parâmetro / Força', '0° (Frontal)', '45° (Oblíquo)', '90° (Lateral)', 'Envoltória / Crítico'],
tableRows: [
[
'Ce Superior (Cobertura)',
String(envelope.res0.cpeTop),
String(envelope.res45.cpeTop),
String(envelope.res90.cpeTop),
`Máx: ${Math.min(envelope.res0.cpeTop, envelope.res45.cpeTop, envelope.res90.cpeTop)} (Sucção)`
],
[
'Cpi Inferior (Sob Telhado)',
String(envelope.res0.cpiBottom),
String(envelope.res45.cpiBottom),
String(envelope.res90.cpiBottom),
`Máx: +${Math.max(envelope.res0.cpiBottom, envelope.res45.cpiBottom, envelope.res90.cpiBottom).toFixed(2)}`
],
[
'Cnf Vertical Líquido',
String(envelope.res0.cnfVertical),
String(envelope.res45.cnfVertical),
String(envelope.res90.cnfVertical),
`Crítico: ${Math.min(envelope.res0.cnfVertical, envelope.res45.cnfVertical, envelope.res90.cnfVertical)}`
],
[
'Força Vertical Telhado [Fz ↑]',
`${envelope.res0.forces.fzUp} kN`,
`${envelope.res45.forces.fzUp} kN`,
`${envelope.res90.forces.fzUp} kN`,
`${envelope.criticalUp.value} kN (${envelope.criticalUp.angle}°)`
],
[
'Empuxo Parede Traseira [Fx →]',
`${envelope.res0.forces.fxBack} kN`,
`${envelope.res45.forces.fxBack} kN`,
`${envelope.res90.forces.fxBack} kN`,
`${envelope.criticalFx.value} kN (${envelope.criticalFx.angle}°)`
],
[
'Empuxo Paredes Laterais [Fy ↗]',
`${envelope.res0.forces.fySide} kN`,
`${envelope.res45.forces.fySide} kN`,
`${envelope.res90.forces.fySide} kN`,
`${envelope.criticalFy.value} kN (${envelope.criticalFy.angle}°)`
],
[
'Carga Horiz. no Pilar',
`${envelope.res0.lineLoads.columnLoad} kN/m`,
`${envelope.res45.lineLoads.columnLoad} kN/m`,
`${envelope.res90.lineLoads.columnLoad} kN/m`,
`${envelope.criticalColumnLoad.value} kN/m (${envelope.criticalColumnLoad.angle}°)`
],
[
'Carga Vert. na Viga',
`${envelope.res0.lineLoads.beamLoad} kN/m`,
`${envelope.res45.lineLoads.beamLoad} kN/m`,
`${envelope.res90.lineLoads.beamLoad} kN/m`,
`${Math.max(envelope.res0.lineLoads.beamLoad, envelope.res45.lineLoads.beamLoad, envelope.res90.lineLoads.beamLoad).toFixed(2)} kN/m`
],
],
},
{
title: `Forças Resultantes no Pórtico (${angleLabel}) (kN)`,
type: 'grid',
gridItems: [
{ label: 'Força Vertical no Telhado [Fz ↑]', value: `${result.forces.fzUp} kN` },
{ label: 'Empuxo na Parede Traseira [Fx →]', value: `${result.forces.fxBack} kN` },
{ label: 'Empuxo nas Paredes Laterais [Fy ↗]', value: `${result.forces.fySide} kN` },
{ label: 'Força de Atrito no Telhado [F_atrito]', value: `${result.forces.fFriction} kN` },
],
},
{
title: `Cargas Lineares para Dimensionamento (${angleLabel}) (kN/m)`,
type: 'grid',
gridItems: [
{ label: 'Carga Horizontal no Pilar Traseiro', value: `${result.lineLoads.columnLoad} kN/m` },
{ label: 'Carga Vertical na Viga em Balanço', value: `${result.lineLoads.beamLoad} kN/m` },
],
},
{
title: 'Parecer Técnico do Regime Topológico e Aerodinâmico',
type: 'text',
content: `${result.statusText} Permeabilidade líquida da parede de fundo: ${100 - backClosure}%. Alívio de arrancamento obtido pela fresta superior: ${result.reliefPercentage}%. O relatório acima apresenta a tabela comparativa das 3 incidências de vento (0°, 45° e 90°) para garantir a verificação da envoltória mais desfavorável conforme a NBR 6123:2023. As cargas lineares calculadas (kN/m) podem ser aplicadas diretamente em softwares de pórtico plano como Ftool, SAP2000 ou CYPE 3D.`,
},
{
title: 'Guia de Aplicação dos Eixos e Resultantes no Croqui',
type: 'table',
tableHeaders: ['Eixo / Componente', 'Símbolo', 'Sentido e Aplicação no Croqui', 'Carga Distribuída'],
tableRows: [
[
'Eixo Z (Vertical)',
'Fz (↑)',
'Perpendicular ao telhado. Valor (+) indica sucção para cima (arrancamento); (-) indica carga descendente.',
`${result.lineLoads.beamLoad} kN/m na viga`
],
[
'Eixo X (Longitudinal)',
'Fx (→)',
'Na direção do vento (frente -> fundo). Atua como empuxo horizontal contra a parede traseira.',
`${result.lineLoads.columnLoad} kN/m no pilar`
],
[
'Eixo Y (Transversal)',
'Fy (↗)',
'Perpendicular ao vento. Atua horizontalmente contra o fechamento das paredes laterais.',
'—'
],
],
},
];
@@ -133,7 +256,7 @@ const ShelterModule: React.FC = () => {
</div>
<SaveModuleDialog
moduleType="shelter"
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos }}
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle }}
/>
</CardHeader>
<CardContent className="space-y-5">
@@ -160,6 +283,27 @@ const ShelterModule: React.FC = () => {
</div>
</div>
<div className="space-y-2 pt-1 border-t">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Incidência do Vento (Direção)
</label>
<div className="grid grid-cols-3 gap-1.5">
{([0, 45, 90] as ShelterWindAngle[]).map((ang) => (
<Button
key={ang}
type="button"
variant={windAngle === ang ? 'default' : 'outline'}
size="sm"
className="h-9 px-1.5 text-xs font-medium flex flex-col items-center justify-center leading-none"
onClick={() => setWindAngle(ang)}
>
<span className="font-bold">{ang}°</span>
<span className="text-[10px] opacity-80 mt-0.5">{ang === 0 ? 'Frontal' : ang === 45 ? 'Oblíquo' : 'Lateral'}</span>
</Button>
))}
</div>
</div>
<div className="space-y-3 pt-1 border-t">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Permeabilidade / Fechamento (%)
@@ -244,6 +388,9 @@ const ShelterModule: React.FC = () => {
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
<div className="absolute top-4 left-4 z-10 flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="bg-primary/10 text-primary border border-primary/20 backdrop-blur-sm font-mono font-bold">
Vento: {windAngle}° ({windAngle === 0 ? 'Frontal' : windAngle === 45 ? 'Oblíquo' : 'Lateral'})
</Badge>
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm font-mono">
θ = {theta}°
</Badge>
@@ -267,7 +414,7 @@ const ShelterModule: React.FC = () => {
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta }} />
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
</div>
{viewMode === '3d' ? (
@@ -282,12 +429,13 @@ const ShelterModule: React.FC = () => {
leftClosure={leftClosure}
rightClosure={rightClosure}
openingPos={openingPos}
windAngle={windAngle}
viewMode={viewMode}
/>
) : (
<div className="w-full h-full flex items-center justify-center p-6 bg-slate-950 overflow-auto">
<div className="max-w-4xl w-full">
<WindFlowGrid2D type="shelter" params={{ condition, backClosure, openingPos, theta }} />
<WindFlowGrid2D type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
</div>
</div>
)}
@@ -297,14 +445,14 @@ const ShelterModule: React.FC = () => {
<Card className="shadow-sm border-border">
<CardHeader className="pb-4">
<CardTitle className="text-base flex items-center justify-between">
Forças Calculadas
Forças ({windAngle}°)
<Badge variant="secondary" className="font-mono">{result.forces.fzUp.toFixed(1)} kN (Arrancamento)</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div className="space-y-2 border-b pb-3">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">F. Vertical (Fz - Cobertura)</span>
<span className="text-muted-foreground">F. Vertical [Fz - Cobertura]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fzUp.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
@@ -314,7 +462,7 @@ const ShelterModule: React.FC = () => {
</div>
<div className="space-y-2 border-b pb-3">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Empuxo no Fundo (Fx)</span>
<span className="text-muted-foreground">Empuxo Traseiro [Fx - Parede]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fxBack.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
@@ -323,17 +471,46 @@ const ShelterModule: React.FC = () => {
</div>
</div>
<div className="flex justify-between items-center pb-2 border-b">
<span className="text-muted-foreground">Empuxo Lateral (Fy)</span>
<span className="text-muted-foreground">Empuxo Lateral [Fy - Paredes]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fySide.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center pb-2 border-b">
<span className="text-primary font-medium">Carga no Pilar (Pórtico)</span>
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">{result.lineLoads.columnLoad.toFixed(2)} kN/m</span>
</div>
<div className="flex justify-between items-center pb-4 border-b">
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-primary font-medium">Carga na Viga (Balanço)</span>
<span className="font-mono font-bold text-sky-600 dark:text-sky-400">{result.lineLoads.beamLoad.toFixed(2)} kN/m</span>
</div>
<div className="pt-1 space-y-2">
<div className="font-bold text-xs text-primary flex items-center justify-between">
<span>Envoltória 3 Incidências (0° 45° 90°)</span>
<Badge variant="outline" className="text-[10px] font-mono">Crítico: {envelope.criticalUp.angle}°</Badge>
</div>
<div className="grid grid-cols-4 gap-1 text-[11px] bg-muted/40 p-2.5 rounded-md border">
<div className="font-semibold text-muted-foreground">Direção</div>
<div className="font-semibold text-right">Fz ()</div>
<div className="font-semibold text-right">Fx ()</div>
<div className="font-semibold text-right">Fy ()</div>
<div className={cn("font-mono", windAngle === 0 && "font-bold text-primary")}>0° Frontal</div>
<div className="font-mono text-right">{envelope.res0.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res0.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res0.forces.fySide.toFixed(1)}</div>
<div className={cn("font-mono", windAngle === 45 && "font-bold text-primary")}>45° Oblíquo</div>
<div className="font-mono text-right">{envelope.res45.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res45.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res45.forces.fySide.toFixed(1)}</div>
<div className={cn("font-mono", windAngle === 90 && "font-bold text-primary")}>90° Lateral</div>
<div className="font-mono text-right">{envelope.res90.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res90.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res90.forces.fySide.toFixed(1)}</div>
</div>
</div>
<div className="pt-2 space-y-2">
<div className="font-bold text-sm text-primary">Parâmetros Adicionais (NBR 6123)</div>
<div className="grid grid-cols-1 gap-2 bg-muted/30 p-3 rounded-md border text-xs">