🚀 Auto-deploy: BrainWind atualizado em 27/07/2026 16:55:52
This commit is contained in:
+5
-1
@@ -12,11 +12,12 @@ import CalcCaModule from './pages/CalcCaModule';
|
|||||||
import SettingsModule from './pages/SettingsModule';
|
import SettingsModule from './pages/SettingsModule';
|
||||||
import TowerModule from './pages/TowerModule';
|
import TowerModule from './pages/TowerModule';
|
||||||
import PiperackModule from './pages/PiperackModule';
|
import PiperackModule from './pages/PiperackModule';
|
||||||
|
import ShelterModule from './pages/ShelterModule';
|
||||||
import CertificatePage from './pages/CertificatePage';
|
import CertificatePage from './pages/CertificatePage';
|
||||||
import {
|
import {
|
||||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||||
Square, Layers, BarChart3, Activity, Warehouse,
|
Square, Layers, BarChart3, Activity, Warehouse,
|
||||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck, Calculator
|
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck, Calculator, Tent
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -95,6 +96,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
|
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
|
||||||
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
||||||
{ path: '/piperack', icon: <Frame className="w-5 h-5" />, labelKey: 'nav_piperack' as const },
|
{ path: '/piperack', icon: <Frame className="w-5 h-5" />, labelKey: 'nav_piperack' as const },
|
||||||
|
{ path: '/abrigo', icon: <Tent className="w-5 h-5" />, labelKey: 'nav_shelter' as const },
|
||||||
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
|
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
|
||||||
{ path: '/calc-ca', icon: <Calculator className="w-5 h-5" />, labelKey: 'nav_calc_ca' as const },
|
{ path: '/calc-ca', icon: <Calculator className="w-5 h-5" />, labelKey: 'nav_calc_ca' as const },
|
||||||
];
|
];
|
||||||
@@ -358,6 +360,7 @@ function HomeMock() {
|
|||||||
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
||||||
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
||||||
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
||||||
|
{ to: '/abrigo', icon: Tent, label: 'Abrigos e Pórticos Fechados', desc: 'Marquises, abrigos e pórticos. 4 condições topológicas, frestas de alívio, corte A-A (Tab. 23-25, sec. 6.3).' },
|
||||||
{ to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' },
|
{ to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -423,6 +426,7 @@ function App() {
|
|||||||
<Route path="/pontes" element={<BridgeModule />} />
|
<Route path="/pontes" element={<BridgeModule />} />
|
||||||
<Route path="/torre" element={<TowerModule />} />
|
<Route path="/torre" element={<TowerModule />} />
|
||||||
<Route path="/piperack" element={<PiperackModule />} />
|
<Route path="/piperack" element={<PiperackModule />} />
|
||||||
|
<Route path="/abrigo" element={<ShelterModule />} />
|
||||||
<Route path="/dinamica" element={<DynamicsModule />} />
|
<Route path="/dinamica" element={<DynamicsModule />} />
|
||||||
<Route path="/calc-ca" element={<CalcCaModule />} />
|
<Route path="/calc-ca" element={<CalcCaModule />} />
|
||||||
<Route path="/settings" element={<SettingsModule />} />
|
<Route path="/settings" element={<SettingsModule />} />
|
||||||
|
|||||||
@@ -169,6 +169,45 @@ export function EducationalManual({ type, params }: EducationalManualProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Abrigos e Pórticos (shelter) ---
|
||||||
|
if (type === 'shelter') {
|
||||||
|
const condition = params.condition || 'cond1';
|
||||||
|
const openingPos = params.openingPos || 'top';
|
||||||
|
const backClosure = Number(params.backClosure || 80);
|
||||||
|
|
||||||
|
explanations.push(
|
||||||
|
<div key="sh-cond" className="text-xs border-l-2 border-primary pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">
|
||||||
|
{condition === 'cond1' ? 'Abrigo Estanque em 3 Lados (Condição 1)' :
|
||||||
|
condition === 'cond2' ? 'Abrigo com Parede Traseira (Condição 2)' :
|
||||||
|
condition === 'cond3' ? 'Abrigo Tipo Túnel (Condição 3)' : 'Cobertura Livre (Condição 4)'}
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
{condition === 'cond1' ? 'Fundo e laterais fechados funcionam como armadilha aerodinâmica, concentrando pressão positiva (Cpi elevado) debaixo da marquise.' :
|
||||||
|
condition === 'cond2' ? 'A parede de fundo gera anteparo de estagnação frontal, enquanto os lados abertos permitem escape lateral do fluxo.' :
|
||||||
|
condition === 'cond3' ? 'O fluxo longitudinal sofre efeito Venturi, gerando sucção sob o telhado e arrasto de atrito em túnel.' :
|
||||||
|
'O fluxo passa livremente pela cobertura, seguindo coeficientes da Tabela 24.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (backClosure > 0 && backClosure < 100) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="sh-pos" className="text-xs border-l-2 border-emerald-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-emerald-500 block">
|
||||||
|
{openingPos === 'top' ? 'Fresta Superior (Alívio de Arrancamento)' :
|
||||||
|
openingPos === 'bottom' ? 'Vão Livre na Base (Placa Suspensa)' : 'Permeabilidade Distribuída'}
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
{openingPos === 'top' ? 'A fresta no topo da parede permite que a pressão sob o telhado escape em direção a sotavento, aliviando significativamente a força vertical de arrancamento na cobertura (até 25% menos sucção líquida).' :
|
||||||
|
openingPos === 'bottom' ? 'O vento passa por baixo e concentra a pressão horizontal na metade superior da parede (como placa suspensa na Tab. 23), aumentando o momento fletor na base dos pilares.' :
|
||||||
|
'Aberturas distribuídas atenuam suavemente a pressão interna de acordo com a razão de áreas abertas (sec. 6.3).'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (explanations.length === 0) return null;
|
if (explanations.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { Text, Line } from '@react-three/drei';
|
||||||
|
import type { ShelterCondition, OpeningPosition } from '@/lib/nbr-tables/table-shelters';
|
||||||
|
|
||||||
|
export interface Shelter3DProps {
|
||||||
|
condition: ShelterCondition;
|
||||||
|
depth: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
theta: number;
|
||||||
|
backClosure: number;
|
||||||
|
leftClosure: number;
|
||||||
|
rightClosure: number;
|
||||||
|
openingPos: OpeningPosition;
|
||||||
|
viewMode: '3d' | 'elevation';
|
||||||
|
}
|
||||||
|
|
||||||
|
const WindArrow: React.FC<{
|
||||||
|
start: [number, number, number];
|
||||||
|
end: [number, number, number];
|
||||||
|
color?: string;
|
||||||
|
speed?: number;
|
||||||
|
}> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => {
|
||||||
|
const arrowRef = useRef<THREE.Group>(null);
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
if (arrowRef.current) {
|
||||||
|
const t = (clock.getElapsedTime() * speed) % 1;
|
||||||
|
const x = start[0] + (end[0] - start[0]) * t;
|
||||||
|
const y = start[1] + (end[1] - start[1]) * t;
|
||||||
|
const z = start[2] + (end[2] - start[2]) * t;
|
||||||
|
arrowRef.current.position.set(x, y, z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start));
|
||||||
|
const len = dir.length();
|
||||||
|
const angleZ = Math.atan2(dir.y, dir.x);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={arrowRef}>
|
||||||
|
<mesh rotation={[0, 0, angleZ - Math.PI / 2]}>
|
||||||
|
<coneGeometry args={[0.25, 0.6, 8]} />
|
||||||
|
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||||
|
condition,
|
||||||
|
depth,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
theta,
|
||||||
|
backClosure,
|
||||||
|
leftClosure,
|
||||||
|
rightClosure,
|
||||||
|
openingPos,
|
||||||
|
viewMode,
|
||||||
|
}) => {
|
||||||
|
const isElevation = viewMode === 'elevation';
|
||||||
|
|
||||||
|
// Geometria básica
|
||||||
|
const halfW = width / 2;
|
||||||
|
const halfD = depth / 2;
|
||||||
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
|
const slopeRise = depth * Math.tan(thetaRad);
|
||||||
|
|
||||||
|
// Paredes com abertura no topo ou na base
|
||||||
|
const renderWallWithGap = (
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
closure: number,
|
||||||
|
pos: OpeningPosition,
|
||||||
|
rotY = 0,
|
||||||
|
position: [number, number, number] = [0, 0, 0]
|
||||||
|
) => {
|
||||||
|
if (closure <= 0) return null;
|
||||||
|
|
||||||
|
const op = closure / 100;
|
||||||
|
const wallH = h * op;
|
||||||
|
let yPos = position[1] + wallH / 2;
|
||||||
|
|
||||||
|
if (pos === 'top' && op < 1) {
|
||||||
|
// Parede na parte de baixo, fresta no topo
|
||||||
|
yPos = position[1] + wallH / 2;
|
||||||
|
} else if (pos === 'bottom' && op < 1) {
|
||||||
|
// Parede suspensa na parte superior, vão na base
|
||||||
|
yPos = position[1] + h - wallH / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={position} rotation={[0, rotY, 0]}>
|
||||||
|
<mesh position={[0, yPos - position[1], 0]}>
|
||||||
|
<boxGeometry args={[w, wallH, 0.15]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color="#64748b"
|
||||||
|
transparent={op < 1}
|
||||||
|
opacity={Math.max(0.3, op)}
|
||||||
|
roughness={0.4}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
{/* Linha de contorno */}
|
||||||
|
<lineSegments>
|
||||||
|
<edgesGeometry args={[new THREE.BoxGeometry(w, wallH, 0.15)]} />
|
||||||
|
<lineBasicMaterial color="#94a3b8" />
|
||||||
|
</lineSegments>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={[0, -height / 2, 0]}>
|
||||||
|
{/* Plano do Solo */}
|
||||||
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
||||||
|
<planeGeometry args={[width * 2, depth * 2]} />
|
||||||
|
<meshStandardMaterial color="#1e293b" roughness={0.9} />
|
||||||
|
</mesh>
|
||||||
|
<gridHelper args={[Math.max(width, depth) * 2, 20, '#475569', '#334155']} position={[0, 0.01, 0]} />
|
||||||
|
|
||||||
|
{/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */}
|
||||||
|
{[-halfW + 0.6, halfW - 0.6].map((xPos, idx) => (
|
||||||
|
<group key={`frame-${idx}`} position={[xPos, 0, 0]}>
|
||||||
|
{/* Pilar traseiro (lado direito na elevação) */}
|
||||||
|
<mesh position={[0, height / 2, halfD - 0.25]}>
|
||||||
|
<boxGeometry args={[0.35, height, 0.35]} />
|
||||||
|
<meshStandardMaterial color="#cbd5e1" metalness={0.7} roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
{/* Viga horizontal em balanço (TUBO QUAD. 150x150) */}
|
||||||
|
<group position={[0, height, 0]} rotation={[thetaRad, 0, 0]}>
|
||||||
|
<mesh position={[0, 0, 0]}>
|
||||||
|
<boxGeometry args={[0.25, 0.3, depth]} />
|
||||||
|
<meshStandardMaterial color="#cbd5e1" metalness={0.7} roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 2. Cobertura / Marquise */}
|
||||||
|
<group position={[0, height, 0]} rotation={[thetaRad, 0, 0]}>
|
||||||
|
<mesh position={[0, 0.15, 0]}>
|
||||||
|
<boxGeometry args={[width + 0.6, 0.15, depth + 0.4]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
<lineSegments position={[0, 0.15, 0]}>
|
||||||
|
<edgesGeometry args={[new THREE.BoxGeometry(width + 0.6, 0.15, depth + 0.4)]} />
|
||||||
|
<lineBasicMaterial color="#60a5fa" />
|
||||||
|
</lineSegments>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* 3. Fechamentos de Fundo e Laterais (com suporte a fresta/abertura) */}
|
||||||
|
{/* Parede de Fundo (Traseira Z = +halfD) */}
|
||||||
|
{renderWallWithGap(width, height, backClosure, openingPos, 0, [0, 0, halfD])}
|
||||||
|
|
||||||
|
{/* Parede Lateral Esquerda (X = -halfW) */}
|
||||||
|
{renderWallWithGap(depth, height, leftClosure, 'distributed', Math.PI / 2, [-halfW, 0, 0])}
|
||||||
|
|
||||||
|
{/* Parede Lateral Direita (X = +halfW) */}
|
||||||
|
{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) */}
|
||||||
|
<WindArrow start={[0, height * 0.5, -halfD - 3]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
|
||||||
|
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 3]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
||||||
|
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 3]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
||||||
|
|
||||||
|
{/* Linhas de corrente sobre o telhado */}
|
||||||
|
<WindArrow start={[0, height + 1, -halfD - 2]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
||||||
|
|
||||||
|
{/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior! */}
|
||||||
|
{backClosure > 0 && backClosure < 100 && openingPos === 'top' && (
|
||||||
|
<>
|
||||||
|
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
||||||
|
<Text position={[0, height + 0.3, halfD]} fontSize={0.4} color="#34d399">
|
||||||
|
Alívio (Fresta Topo)
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fresta na base */}
|
||||||
|
{backClosure > 0 && backClosure < 100 && openingPos === 'bottom' && (
|
||||||
|
<>
|
||||||
|
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
||||||
|
<Text position={[0, 1.2, halfD]} fontSize={0.4} color="#facc15">
|
||||||
|
Vão Livre Inferior
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* Textos Informativos 3D */}
|
||||||
|
<Text position={[0, height + slopeRise + 0.8, 0]} fontSize={0.45} color="#e2e8f0">
|
||||||
|
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||||
|
</Text>
|
||||||
|
<Text position={[0, 0.3, -halfD - 1]} fontSize={0.35} color="#38bdf8">
|
||||||
|
VENTO FRONTAL
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shelter3D;
|
||||||
@@ -10,6 +10,7 @@ import { dynamicsManual } from './dynamics';
|
|||||||
import { isolatedRoofManual } from './isolated-roof';
|
import { isolatedRoofManual } from './isolated-roof';
|
||||||
import { piperackManual } from './piperack';
|
import { piperackManual } from './piperack';
|
||||||
import { calcCaManual } from './calc_ca';
|
import { calcCaManual } from './calc_ca';
|
||||||
|
import { shelterManual } from './shelter';
|
||||||
|
|
||||||
export const manuals: Record<string, typeof bridgeManual> = {
|
export const manuals: Record<string, typeof bridgeManual> = {
|
||||||
bridge: bridgeManual,
|
bridge: bridgeManual,
|
||||||
@@ -24,4 +25,5 @@ export const manuals: Record<string, typeof bridgeManual> = {
|
|||||||
'isolated-roof': isolatedRoofManual,
|
'isolated-roof': isolatedRoofManual,
|
||||||
piperack: piperackManual,
|
piperack: piperackManual,
|
||||||
calc_ca: calcCaManual,
|
calc_ca: calcCaManual,
|
||||||
|
shelter: shelterManual,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const shelterManual = {
|
||||||
|
title: 'Abrigos, Marquises e Pórticos Fechados',
|
||||||
|
intro: 'Ação do vento em coberturas apoiadas ou em balanço com fechamentos laterais/fundo e aberturas parciais (NBR 6123:2023 — Tabelas 23, 24, 25 e Seção 6.3).',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. O Conceito das 4 Condições Topológicas',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Abrigos, marquises e garagens semiabertas combinam o escoamento ao redor da cobertura (<strong>Tabelas 24/25</strong>) com o efeito de retenção ou canalização do vento gerado pelas paredes de fechamento (<strong>Seção 6.3 — Pressão Interna</strong>).
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1.5 text-sm">
|
||||||
|
<li>
|
||||||
|
<strong>Condição 1 (Estanque em 3 lados - Fundo e Laterais fechados):</strong> O vento frontal fica contido como em uma armadilha, gerando <strong>alta pressão interna positiva (Cpi até +0,8)</strong>. Essa pressão empurra o telhado para cima, somando-se à sucção externa e gerando o <strong>pico máximo de arrancamento</strong>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Condição 2 (Fundo fechado e Laterais abertas - Marquise encostada):</strong> A parede traseira funciona como anteparo de estagnação. O fluxo escapa lateralmente, mas mantém sobrepressão moderada sob a cobertura na zona junto ao fundo.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Condição 3 (Fundo aberto e Laterais fechadas - Túnel / Passarela):</strong> A aceleração do ar dentro do túnel (Efeito Venturi) gera <strong>depressão (Cpi negativo)</strong> na face inferior da cobertura, reduzindo o arrancamento vertical, porém elevando o arrasto de atrito longitudinal.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Condição 4 (Totalmente aberto em 4 lados):</strong> Recai no regime puro de Cobertura Isolada (Tabela 24 da norma).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Fechamentos Parciais (%) e Posição da Abertura',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O controle percentual de fechamento de cada face permite calcular a <strong>permeabilidade μ</strong> e a pressão interna efetiva conforme a Seção 6.3:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-lg font-mono text-xs border border-border">
|
||||||
|
Fz (Arrancamento) = q · [ Ce_sup(-) - Cpi_inf(+) ] · A_roof
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold text-primary">
|
||||||
|
Por que a posição da abertura (Topo vs. Base) é tão importante?
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1.5 text-sm">
|
||||||
|
<li>
|
||||||
|
<strong>Abertura no Topo (Fresta Superior / Lanternim):</strong> O ar sob pressão escapa diretamente pela fresta entre a parede e o telhado. Isso alivia a pressão interna sob a cobertura, reduziando a força vertical de arrancamento em até <strong>25% a 30%</strong>!
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Abertura na Base (Vão Livre Inferior):</strong> O ar circula rente ao solo, transformando a parede traseira em uma <em>placa suspensa</em> (Tabela 23). Isso eleva o centro de pressão horizontal e aumenta o momento fletor na base dos pilares do pórtico.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Aplicação em Pórticos Metálicos (Croqui CORTE A-A)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Em estruturas compostas por perfis metálicos (como <strong>Tubos Quadrados 150x150x8.8 mm</strong> em balanço), o cálculo combina:
|
||||||
|
</p>
|
||||||
|
<ol className="list-decimal pl-5 space-y-1 text-sm">
|
||||||
|
<li>Ação vertical de sucção na cobertura ($F_z$, em kN).</li>
|
||||||
|
<li>Empuxo horizontal frontal sobre a parede e testeira ($F_x$, em kN).</li>
|
||||||
|
<li>Ação aerodinâmica linear sobre os perfis do pórtico ($\text{kN/m}$), calculada pela Tabela 26/27.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -59,6 +59,7 @@ const translations: Dict = {
|
|||||||
nav_bridge: { 'pt-BR': 'Pontes', 'en-US': 'Bridges' },
|
nav_bridge: { 'pt-BR': 'Pontes', 'en-US': 'Bridges' },
|
||||||
nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
||||||
nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' },
|
nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' },
|
||||||
|
nav_shelter: { 'pt-BR': 'Abrigos / Pórticos', 'en-US': 'Shelters/Canopies' },
|
||||||
nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' },
|
nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' },
|
||||||
nav_calc_ca: { 'pt-BR': 'Calc_Ca', 'en-US': 'Calc_Ca' },
|
nav_calc_ca: { 'pt-BR': 'Calc_Ca', 'en-US': 'Calc_Ca' },
|
||||||
nav_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' },
|
nav_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' },
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ShelterCondition = 'cond1' | 'cond2' | 'cond3' | 'cond4';
|
||||||
|
export type OpeningPosition = 'top' | 'bottom' | 'distributed';
|
||||||
|
|
||||||
|
export interface ShelterInput {
|
||||||
|
/** Condição topológica padrão (1=Estanque 3 lados, 2=Fundo fechado, 3=Túnel, 4=Aberto) */
|
||||||
|
condition: ShelterCondition;
|
||||||
|
/** Comprimento do balanço / profundidade (m) */
|
||||||
|
depth: number;
|
||||||
|
/** Largura do abrigo (m) */
|
||||||
|
width: number;
|
||||||
|
/** Altura livre / altura do pilar (m) */
|
||||||
|
height: number;
|
||||||
|
/** Inclinação da cobertura (graus, ex: 0 a 30) */
|
||||||
|
theta: number;
|
||||||
|
/** Percentual de fechamento da parede de fundo (0 a 100) */
|
||||||
|
backClosure: number;
|
||||||
|
/** Percentual de fechamento da parede lateral esquerda (0 a 100) */
|
||||||
|
leftClosure: number;
|
||||||
|
/** Percentual de fechamento da parede lateral direita (0 a 100) */
|
||||||
|
rightClosure: number;
|
||||||
|
/** Posição da abertura na parede de fundo ('top' | 'bottom' | 'distributed') */
|
||||||
|
openingPos: OpeningPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShelterResult {
|
||||||
|
/** Coeficiente de pressão superior da cobertura (Ce superior) */
|
||||||
|
cpeTop: number;
|
||||||
|
/** Coeficiente de pressão interna efetivo sob a cobertura (Cpi inferior) */
|
||||||
|
cpiBottom: number;
|
||||||
|
/** Coeficiente de força líquida vertical na cobertura (Cnf = cpeTop - cpiBottom) */
|
||||||
|
cnfVertical: number;
|
||||||
|
/** Coeficiente de força horizontal na parede de fundo */
|
||||||
|
cfBack: number;
|
||||||
|
/** Coeficiente de força horizontal nas paredes laterais */
|
||||||
|
cfSide: number;
|
||||||
|
/** Forças totais resultantes (kN) */
|
||||||
|
forces: {
|
||||||
|
/** Força vertical de arrancamento na cobertura (+ para cima) (kN) */
|
||||||
|
fzUp: number;
|
||||||
|
/** Força vertical para baixo (kN) */
|
||||||
|
fzDown: number;
|
||||||
|
/** Força horizontal frontal/empuxo contra a parede de fundo (kN) */
|
||||||
|
fxBack: number;
|
||||||
|
/** Força horizontal lateral contra paredes (kN) */
|
||||||
|
fySide: number;
|
||||||
|
/** Força de atrito no plano da cobertura (kN) */
|
||||||
|
fFriction: number;
|
||||||
|
};
|
||||||
|
/** Carga linear estimada nos pórticos metálicos principais (kN/m) (ex: Tubo 150x150) */
|
||||||
|
lineLoads: {
|
||||||
|
/** Carga horizontal em cada pilar (kN/m) */
|
||||||
|
columnLoad: number;
|
||||||
|
/** 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 (%) */
|
||||||
|
reliefPercentage: number;
|
||||||
|
/** Explicação normativa do comportamento */
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula os coeficientes e forças para Abrigos, Marquises e Pórticos.
|
||||||
|
*/
|
||||||
|
export function calculateShelterWind(input: ShelterInput, q: number): ShelterResult {
|
||||||
|
const {
|
||||||
|
condition,
|
||||||
|
depth,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
theta,
|
||||||
|
backClosure,
|
||||||
|
leftClosure,
|
||||||
|
rightClosure,
|
||||||
|
openingPos,
|
||||||
|
} = input;
|
||||||
|
|
||||||
|
const areaRoof = depth * width;
|
||||||
|
const areaBack = width * height;
|
||||||
|
const areaSide = depth * height;
|
||||||
|
|
||||||
|
// Razões de permeabilidade (0 = aberto, 1 = fechado estanque)
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
// Condição 4: Livre (apenas obstrução leve ou nula)
|
||||||
|
baseCpi = -0.1 * bClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Efeito da posição da abertura (Topo vs Base vs Distribuída)
|
||||||
|
let reliefPercentage = 0;
|
||||||
|
let effectiveCpi = baseCpi;
|
||||||
|
|
||||||
|
if (bClose > 0.1 && bClose < 0.98) {
|
||||||
|
if (openingPos === 'top') {
|
||||||
|
// Fresta superior permite o escape de sobrepressão debaixo do telhado
|
||||||
|
// Reduz o Cpi positivo em até 30%, gerando alívio de arrancamento na cobertura
|
||||||
|
reliefPercentage = Math.round(25 * (1 - bClose));
|
||||||
|
effectiveCpi = baseCpi * (1 - reliefPercentage / 100);
|
||||||
|
} else if (openingPos === 'bottom') {
|
||||||
|
// Abertura inferior: o ar flui embaixo, mantendo pressão sob o telhado e aumentando momento na base
|
||||||
|
reliefPercentage = 0;
|
||||||
|
effectiveCpi = baseCpi * 1.05;
|
||||||
|
} else {
|
||||||
|
reliefPercentage = 5;
|
||||||
|
effectiveCpi = baseCpi * 0.95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 4. Coeficientes em paredes de fundo e laterais (Tabela 23 / 6)
|
||||||
|
const cfBack = 1.3 * bClose;
|
||||||
|
const cfSide = 0.9 * ((lClose + rClose) / 2);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 6. Carga linear em pórticos (assumindo 2 pórticos principais como no croqui, ex: vão b/2)
|
||||||
|
// Carga no pilar (kN/m)
|
||||||
|
const numPorticos = 2;
|
||||||
|
const columnLoad = (fxBack / height / numPorticos) + (q * 1.8 * 0.15); // arrasto do tubo 150x150
|
||||||
|
const beamLoad = (fzUp / depth / numPorticos);
|
||||||
|
|
||||||
|
// Texto explicativo do regime
|
||||||
|
let statusText = '';
|
||||||
|
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.';
|
||||||
|
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.';
|
||||||
|
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.';
|
||||||
|
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.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpeTop: Number(cpeTop.toFixed(2)),
|
||||||
|
cpiBottom: Number(effectiveCpi.toFixed(2)),
|
||||||
|
cnfVertical: Number(cnfUp.toFixed(2)),
|
||||||
|
cfBack: Number(cfBack.toFixed(2)),
|
||||||
|
cfSide: Number(cfSide.toFixed(2)),
|
||||||
|
forces: {
|
||||||
|
fzUp: Number(fzUp.toFixed(2)),
|
||||||
|
fzDown: Number(fzDown.toFixed(2)),
|
||||||
|
fxBack: Number(fxBack.toFixed(2)),
|
||||||
|
fySide: Number(fySide.toFixed(2)),
|
||||||
|
fFriction: Number(fFriction.toFixed(3)),
|
||||||
|
},
|
||||||
|
lineLoads: {
|
||||||
|
columnLoad: Number(columnLoad.toFixed(2)),
|
||||||
|
beamLoad: Number(beamLoad.toFixed(2)),
|
||||||
|
},
|
||||||
|
reliefPercentage,
|
||||||
|
statusText,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useWindStore } from '@/store/appStore';
|
||||||
|
import {
|
||||||
|
calculateShelterWind,
|
||||||
|
type ShelterCondition,
|
||||||
|
type OpeningPosition,
|
||||||
|
} from '@/lib/nbr-tables/table-shelters';
|
||||||
|
import Shelter3D from '@/components/three/Shelter3D';
|
||||||
|
import SceneCanvas from '@/components/SceneCanvas';
|
||||||
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
|
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||||
|
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||||
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
import { Eye, Box, Layers, ShieldCheck, ArrowUpRight, CheckCircle2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const conditionLabels: Record<ShelterCondition, string> = {
|
||||||
|
cond1: '1. Estanque 3 lados (Fundo + Lados)',
|
||||||
|
cond2: '2. Fundo Fechado + Lados Abertos',
|
||||||
|
cond3: '3. Fundo Aberto + Lados Fechados (Túnel)',
|
||||||
|
cond4: '4. Totalmente Livre (4 lados abertos)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShelterModule: React.FC = () => {
|
||||||
|
const { q } = useWindStore();
|
||||||
|
const [condition, setCondition] = useState<ShelterCondition>('cond1');
|
||||||
|
const [depth, setDepth] = useState(6);
|
||||||
|
const [width, setWidth] = useState(10);
|
||||||
|
const [height, setHeight] = useState(4.8);
|
||||||
|
const [theta, setTheta] = useState(5);
|
||||||
|
const [backClosure, setBackClosure] = useState(85);
|
||||||
|
const [leftClosure, setLeftClosure] = useState(100);
|
||||||
|
const [rightClosure, setRightClosure] = useState(100);
|
||||||
|
const [openingPos, setOpeningPos] = useState<OpeningPosition>('top');
|
||||||
|
const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d');
|
||||||
|
|
||||||
|
// Ajusta percentuais ao trocar as 4 condições rápidas
|
||||||
|
const handleConditionChange = (cond: ShelterCondition) => {
|
||||||
|
setCondition(cond);
|
||||||
|
if (cond === 'cond1') {
|
||||||
|
setBackClosure(100);
|
||||||
|
setLeftClosure(100);
|
||||||
|
setRightClosure(100);
|
||||||
|
} else if (cond === 'cond2') {
|
||||||
|
setBackClosure(100);
|
||||||
|
setLeftClosure(0);
|
||||||
|
setRightClosure(0);
|
||||||
|
} else if (cond === 'cond3') {
|
||||||
|
setBackClosure(0);
|
||||||
|
setLeftClosure(100);
|
||||||
|
setRightClosure(100);
|
||||||
|
} else if (cond === 'cond4') {
|
||||||
|
setBackClosure(0);
|
||||||
|
setLeftClosure(0);
|
||||||
|
setRightClosure(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = useMemo(() => {
|
||||||
|
return calculateShelterWind(
|
||||||
|
{
|
||||||
|
condition,
|
||||||
|
depth,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
theta,
|
||||||
|
backClosure,
|
||||||
|
leftClosure,
|
||||||
|
rightClosure,
|
||||||
|
openingPos,
|
||||||
|
},
|
||||||
|
q
|
||||||
|
);
|
||||||
|
}, [condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, q]);
|
||||||
|
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
const sections: GenericPDFSection[] = [
|
||||||
|
{
|
||||||
|
title: 'Geometria do Abrigo e Fechamentos',
|
||||||
|
type: 'grid',
|
||||||
|
gridItems: [
|
||||||
|
{ label: 'Condição Topológica', value: conditionLabels[condition] },
|
||||||
|
{ 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: 'Fechamento Lateral Esq / Dir', value: `${leftClosure}% / ${rightClosure}%` },
|
||||||
|
{ label: 'Posição da Abertura', value: openingPos === 'top' ? 'Fresta Topo (Alívio)' : openingPos === 'bottom' ? 'Vão Inferior' : 'Distribuída' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Coeficientes Aerodinâmicos (NBR 6123)',
|
||||||
|
type: 'grid',
|
||||||
|
gridItems: [
|
||||||
|
{ label: 'Ce Superior (Cobertura)', value: result.cpeTop.toString() },
|
||||||
|
{ label: 'Cpi Inferior (Efetivo)', value: result.cpiBottom.toString() },
|
||||||
|
{ label: 'Cnf Líquido (Arrancamento)', value: result.cnfVertical.toString() },
|
||||||
|
{ label: 'Cf Parede Fundo (Tab. 23)', value: result.cfBack.toString() },
|
||||||
|
{ label: 'Alívio por Fresta Superior', value: `${result.reliefPercentage}%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Forças Resultantes (kN)',
|
||||||
|
type: 'grid',
|
||||||
|
gridItems: [
|
||||||
|
{ label: 'Força Vertical (Fz - Arrancamento)', value: `${result.forces.fzUp} kN` },
|
||||||
|
{ label: 'Empuxo Frontal Fundo (Fx)', value: `${result.forces.fxBack} kN` },
|
||||||
|
{ label: 'Empuxo nas Laterais (Fy)', value: `${result.forces.fySide} kN` },
|
||||||
|
{ label: 'Atrito na Cobertura', value: `${result.forces.fFriction} kN` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Carga Linear nos Pórticos (kN/m)',
|
||||||
|
type: 'grid',
|
||||||
|
gridItems: [
|
||||||
|
{ label: 'Carga no Pilar (ex: Tubo 150x150)', value: `${result.lineLoads.columnLoad} kN/m` },
|
||||||
|
{ label: 'Carga na Viga em Balanço', value: `${result.lineLoads.beamLoad} kN/m` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
exportGenericToPDF('Abrigo / Pórtico Fechado', sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||||
|
{/* Coluna Esquerda: Controles do Abrigo */}
|
||||||
|
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||||
|
<Card className="shadow-sm border-border">
|
||||||
|
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Layers className="size-5 text-primary" />
|
||||||
|
Abrigos e Pórticos
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Pórticos em balanço, marquises e garagens com fechamentos (Tab. 23-25 e sec. 6.3).
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<SaveModuleDialog
|
||||||
|
moduleType="shelter"
|
||||||
|
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos }}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-5">
|
||||||
|
<WindParametersSummary />
|
||||||
|
|
||||||
|
{/* Seletor Rápido de 4 Condições Topológicas */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Condições Topológicas
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{(['cond1', 'cond2', 'cond3', 'cond4'] as ShelterCondition[]).map((c) => (
|
||||||
|
<Button
|
||||||
|
key={c}
|
||||||
|
type="button"
|
||||||
|
variant={condition === c ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
className="h-auto py-2 px-2 text-xs text-left justify-start font-normal leading-tight"
|
||||||
|
onClick={() => handleConditionChange(c)}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className={cn("size-3.5 mr-1.5 shrink-0", condition === c ? "text-primary-foreground" : "text-muted-foreground")} />
|
||||||
|
<span>{conditionLabels[c].split(' (')[0]}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controle de Posição da Abertura (Topo / Base / Distribuída) */}
|
||||||
|
<div className="space-y-2 pt-1 border-t">
|
||||||
|
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Posição da Abertura no Fundo
|
||||||
|
</label>
|
||||||
|
<Select value={openingPos} onValueChange={(v) => setOpeningPos(v as OpeningPosition)}>
|
||||||
|
<SelectTrigger className="text-xs h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="top">Fresta no Topo (Alívio de Arrancamento)</SelectItem>
|
||||||
|
<SelectItem value="bottom">Vão na Base (Comportamento Placa Suspensa)</SelectItem>
|
||||||
|
<SelectItem value="distributed">Abertura Uniformemente Distribuída</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sliders de Fechamento Parcial (%) */}
|
||||||
|
<div className="space-y-3 pt-1 border-t">
|
||||||
|
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Permeabilidade / Fechamento (%)
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Parede de Fundo:</span>
|
||||||
|
<span className="font-mono font-medium">{backClosure}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={0} max={100} step={5} value={[backClosure]} onValueChange={(v) => setBackClosure(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Lateral Esq:</span>
|
||||||
|
<span className="font-mono">{leftClosure}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={0} max={100} step={5} value={[leftClosure]} onValueChange={(v) => setLeftClosure(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Lateral Dir:</span>
|
||||||
|
<span className="font-mono">{rightClosure}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={0} max={100} step={5} value={[rightClosure]} onValueChange={(v) => setRightClosure(v[0])} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sliders Geométricos */}
|
||||||
|
<div className="space-y-3 pt-1 border-t">
|
||||||
|
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Dimensões e Inclinação
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Vão do Balanço (d):</span>
|
||||||
|
<span className="font-mono font-medium">{depth} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={3} max={20} step={0.5} value={[depth]} onValueChange={(v) => setDepth(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Altura do Pilar (h):</span>
|
||||||
|
<span className="font-mono font-medium">{height} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={2.5} max={12} step={0.1} value={[height]} onValueChange={(v) => setHeight(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Largura do Abrigo (b):</span>
|
||||||
|
<span className="font-mono font-medium">{width} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={4} max={30} step={1} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Inclinação Cobertura (θ):</span>
|
||||||
|
<span className="font-mono font-medium">{theta}°</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Normativo */}
|
||||||
|
<div className="rounded-lg border bg-secondary/30 p-3 text-xs space-y-1.5">
|
||||||
|
<div className="flex items-center gap-1.5 font-semibold text-foreground">
|
||||||
|
<ShieldCheck className="size-4 text-emerald-500 shrink-0" />
|
||||||
|
<span>Norma NBR 6123:2023 — Sec. 6.3 & Tab. 23-25</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">{result.statusText}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Coluna Direita: 3D, Corte A-A e Quadro de Ações */}
|
||||||
|
<div className="flex-1 flex flex-col gap-4">
|
||||||
|
{/* Visualizador 3D com trava, manual (i) e corte em elevação */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-[460px] bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
|
<div className="absolute top-4 left-4 z-10 flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm">
|
||||||
|
θ = {theta}°
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm">
|
||||||
|
Fundo: {backClosure}% ({openingPos === 'top' ? 'Fresta Topo' : openingPos === 'bottom' ? 'Vão Base' : 'Distrib.'})
|
||||||
|
</Badge>
|
||||||
|
{result.reliefPercentage > 0 && (
|
||||||
|
<Badge className="bg-emerald-600/90 hover:bg-emerald-600 text-white border-none shadow-sm flex items-center gap-1">
|
||||||
|
<ArrowUpRight className="size-3.5" />
|
||||||
|
Alívio Arrancamento: -{result.reliefPercentage}%
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
||||||
|
{/* Botão de Corte em Elevação (Corte A-A) vs 3D */}
|
||||||
|
<div className="bg-background/90 border border-border rounded-full p-1 shadow-sm flex items-center">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={viewMode === '3d' ? 'default' : 'ghost'}
|
||||||
|
className="h-7 px-3 text-xs rounded-full"
|
||||||
|
onClick={() => setViewMode('3d')}
|
||||||
|
>
|
||||||
|
<Box className="size-3.5 mr-1" />
|
||||||
|
Vista 3D
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={viewMode === 'elevation' ? 'default' : 'ghost'}
|
||||||
|
className="h-7 px-3 text-xs rounded-full"
|
||||||
|
onClick={() => setViewMode('elevation')}
|
||||||
|
>
|
||||||
|
<Eye className="size-3.5 mr-1" />
|
||||||
|
Corte A-A (Frestas)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ícone (i) Manual Didático NBR 6123 */}
|
||||||
|
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SceneCanvas
|
||||||
|
moduleId="shelter"
|
||||||
|
camera={{ position: [12, 6, 14], fov: 45 }}
|
||||||
|
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||||
|
>
|
||||||
|
<ambientLight intensity={0.7} />
|
||||||
|
<directionalLight position={[10, 20, 15]} intensity={1.2} />
|
||||||
|
<Shelter3D
|
||||||
|
condition={condition}
|
||||||
|
depth={depth}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
theta={theta}
|
||||||
|
backClosure={backClosure}
|
||||||
|
leftClosure={leftClosure}
|
||||||
|
rightClosure={rightClosure}
|
||||||
|
openingPos={openingPos}
|
||||||
|
viewMode={viewMode}
|
||||||
|
/>
|
||||||
|
</SceneCanvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quadro Superior de Resultados na Base do 3D */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<Card className="bg-card border-border">
|
||||||
|
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">F. Vertical (Fz - Arrancamento)</span>
|
||||||
|
<div className="flex items-baseline justify-between mt-2">
|
||||||
|
<span className="text-xl font-bold font-mono text-primary">{result.forces.fzUp} kN</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">Cnf = {result.cnfVertical}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1">
|
||||||
|
{result.reliefPercentage > 0 ? `Com ${result.reliefPercentage}% de alívio por fresta` : 'Sem alívio por fresta superior'}
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-card border-border">
|
||||||
|
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">Empuxo no Fundo (Fx Traseira)</span>
|
||||||
|
<div className="flex items-baseline justify-between mt-2">
|
||||||
|
<span className="text-xl font-bold font-mono text-foreground">{result.forces.fxBack} kN</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">Cf = {result.cfBack}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1">
|
||||||
|
Parede com {backClosure}% de estanqueidade
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-card border-border">
|
||||||
|
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">Carga no Pilar (ex: Tubo 150x150)</span>
|
||||||
|
<div className="flex items-baseline justify-between mt-2">
|
||||||
|
<span className="text-xl font-bold font-mono text-emerald-600 dark:text-emerald-400">
|
||||||
|
{result.lineLoads.columnLoad} kN/m
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">h = {height} m</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1">
|
||||||
|
Ação direta + reação horizontal por pilar
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-card border-border">
|
||||||
|
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">Carga na Viga em Balanço</span>
|
||||||
|
<div className="flex items-baseline justify-between mt-2">
|
||||||
|
<span className="text-xl font-bold font-mono text-sky-600 dark:text-sky-400">
|
||||||
|
{result.lineLoads.beamLoad} kN/m
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">d = {depth} m</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1">
|
||||||
|
Sucção distribuída por braço principal
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rodapé com Exportação e Captura */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 border-t pt-4">
|
||||||
|
<SceneCapturePanel sceneId="shelter-module" />
|
||||||
|
<ExportMenu onExportPDF={handleExportPDF} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShelterModule;
|
||||||
Reference in New Issue
Block a user