feat: adicionar super manual didatico com fluxos 2d e sheet panel
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { BookOpen, HelpCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import { manuals } from '@/data/manuals';
|
||||
import { WindFlowGrid2D } from './WindFlowGrid2D';
|
||||
|
||||
interface EducationalManualProps {
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export function EducationalManual({ type, params }: EducationalManualProps) {
|
||||
const manual = manuals[type];
|
||||
|
||||
if (!manual) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-10 bg-background/80 backdrop-blur-xs border border-border shadow-md hover:bg-accent/80 transition-all rounded-full flex items-center justify-center"
|
||||
title="Manual Didático da Norma NBR 6123"
|
||||
>
|
||||
<BookOpen className="size-5 text-primary animate-pulse" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="overflow-y-auto w-full sm:max-w-xl p-6 flex flex-col gap-6 border-l border-border bg-card">
|
||||
<SheetHeader className="border-b border-border pb-4 flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<HelpCircle className="size-5" />
|
||||
<SheetTitle className="text-xl font-bold tracking-tight">{manual.title}</SheetTitle>
|
||||
</div>
|
||||
<SheetDescription className="text-sm text-muted-foreground leading-relaxed">
|
||||
{manual.intro}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{/* Simulador Interativo 2D no topo do manual */}
|
||||
<WindFlowGrid2D type={type} params={params} />
|
||||
|
||||
{/* Seções de Texto e Fórmulas */}
|
||||
<div className="flex flex-col gap-6 text-foreground leading-relaxed">
|
||||
{manual.sections.map((section, idx) => (
|
||||
<div key={idx} className="flex flex-col gap-3 bg-muted/20 p-4 rounded-xl border border-border/40">
|
||||
<h3 className="text-base font-bold text-primary flex items-center gap-2 border-b border-border/60 pb-1.5">
|
||||
{section.title}
|
||||
</h3>
|
||||
<div className="text-sm prose prose-neutral dark:prose-invert max-w-none text-muted-foreground">
|
||||
{section.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto border-t border-border pt-4 text-center">
|
||||
<p className="text-[10px] text-muted-foreground font-mono">
|
||||
VentoApp • Referência: ABNT NBR 6123:2023
|
||||
</p>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -403,6 +403,7 @@ interface IsolatedRoofProps {
|
||||
type: 'shed' | 'gable';
|
||||
theta: number;
|
||||
height: number;
|
||||
width: number;
|
||||
depth: number;
|
||||
cpeWindward: number;
|
||||
cpeLeeward: number;
|
||||
@@ -410,7 +411,7 @@ interface IsolatedRoofProps {
|
||||
forceKN: number;
|
||||
}
|
||||
|
||||
function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) {
|
||||
function IsolatedRoofDiagram({ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) {
|
||||
const vw = 400, vh = 300;
|
||||
const s = Math.min((vw - 100) / depth, (vh - 80) / (height + depth * Math.tan((theta * Math.PI) / 180)));
|
||||
const cx = vw / 2, ground = vh - 40;
|
||||
@@ -451,7 +452,7 @@ function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeewa
|
||||
<text x={cx + 12} y={ground - h - rise - 12 - forceLen(forceKN) / 2} fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
|
||||
</g>
|
||||
)}
|
||||
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | prof. = {depth}m</text>
|
||||
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | b = {width}m | l = {depth}m</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,15 +8,35 @@ interface SceneCanvasProps extends CanvasProps {
|
||||
fallback: ReactNode;
|
||||
}
|
||||
|
||||
function CanvasInner({ fallback: _, ...canvasProps }: SceneCanvasProps) {
|
||||
function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) {
|
||||
const registerCanvas = useCaptureStore((s) => s.registerCanvas);
|
||||
const unregisterCanvas = useCaptureStore((s) => s.unregisterCanvas);
|
||||
|
||||
useEffect(() => () => unregisterCanvas(), [unregisterCanvas]);
|
||||
|
||||
// Suprimir avisos de depreciação conhecidos do Three.js (r185) que o R3F ainda aciona
|
||||
useEffect(() => {
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
if (typeof args[0] === 'string' && (args[0].includes('THREE.Clock') || args[0].includes('THREE.WebGLShadowMap'))) {
|
||||
return;
|
||||
}
|
||||
originalWarn(...args);
|
||||
};
|
||||
return () => {
|
||||
console.warn = originalWarn;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// O R3F v9 usa PCFSoftShadowMap por padrão, que está deprecado no Three r185.
|
||||
// Mudar para PCFShadowMap (1) via propriedade shadows.
|
||||
const shadowConfig = shadows === true ? { type: 1 as any } : shadows;
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
frameloop="demand"
|
||||
{...canvasProps}
|
||||
shadows={shadowConfig}
|
||||
onCreated={(state) => {
|
||||
registerCanvas(state.gl.domElement);
|
||||
canvasProps.onCreated?.(state);
|
||||
|
||||
@@ -32,27 +32,27 @@ function PressureArrow({
|
||||
}) {
|
||||
const { q } = useWindStore();
|
||||
const force = p * q; // kN/m2
|
||||
if (Math.abs(force) < 0.05) return null;
|
||||
|
||||
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
|
||||
const isPressure = p > 0;
|
||||
const color = isPressure ? '#3b82f6' : '#ef4444';
|
||||
|
||||
|
||||
const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]);
|
||||
const centerVec = useMemo(() => new THREE.Vector3(...center), [center]);
|
||||
|
||||
const dir = isPressure ? normVec.clone().negate() : normVec.clone();
|
||||
|
||||
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
|
||||
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
|
||||
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||
|
||||
const isPressure = p > 0;
|
||||
const dir = useMemo(() => isPressure ? normVec.clone().negate() : normVec.clone(), [isPressure, normVec]);
|
||||
|
||||
const quat = useMemo(() => {
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [dir]);
|
||||
|
||||
|
||||
if (Math.abs(force) < 0.05) return null;
|
||||
|
||||
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
|
||||
const color = isPressure ? '#3b82f6' : '#ef4444';
|
||||
|
||||
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
|
||||
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
|
||||
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||
|
||||
const headLen = Math.min(0.4, length * 0.4);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
|
||||
|
||||
interface WindFlowGrid2DProps {
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
|
||||
// Estilo global de animação inserido no próprio SVG
|
||||
const styleBlock = (
|
||||
<style>{`
|
||||
@keyframes windFlow {
|
||||
0% { stroke-dashoffset: 36; }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
@keyframes vortexRotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes vibration {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.wind-line {
|
||||
stroke: #60a5fa;
|
||||
stroke-width: 1.8;
|
||||
stroke-dasharray: 8 16;
|
||||
animation: windFlow 1.8s linear infinite;
|
||||
fill: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.wind-line-fast {
|
||||
stroke: #38bdf8;
|
||||
stroke-width: 2.2;
|
||||
stroke-dasharray: 6 12;
|
||||
animation: windFlow 0.9s linear infinite;
|
||||
fill: none;
|
||||
}
|
||||
.wind-line-slow {
|
||||
stroke: #93c5fd;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 10 20;
|
||||
animation: windFlow 3s linear infinite;
|
||||
fill: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.vortex-line {
|
||||
stroke: #f87171;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4 8;
|
||||
animation: windFlow 2.5s linear infinite;
|
||||
fill: none;
|
||||
}
|
||||
.vortex-spin-cw {
|
||||
transform-origin: center;
|
||||
animation: vortexRotate 3s linear infinite;
|
||||
}
|
||||
.vortex-spin-ccw {
|
||||
transform-origin: center;
|
||||
animation: vortexRotate 3s linear infinite reverse;
|
||||
}
|
||||
.vibe-cyl {
|
||||
animation: vibration 1.5s ease-in-out infinite;
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
|
||||
// Renderizadores específicos de fluxo ativo com base nos sliders da página
|
||||
const renderActiveSimulation = () => {
|
||||
switch (type) {
|
||||
case 'bridge': {
|
||||
const alpha = Number(params.alpha || 0);
|
||||
// O vento muda de ângulo
|
||||
const rotationAngle = alpha * 3; // amplifica o ângulo para ficar visível
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
|
||||
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
{/* Linhas de vento de fundo */}
|
||||
<g transform={`rotate(${rotationAngle}, 200, 100)`}>
|
||||
<path d="M -50,30 L 450,30" className="wind-line-slow" />
|
||||
<path d="M -50,70 Q 150,50 450,70" className="wind-line" />
|
||||
<path d="M -50,130 Q 150,150 450,130" className="wind-line" />
|
||||
<path d="M -50,170 L 450,170" className="wind-line-slow" />
|
||||
|
||||
{/* Linhas aceleradas acima e abaixo */}
|
||||
<path d="M -50,90 Q 200,60 450,90" className="wind-line-fast" />
|
||||
<path d="M -50,110 Q 200,140 450,110" className="wind-line-fast" />
|
||||
</g>
|
||||
|
||||
{/* Tabuleiro da ponte */}
|
||||
<g transform="translate(150, 85)">
|
||||
<rect x="0" y="0" width="100" height="30" rx="4" fill="#1e293b" stroke="#334155" strokeWidth="2" />
|
||||
{/* Barreiras de proteção */}
|
||||
<rect x="5" y="-6" width="6" height="6" fill="#64748b" />
|
||||
<rect x="89" y="-6" width="6" height="6" fill="#64748b" />
|
||||
</g>
|
||||
|
||||
{/* Vetores de Força baseados no ângulo */}
|
||||
{alpha !== 0 && (
|
||||
<g>
|
||||
{/* Vetor de sustentação (Fz) */}
|
||||
<path
|
||||
d={alpha > 0 ? "M 200,75 L 200,30" : "M 200,125 L 200,170"}
|
||||
stroke={alpha > 0 ? "#10b981" : "#ef4444"}
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
markerEnd="url(#arrow)"
|
||||
/>
|
||||
<text x="210" y={alpha > 0 ? 45 : 160} fill={alpha > 0 ? "#10b981" : "#ef4444"} className="text-xs font-bold font-mono">
|
||||
{alpha > 0 ? 'Sustentação (+Fz)' : 'Downforce (-Fz)'}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Marcadores SVG de setas */}
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="context-stroke" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
|
||||
Ângulo: {alpha}º
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'tower': {
|
||||
const section = params.section || 'square';
|
||||
const alphaWind = Number(params.alphaWind || 0);
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
|
||||
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
|
||||
{/* Linhas de vento fluindo */}
|
||||
<g>
|
||||
<path d="M -20,20 L 420,20" className="wind-line-slow" />
|
||||
<path d="M -20,60 C 150,60 170,40 230,40 C 270,40 320,60 420,60" className="wind-line" />
|
||||
<path d="M -20,140 C 150,140 170,160 230,160 C 270,160 320,140 420,140" className="wind-line" />
|
||||
<path d="M -20,180 L 420,180" className="wind-line-slow" />
|
||||
|
||||
{/* Turbulência traseira (esteira) */}
|
||||
<path d="M 230,90 Q 300,75 420,90" className="vortex-line" />
|
||||
<path d="M 230,110 Q 300,125 420,110" className="vortex-line" />
|
||||
</g>
|
||||
|
||||
{/* Perfil da Seção da Torre (Centro) */}
|
||||
<g transform={`translate(200, 100) rotate(${alphaWind})`}>
|
||||
{section === 'square' ? (
|
||||
<rect x="-25" y="-25" width="50" height="50" fill="none" stroke="#64748b" strokeWidth="4" />
|
||||
) : (
|
||||
<polygon points="0,-28 25,15 -25,15" fill="none" stroke="#64748b" strokeWidth="4" />
|
||||
)}
|
||||
{/* Barras internas treliçadas */}
|
||||
{section === 'square' ? (
|
||||
<>
|
||||
<line x1="-25" y1="-25" x2="25" y2="25" stroke="#475569" strokeWidth="1.5" />
|
||||
<line x1="25" y1="-25" x2="-25" y2="25" stroke="#475569" strokeWidth="1.5" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="0" y1="-28" x2="0" y2="15" stroke="#475569" strokeWidth="1.5" />
|
||||
<line x1="-25" y1="15" x2="12.5" y2="-6.5" stroke="#475569" strokeWidth="1.5" />
|
||||
<line x1="25" y1="15" x2="-12.5" y2="-6.5" stroke="#475569" strokeWidth="1.5" />
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
|
||||
{section === 'square' ? 'Quadrada' : 'Triangular'} | {alphaWind}º
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'cylinder': {
|
||||
const isSmooth = params.roughness === 'smooth' || params.d_over_k > 1e5;
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
|
||||
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
|
||||
{/* Fluxo contornando cilindro */}
|
||||
<g>
|
||||
<path d="M -20,20 L 420,20" className="wind-line-slow" />
|
||||
<path d="M -20,60 C 150,55 160,35 200,35 C 240,35 250,55 420,55" className="wind-line-fast" />
|
||||
<path d="M -20,140 C 150,145 160,165 200,165 C 240,165 250,145 420,145" className="wind-line-fast" />
|
||||
<path d="M -20,180 L 420,180" className="wind-line-slow" />
|
||||
</g>
|
||||
|
||||
{/* Esteira de Vórtices de Von Kármán */}
|
||||
<g transform="translate(250, 100)">
|
||||
<circle cx="20" cy="-25" r="10" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-cw" />
|
||||
<circle cx="60" cy="25" r="12" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-ccw" />
|
||||
<circle cx="100" cy="-20" r="14" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-cw" />
|
||||
<path d="M 0,0 Q 40,-35 80,0 Q 120,35 160,0" className="vortex-line" />
|
||||
</g>
|
||||
|
||||
{/* Cilindro */}
|
||||
<circle cx="200" cy="100" r="35" fill="#334155" stroke="#475569" strokeWidth={isSmooth ? "2" : "5"} />
|
||||
</svg>
|
||||
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
|
||||
Superfície: {isSmooth ? 'Lisa (Menor Arrasto)' : 'Rugosa (Maior Arrasto)'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'dynamics': {
|
||||
const fn = Number(params.fn || 0.5);
|
||||
// Frequência regula a velocidade da vibração visual
|
||||
const vibeDuration = Math.max(0.5, Math.min(2.5, 1 / fn));
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
|
||||
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
|
||||
{/* Linhas de vento */}
|
||||
<g>
|
||||
<path d="M -20,20 L 420,20" className="wind-line-slow" />
|
||||
<path d="M -20,60 C 150,55 160,35 200,35 C 240,35 250,55 420,55" className="wind-line" />
|
||||
<path d="M -20,140 C 150,145 160,165 200,165 C 240,165 250,145 420,145" className="wind-line" />
|
||||
<path d="M -20,180 L 420,180" className="wind-line-slow" />
|
||||
</g>
|
||||
|
||||
{/* Vórtices alternados desalinhados de Von Kármán */}
|
||||
<g transform="translate(250, 100)">
|
||||
<path d="M 0,0 Q 40,-25 80,10 Q 120,-15 160,5" className="vortex-line" />
|
||||
</g>
|
||||
|
||||
{/* Cilindro que VIBRA verticalmente na ressonância */}
|
||||
<g className="vibe-cyl" style={{ animationDuration: `${vibeDuration}s` }}>
|
||||
<circle cx="200" cy="100" r="30" fill="#ef4444" stroke="#f87171" strokeWidth="2" opacity="0.9" />
|
||||
<line x1="200" y1="100" x2="200" y2="40" stroke="#f87171" strokeWidth="2" strokeDasharray="2 4" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
|
||||
fn: {fn} Hz | Oscilação Ativa
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
// Padrão genérico de fluxo para os demais módulos
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Visualização do Escoamento</h4>
|
||||
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
<path d="M -20,30 L 420,30" className="wind-line-slow" />
|
||||
<path d="M -20,70 Q 200,40 420,70" className="wind-line" />
|
||||
<path d="M -20,130 Q 200,160 420,130" className="wind-line" />
|
||||
<path d="M -20,170 L 420,170" className="wind-line-slow" />
|
||||
|
||||
{/* Elemento genérico no centro */}
|
||||
<rect x="180" y="80" width="40" height="40" rx="3" fill="#334155" stroke="#475569" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Comparativos estáticos baseados em grids como sugerido pelo usuário
|
||||
const renderComparisonGrid = () => {
|
||||
if (type === 'bridge') {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] font-bold text-muted-foreground uppercase">Incidência Descendente (α = -5º)</span>
|
||||
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full opacity-90">
|
||||
{styleBlock}
|
||||
<g transform="rotate(-15, 200, 100)">
|
||||
<path d="M -50,50 L 450,50" className="wind-line-slow" />
|
||||
<path d="M -50,100 L 450,100" className="wind-line" />
|
||||
<path d="M -50,150 L 450,150" className="wind-line-slow" />
|
||||
</g>
|
||||
<rect x="150" y="85" width="100" height="30" rx="4" fill="#334155" />
|
||||
{/* Seta para baixo */}
|
||||
<path d="M 200,125 L 200,170" stroke="#ef4444" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Vento empurra o tabuleiro para baixo, aumentando os esforços de compressão nos pilares.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] font-bold text-muted-foreground uppercase">Incidência Ascendente (α = +5º)</span>
|
||||
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full opacity-90">
|
||||
{styleBlock}
|
||||
<g transform="rotate(15, 200, 100)">
|
||||
<path d="M -50,50 L 450,50" className="wind-line-slow" />
|
||||
<path d="M -50,100 L 450,100" className="wind-line" />
|
||||
<path d="M -50,150 L 450,150" className="wind-line-slow" />
|
||||
</g>
|
||||
<rect x="150" y="85" width="100" height="30" rx="4" fill="#334155" />
|
||||
{/* Seta para cima */}
|
||||
<path d="M 200,75 L 200,30" stroke="#10b981" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Efeito aerofólio levanta a ponte. Crítico para pontes estaiadas ou suspensas.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'tower') {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] font-bold text-muted-foreground uppercase">Vento Normal (0º)</span>
|
||||
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
<path d="M -20,60 C 150,60 170,45 230,45 Q 320,60 420,60" className="wind-line" />
|
||||
<path d="M -20,140 C 150,140 170,155 230,155 Q 320,140 420,140" className="wind-line" />
|
||||
<g transform="translate(180, 80)">
|
||||
<rect x="0" y="0" width="40" height="40" fill="none" stroke="#64748b" strokeWidth="3" />
|
||||
<line x1="0" y1="0" x2="40" y2="40" stroke="#475569" />
|
||||
<line x1="40" y1="0" x2="0" y2="40" stroke="#475569" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Vento incidindo perpendicularmente à face frontal. Menor área exposta efetiva.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] font-bold text-muted-foreground uppercase">Vento na Diagonal (45º)</span>
|
||||
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
|
||||
<svg viewBox="0 0 400 200" className="w-full h-full">
|
||||
{styleBlock}
|
||||
<path d="M -20,40 C 150,40 170,30 230,30 Q 320,40 420,40" className="wind-line" />
|
||||
<path d="M -20,160 C 150,160 170,170 230,170 Q 320,160 420,160" className="wind-line" />
|
||||
<g transform="translate(200, 100) rotate(45)">
|
||||
<rect x="-20" y="-20" width="40" height="40" fill="none" stroke="#64748b" strokeWidth="3" />
|
||||
<line x1="-20" y1="-20" x2="20" y2="20" stroke="#475569" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Aumenta a esteira e o coeficiente de arrasto Ca nas torres de seção quadrada.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Grid padrão se não houver um caso comparativo customizado
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 my-6 bg-slate-900/40 p-4 border rounded-xl">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-sm font-semibold">Túnel de Vento Virtual (Escoamento 2D)</h3>
|
||||
<span className="text-[10px] bg-sky-500/10 text-sky-400 font-bold border border-sky-500/20 px-2 py-0.5 rounded-full">SVG Interativo</span>
|
||||
</div>
|
||||
{renderActiveSimulation()}
|
||||
{renderComparisonGrid()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,11 +25,6 @@ export interface Bar3DInput {
|
||||
cx: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
||||
*/
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.3, 0.3), 4);
|
||||
|
||||
function BarModel({
|
||||
barType,
|
||||
section,
|
||||
@@ -37,94 +32,60 @@ function BarModel({
|
||||
width,
|
||||
length,
|
||||
alpha,
|
||||
fxKN,
|
||||
fyKN,
|
||||
cx,
|
||||
}: Bar3DInput) {
|
||||
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
|
||||
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
|
||||
|
||||
// Cor baseada em Cx
|
||||
const barColor = useMemo(() => {
|
||||
const intensity = Math.min(1, Math.abs(cx) / 2.5);
|
||||
const hue = 215 - intensity * 215;
|
||||
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
|
||||
}, [cx]);
|
||||
|
||||
// Rotação da barra em torno do eixo Y (alinhada com eixo X inicialmente)
|
||||
// Direção do vento é +X; α é o ângulo da face da barra em relação ao vento
|
||||
// Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
|
||||
// O eixo longitudinal na nossa geometria é o X.
|
||||
const alphaRad = (alpha * Math.PI) / 180;
|
||||
const barRotation = -alphaRad; // rotação em torno do eixo Y para alinhar a face
|
||||
|
||||
// Direção do vetor de força resultante (na direção da força calculada)
|
||||
const forceMag = Math.sqrt(fxKN * fxKN + fyKN * fyKN);
|
||||
const forceAngle = Math.atan2(fyKN, fxKN);
|
||||
const arrowLen = forceToLength(forceMag);
|
||||
|
||||
// Centro da barra (origem)
|
||||
const center = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
// Posição da ponta da seta
|
||||
const arrowEnd = useMemo(
|
||||
() => new THREE.Vector3(
|
||||
Math.cos(forceAngle) * arrowLen,
|
||||
Math.sin(forceAngle) * arrowLen,
|
||||
0,
|
||||
),
|
||||
[forceAngle, arrowLen],
|
||||
);
|
||||
const arrowMid = useMemo(
|
||||
() => new THREE.Vector3(arrowEnd.x / 2, arrowEnd.y / 2, 0),
|
||||
[arrowEnd],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = arrowEnd.clone().normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [arrowEnd]);
|
||||
|
||||
const headLen = 0.25;
|
||||
// A rotação deve ser negativa para que o vento (vindo de -Z) atinja as faces corretas conforme a NBR.
|
||||
const barRotationX = -alphaRad;
|
||||
|
||||
return (
|
||||
<group rotation={[0, barRotation, 0]}>
|
||||
{/* Eixo principal da barra ao longo do eixo X */}
|
||||
{barType === 'circular' ? (
|
||||
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
|
||||
<cylinderGeometry args={[barRadius, barRadius, length, 16]} />
|
||||
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
|
||||
<group>
|
||||
{/* Seta do Vento (Fixa Globalmente, soprando em +Z) */}
|
||||
<group position={[0, 0, barRadius + 0.5]}>
|
||||
<mesh position={[0, 0, 0.4]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<cylinderGeometry args={[0.02, 0.02, 0.8, 8]} />
|
||||
<meshStandardMaterial color="#22c55e" />
|
||||
</mesh>
|
||||
) : (
|
||||
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
|
||||
)}
|
||||
|
||||
{/* Eixos de referência */}
|
||||
<axesHelper args={[length * 0.5]} />
|
||||
|
||||
{/* Vetor de força (resultante) */}
|
||||
{forceMag > 0.01 && (
|
||||
<group>
|
||||
{arrowLen - headLen > 0.01 && (
|
||||
<mesh position={arrowMid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.04, 0.04, arrowLen - headLen, 10]} />
|
||||
<meshStandardMaterial color="#ef4444" />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={arrowEnd.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.1, headLen, 10]} />
|
||||
<meshStandardMaterial color="#ef4444" />
|
||||
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<coneGeometry args={[0.08, 0.2, 8]} />
|
||||
<meshStandardMaterial color="#22c55e" />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Barra Rotacionada (Roll) */}
|
||||
<group rotation={[barRotationX, 0, 0]}>
|
||||
{/* Eixo principal da barra ao longo do eixo X */}
|
||||
{barType === 'circular' ? (
|
||||
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
|
||||
<cylinderGeometry args={[barRadius, barRadius, length, 16]} />
|
||||
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
|
||||
</mesh>
|
||||
</group>
|
||||
)}
|
||||
) : (
|
||||
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
|
||||
)}
|
||||
|
||||
{/* Eixos de referência locais da barra */}
|
||||
<axesHelper args={[length * 0.5]} />
|
||||
</group>
|
||||
|
||||
{/* Marca de origem */}
|
||||
<mesh position={[0, 0, 0]} castShadow>
|
||||
<sphereGeometry args={[0.06, 12, 12]} />
|
||||
<meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} />
|
||||
</mesh>
|
||||
<axesHelper args={[length * 0.3]} />
|
||||
<Text
|
||||
position={[0, -barRadius * 2 - 0.3, 0]}
|
||||
position={[0, -barRadius * 2 - 0.4, 0]}
|
||||
fontSize={0.3}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
@@ -132,7 +93,6 @@ function BarModel({
|
||||
>
|
||||
α={alpha}° | Cx={cx.toFixed(2)}
|
||||
</Text>
|
||||
{center && null}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface Bridge3DInput {
|
||||
/** Maior vão Lₚ (m) */
|
||||
@@ -21,6 +22,8 @@ export interface Bridge3DInput {
|
||||
fxPerLength: number;
|
||||
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
|
||||
fzPerLength: number;
|
||||
/** Ângulo de ataque do vento (graus) */
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
function BridgeModel({
|
||||
@@ -29,8 +32,7 @@ function BridgeModel({
|
||||
deckHeight,
|
||||
heg,
|
||||
cx,
|
||||
fxPerLength,
|
||||
fzPerLength,
|
||||
alpha,
|
||||
}: Bridge3DInput) {
|
||||
const halfL = lp / 2;
|
||||
const halfW = width / 2;
|
||||
@@ -47,13 +49,6 @@ function BridgeModel({
|
||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||
|
||||
// Vetor de força (Fx horizontal)
|
||||
const fxLen = Math.min(Math.max(Math.abs(fxPerLength) * 0.5, 0.3), 4);
|
||||
const fxDir = fxPerLength >= 0 ? 1 : -1;
|
||||
// Vetor de força (Fz vertical)
|
||||
const fzLen = Math.min(Math.max(Math.abs(fzPerLength) * 0.5, 0.3), 4);
|
||||
const fzDir = fzPerLength >= 0 ? 1 : -1;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Tabuleiro (deck) */}
|
||||
@@ -89,32 +84,15 @@ function BridgeModel({
|
||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||
</mesh>
|
||||
|
||||
{/* Vetor Cx (horizontal) */}
|
||||
<ForceArrow
|
||||
start={[-halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen}
|
||||
color="#ef4444"
|
||||
/>
|
||||
|
||||
{/* Vetor Cz (vertical) */}
|
||||
<ForceArrow
|
||||
start={[halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[0, fzDir, 0]}
|
||||
length={fzLen}
|
||||
color="#3b82f6"
|
||||
/>
|
||||
|
||||
{/* Vetor no centro também para destacar */}
|
||||
<ForceArrow
|
||||
start={[0, deckY + deckThickness + 0.3, 0]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen * 0.7}
|
||||
color="#ef4444"
|
||||
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
||||
<WindArrow
|
||||
target={[0, deckY + deckThickness / 2, 0]}
|
||||
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
||||
scale={4}
|
||||
/>
|
||||
<Text
|
||||
position={[0, deckY + deckThickness + 1.0, 0]}
|
||||
fontSize={0.6}
|
||||
position={[0, deckY + deckThickness + 1.5, 0]}
|
||||
fontSize={1.2}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -125,54 +103,7 @@ function BridgeModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: [number, number, number];
|
||||
direction: [number, number, number];
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const dirVec = useMemo(() => new THREE.Vector3(...direction), [direction]);
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(
|
||||
startVec.x + dirVec.x * length,
|
||||
startVec.y + dirVec.y * length,
|
||||
startVec.z + dirVec.z * length,
|
||||
),
|
||||
[startVec, dirVec, length],
|
||||
);
|
||||
const mid = useMemo(
|
||||
() => new THREE.Vector3().addVectors(startVec, end).multiplyScalar(0.5),
|
||||
[startVec, end],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [startVec, end]);
|
||||
const headLen = 0.3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{length - headLen > 0.01 && (
|
||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.06, 0.06, length - headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.15, headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
||||
const { lp, deckHeight, width } = input;
|
||||
|
||||
@@ -186,6 +186,7 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
frameloop="always"
|
||||
shadows
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface IsolatedRoof3DInput {
|
||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
||||
@@ -11,7 +12,9 @@ export interface IsolatedRoof3DInput {
|
||||
theta: number;
|
||||
/** Altura livre dos suportes (m) */
|
||||
height: number;
|
||||
/** Profundidade da cobertura (m) — dimensão perpendicular à seção */
|
||||
/** Largura da cobertura (m) — dimensão transversal (b) */
|
||||
width: number;
|
||||
/** Profundidade da cobertura (m) — dimensão longitudinal (l) */
|
||||
depth: number;
|
||||
/** Cpe barlavento (sobre a face exposta ao vento) */
|
||||
cpeWindward: number;
|
||||
@@ -23,8 +26,6 @@ export interface IsolatedRoof3DInput {
|
||||
forceKN: number;
|
||||
}
|
||||
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.15, 0.5), 6);
|
||||
|
||||
function pressureColor(cpe: number): THREE.Color {
|
||||
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
|
||||
const t = (clamped + 2.5) / 4.0;
|
||||
@@ -36,6 +37,7 @@ function IsolatedRoofModel({
|
||||
type,
|
||||
theta,
|
||||
height,
|
||||
width,
|
||||
depth,
|
||||
cpeWindward,
|
||||
cpeLeeward,
|
||||
@@ -43,12 +45,11 @@ function IsolatedRoofModel({
|
||||
}: IsolatedRoof3DInput) {
|
||||
const thetaRad = (theta * Math.PI) / 180;
|
||||
const halfDepth = depth / 2;
|
||||
const halfWidth = width / 2;
|
||||
|
||||
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
|
||||
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
|
||||
|
||||
const arrowLen = forceToLength(forceKN);
|
||||
|
||||
const h_diff = depth * Math.tan(thetaRad);
|
||||
const h_half = (depth / 2) * Math.tan(thetaRad);
|
||||
|
||||
@@ -60,29 +61,29 @@ function IsolatedRoofModel({
|
||||
const list: { pos: [number, number, number]; h: number }[] = [];
|
||||
if (type === 'shed') {
|
||||
list.push(
|
||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
);
|
||||
} else {
|
||||
list.push(
|
||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfDepth, height / 2, halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, halfDepth], h: height },
|
||||
{ pos: [-halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfWidth, height / 2, halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, halfDepth], h: height },
|
||||
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [type, depth, height, h_diff, h_half, halfDepth]);
|
||||
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Solo translúcido */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
|
||||
<planeGeometry args={[depth * 2, depth * 2]} />
|
||||
<planeGeometry args={[width * 1.5, depth * 1.5]} />
|
||||
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
|
||||
</mesh>
|
||||
|
||||
@@ -97,7 +98,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
{/* Metade Sotavento (Z > 0) */}
|
||||
@@ -107,7 +108,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -121,7 +122,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
{/* Água Direita / Sotavento (Z > 0) */}
|
||||
@@ -131,7 +132,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -145,12 +146,10 @@ function IsolatedRoofModel({
|
||||
</mesh>
|
||||
))}
|
||||
|
||||
{/* Seta de força resultante (sucção para cima) */}
|
||||
<ForceArrow
|
||||
start={new THREE.Vector3(0, centerY, 0)}
|
||||
direction={new THREE.Vector3(0, 1, 0)}
|
||||
length={arrowLen}
|
||||
color="#ef4444"
|
||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||
<WindArrow
|
||||
target={new THREE.Vector3(0, centerY, 0)}
|
||||
direction={new THREE.Vector3(0, 0, 1)}
|
||||
/>
|
||||
|
||||
{/* === LINHAS DE COTA (CAD-Style) === */}
|
||||
@@ -208,8 +207,7 @@ function IsolatedRoofModel({
|
||||
|
||||
{/* Rótulo Superior */}
|
||||
<Text
|
||||
position={[0, centerY + arrowLen + 0.8, 0]}
|
||||
fontSize={0.4}
|
||||
position={[0, centerY + 3, 0]}
|
||||
color="#1a202c"
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -220,75 +218,23 @@ function IsolatedRoofModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: THREE.Vector3;
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(start.x + direction.x * length, start.y + direction.y * length, start.z + direction.z * length),
|
||||
[start, direction, length],
|
||||
);
|
||||
const headLen = 0.3;
|
||||
const headRadius = 0.1;
|
||||
const shaftRadius = 0.04;
|
||||
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
||||
[start, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [start, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh
|
||||
position={[end.x, end.y, end.z]}
|
||||
rotation={rotation}
|
||||
castShadow
|
||||
>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IsolatedRoof3DViewer({
|
||||
type,
|
||||
theta,
|
||||
height,
|
||||
width,
|
||||
depth,
|
||||
cpeWindward,
|
||||
cpeLeeward,
|
||||
cpeTop,
|
||||
forceKN,
|
||||
}: IsolatedRoof3DInput) {
|
||||
const cameraDistance = Math.max(depth * 1.3, height * 1.5, 8);
|
||||
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="isolatedRoof"
|
||||
props={{ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
||||
props={{ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -311,6 +257,7 @@ export default function IsolatedRoof3DViewer({
|
||||
type={type}
|
||||
theta={theta}
|
||||
height={height}
|
||||
width={width}
|
||||
depth={depth}
|
||||
cpeWindward={cpeWindward}
|
||||
cpeLeeward={cpeLeeward}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface Sign3DInput {
|
||||
/** Comprimento ℓ (m) */
|
||||
@@ -22,10 +23,8 @@ export interface Sign3DInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
||||
* 1 kN = 0.25 m de seta (escala calibrada para visualização).
|
||||
* Módulo 3D para visualização de Muros e Placas Isoladas.
|
||||
*/
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(kN * 0.25, 0.5), 8);
|
||||
|
||||
function SignModel({
|
||||
length,
|
||||
@@ -33,13 +32,11 @@ function SignModel({
|
||||
groundClearance,
|
||||
alpha,
|
||||
cf,
|
||||
forceKN,
|
||||
applicationPoint,
|
||||
}: Sign3DInput) {
|
||||
const baseY = groundClearance;
|
||||
const topY = baseY + height;
|
||||
const halfL = length / 2;
|
||||
const arrowLen = forceToLength(forceKN);
|
||||
|
||||
// Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície)
|
||||
// O ângulo em relação à normal da placa (eixo X) é 90 - α
|
||||
@@ -114,12 +111,10 @@ function SignModel({
|
||||
Resultante (e = {applicationPoint.toFixed(2)}m)
|
||||
</Text>
|
||||
|
||||
{/* Vetor de força resultante */}
|
||||
<ForceArrow
|
||||
start={arrowStart}
|
||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||
<WindArrow
|
||||
target={arrowStart}
|
||||
direction={arrowDir}
|
||||
length={arrowLen}
|
||||
color={forceKN >= 0 ? '#ef4444' : '#3b82f6'}
|
||||
/>
|
||||
|
||||
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
||||
@@ -154,7 +149,7 @@ function SignModel({
|
||||
</group>
|
||||
|
||||
{/* Cota de Comprimento (l) */}
|
||||
<group position={[0.4, baseY + height / 2, 0]}>
|
||||
<group position={[-0.4, baseY + height / 2, 0]}>
|
||||
{/* Linha horizontal longitudinal */}
|
||||
<mesh position={[0, 0, 0]}>
|
||||
<boxGeometry args={[0.015, 0.015, length]} />
|
||||
@@ -172,8 +167,8 @@ function SignModel({
|
||||
</mesh>
|
||||
{/* Texto do comprimento */}
|
||||
<Text
|
||||
position={[0.15, 0, 0]}
|
||||
rotation={[0, Math.PI / 2, 0]}
|
||||
position={[-0.15, 0, 0]}
|
||||
rotation={[0, -Math.PI / 2, 0]}
|
||||
fontSize={0.25}
|
||||
color="#475569"
|
||||
anchorX="center"
|
||||
@@ -197,61 +192,7 @@ function SignModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: THREE.Vector3;
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(start.x + direction.x * length, start.y, start.z + direction.z * length),
|
||||
[start, direction, length],
|
||||
);
|
||||
const headLen = 0.3;
|
||||
const headRadius = 0.1;
|
||||
const shaftRadius = 0.04;
|
||||
|
||||
// Cilindro principal (haste)
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
||||
[start, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
// Rotação do cilindro (apontar de start para end)
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [start, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
{/* Ponta da seta (cone) */}
|
||||
<mesh
|
||||
position={[end.x, end.y, end.z]}
|
||||
rotation={rotation}
|
||||
castShadow
|
||||
>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Sign3DViewer({
|
||||
length,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import { WindArrow } from './WindArrow';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
|
||||
export interface Tower3DInput {
|
||||
@@ -190,31 +191,11 @@ function TowerModel({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Vetores de força distribuídos pelos tramos (meio de cada tramo) */}
|
||||
{Array.from({ length: panels }).map((_, i) => {
|
||||
const pHeight = height / panels;
|
||||
const startY = (i + 0.5) * pHeight; // Altura no meio do tramo
|
||||
const pForce = forceKN / panels; // Força por tramo
|
||||
|
||||
// Escala da seta menor para ficar visualmente agradável
|
||||
const pArrowLen = Math.min(Math.max(pForce * 0.1, 0.5), 2.5);
|
||||
|
||||
// Calcular o ponto inicial para que a ponta da seta encoste na face (baseWidth / 2)
|
||||
const endX = -forceDir.x * (baseWidth / 2);
|
||||
const endZ = -forceDir.z * (baseWidth / 2);
|
||||
const startX = endX - forceDir.x * pArrowLen;
|
||||
const startZ = endZ - forceDir.z * pArrowLen;
|
||||
|
||||
return (
|
||||
<ForceArrow
|
||||
key={`force-${i}`}
|
||||
start={[startX, startY, startZ]}
|
||||
direction={forceDir}
|
||||
length={pArrowLen}
|
||||
color="#ef4444"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Seta de Vento única atingindo o meio da torre */}
|
||||
<WindArrow
|
||||
target={[0, height / 2, 0]}
|
||||
direction={forceDir}
|
||||
/>
|
||||
|
||||
{/* Labels indicativos */}
|
||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||
@@ -234,50 +215,6 @@ function TowerModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: [number, number, number];
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(startVec.x + direction.x * length, startVec.y, startVec.z + direction.z * length),
|
||||
[startVec, direction, length],
|
||||
);
|
||||
const mid = useMemo(
|
||||
() => new THREE.Vector3((startVec.x + end.x) / 2, (startVec.y + end.y) / 2, (startVec.z + end.z) / 2),
|
||||
[startVec, end],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [startVec, end]);
|
||||
const headLen = 0.3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{length - headLen > 0 && (
|
||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.05, 0.05, length - headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.12, headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Tower3DViewer(input: Tower3DInput) {
|
||||
const { baseWidth, height } = input;
|
||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
export interface WindArrowProps {
|
||||
/** Ponto onde a ponta da seta do vento toca (ex: centro geométrico da estrutura) */
|
||||
target: THREE.Vector3 | [number, number, number];
|
||||
/** Direção de onde o vento vem (e para onde ele vai). Vetor normalizado. */
|
||||
direction: THREE.Vector3 | [number, number, number];
|
||||
/** Fator de escala da seta (default: 1) */
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export function WindArrow({ target, direction, scale = 1 }: WindArrowProps) {
|
||||
const length = 2.5 * scale; // Tamanho visual fixo
|
||||
const color = '#3b82f6'; // Azul padrão do vento
|
||||
|
||||
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
|
||||
const dirVec = direction instanceof THREE.Vector3 ? direction : new THREE.Vector3(...direction);
|
||||
|
||||
const end = targetVec;
|
||||
const base = useMemo(
|
||||
() => new THREE.Vector3(end.x - dirVec.x * length, end.y - dirVec.y * length, end.z - dirVec.z * length),
|
||||
[end, dirVec, length],
|
||||
);
|
||||
|
||||
const headLen = 0.3 * scale;
|
||||
const headRadius = 0.1 * scale;
|
||||
const shaftRadius = 0.04 * scale;
|
||||
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((base.x + end.x) / 2, (base.y + end.y) / 2, (base.z + end.z) / 2),
|
||||
[base, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, base).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [base, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
{/* Ponta da seta (cone) */}
|
||||
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as React from "react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md",
|
||||
side === "left" &&
|
||||
"inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
|
||||
side === "top" &&
|
||||
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
side === "bottom" &&
|
||||
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("font-semibold text-foreground text-lg", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
|
||||
export const barManual = {
|
||||
title: 'Barras Estruturais Individuais (Seção 8.2)',
|
||||
intro: 'Coeficientes de arrasto para perfis estruturais isolados (perfis I, U, L, tubulares), abordando forças por unidade de comprimento.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Equação do Esforço',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A força de arrasto exercida sobre barras individuais é dada por metro linear de elemento:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
F = q · Ca · d
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>d</strong> é a dimensão característica da seção transversal do perfil (altura exposta frontalmente) e <strong>Ca</strong> é o coeficiente de arrasto da seção estudada.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Formas Geométricas dos Perfis (Tabelas 26 a 28)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O coeficiente Ca varia drasticamente com o perfil devido a cantos vivos ou aerodinâmicos:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li><strong>Perfis de Faces Planas (L, U, I):</strong> Possuem cantos vivos que forçam o descolamento imediato do vento, gerando altos valores de arrasto (Ca em torno de 1.4 a 2.0).</li>
|
||||
<li><strong>Perfis Circulares/Tubulares:</strong> O vento consegue contornar a forma circular, reduzindo o arrasto consideravelmente (Ca em torno de 0.6 a 1.2, dependendo de Reynolds).</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
|
||||
export const bridgeManual = {
|
||||
title: 'Tabuleiro de Ponte (Seção 11.2 / 11.3)',
|
||||
intro: 'Explicação detalhada sobre a ação estática do vento em pontes, cobrindo forças de arrasto (horizontais) e forças de sustentação (verticais).',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Forças Atuantes',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A ação do vento em tabuleiros de pontes é dividida em duas componentes principais:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm space-y-2 border border-border">
|
||||
<div>
|
||||
<strong>Força de Arrasto (Fx):</strong> Fx = q · Cx · Heg · Lp
|
||||
</div>
|
||||
<div>
|
||||
<strong>Força de Sustentação (Fz):</strong> Fz = q · Cz · B · Lp
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>q</strong> é a pressão dinâmica do vento, <strong>Lp</strong> é o comprimento do vão exposto, <strong>Heg</strong> é a altura equivalente do tabuleiro (soma das áreas de projeção das vigas, guarda-corpos e barreiras) e <strong>B</strong> é a largura da ponte.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Coeficientes Aerodinâmicos (Cx e Cz)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Os coeficientes são determinados com base na relação geométrica entre a largura e a altura do tabuleiro (B / Heg), bem como o ângulo de ataque do vento (α):
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Cx (Arrasto):</strong> Representa a resistência que a ponte oferece ao fluxo horizontal de vento. Valores maiores de Cx significam maior força empurrando a ponte lateralmente.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cz (Sustentação/Lift):</strong> Representa a força vertical (para cima ou para baixo) induzida pela inclinação do vento (ângulo de ataque α).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Se o vento incide com um ângulo positivo (vindo ligeiramente de baixo, α > 0), ele tende a "levantar" a ponte (Cz positivo). Se incide de cima (α < 0), empurra a ponte para baixo (Cz negativo).
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Glossário de Parâmetros',
|
||||
content: (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-border text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 font-semibold">Símbolo</th>
|
||||
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">Lp</td>
|
||||
<td className="py-2">Maior Vão (m)</td>
|
||||
<td className="py-2">O comprimento do vão livre da ponte sob análise.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">B</td>
|
||||
<td className="py-2">Largura (m)</td>
|
||||
<td className="py-2">Largura total do tabuleiro da ponte (perpendicular ao fluxo principal).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">Heg</td>
|
||||
<td className="py-2">Altura Equivalente (m)</td>
|
||||
<td className="py-2">Soma da projeção vertical de todos os elementos expostos ao vento.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">α (alpha)</td>
|
||||
<td className="py-2">Ângulo de Ataque (º)</td>
|
||||
<td className="py-2">Inclinação do vetor do vento em relação ao plano horizontal (varia de -5º a +5º).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">q</td>
|
||||
<td className="py-2">Pressão Dinâmica (kN/m²)</td>
|
||||
<td className="py-2">Pressão do vento calculada a partir da velocidade de projeto.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
|
||||
export const cylinderManual = {
|
||||
title: 'Cilindros e Chaminés (Tabela 13 - Seção 6.1)',
|
||||
intro: 'Ação do vento em cilindros circulares (chaminés, silos e reservatórios), detalhando a influência do número de Reynolds e da rugosidade superficial no arrasto.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Equação do Arrasto Global',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A força de arrasto exercida por unidade de comprimento de um cilindro circular é calculada por:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
F = q · Ca · D
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>D</strong> é o diâmetro externo do cilindro e <strong>Ca</strong> é o coeficiente de arrasto corrigido pela esbeltez.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Regimes de Fluxo e Número de Reynolds (Re)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O comportamento aerodinâmico em cilindros circulares é governado pelo <strong>Número de Reynolds (Re)</strong>, um parâmetro adimensional que indica a relação entre as forças de inércia e de viscosidade do fluxo:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||
Re = 70.000 · Vk · D
|
||||
</div>
|
||||
<p>
|
||||
O fluxo é classificado em três regimes principais na NBR 6123:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Subcrítico (Re < 1.5 · 10⁵):</strong> A camada limite é laminar. O descolamento do fluxo ocorre cedo (cerca de 80º em relação ao ponto de estagnação). A esteira de turbulência atrás do cilindro é larga, resultando em um arrasto elevado (Ca ≈ 1.2).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Crítico / Supercrítico (Re > 4 · 10⁵):</strong> A camada limite torna-se turbulenta. Ela consegue "colar" mais tempo na parede do cilindro, atrasando o descolamento para cerca de 120º. A esteira traseira estreita significativamente, derrubando o arrasto de forma abrupta (Ca pode cair para 0.4 a 0.7).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Influência da Rugosidade Superficial',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Para altos números de Reynolds, a rugosidade superficial do cilindro (rugosidade relativa <strong>d/k</strong>) impede que o arrasto caia excessivamente.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Superfícies lisas (d/k > 10⁶):</strong> Exibem o menor arrasto possível no regime supercrítico (Ca ≈ 0.4 - 0.5).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Superfícies rugosas (d/k reduzido, ex: concreto bruto ou chapa ondulada):</strong> Aceleram a transição para fluxo turbulento, mas a rugosidade gera turbulência local que impede a esteira de estreitar tanto, mantendo o arrasto em patamares mais elevados (Ca ≈ 0.7 - 0.9).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
|
||||
export const domeManual = {
|
||||
title: 'Cúpulas Esféricas (Tabela 21 e 22)',
|
||||
intro: 'Ação do vento em cúpulas e coberturas hemisféricas, analisando coeficientes de forças globais e pressões locais.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Aerodinâmica Tridimensional',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Cúpulas esféricas possuem comportamento aerodinâmico tridimensional muito favorável. O ar contorna a cúpula não apenas por cima, mas também pelos lados, reduzindo a turbulência geral comparado com coberturas planas ou inclinadas.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Coeficientes de Pressão Externa',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O vento acelerado sobre a cúpula cria três zonas de pressões marcantes:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li><strong>Barlavento (frente):</strong> Sobrepressão sutil perto da base, que rapidamente vira sucção à medida que sobe no arco esférico.</li>
|
||||
<li><strong>Topo:</strong> Ponto de máxima velocidade do vento. Sofre uma sucção crítica gerada pelo escoamento aerodinâmico acelerado.</li>
|
||||
<li><strong>Sotavento (traseira):</strong> Zona de vácuo aerodinâmico com sucção homogênea constante e desprendimento de esteira tridimensional.</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
|
||||
export const dynamicsManual = {
|
||||
title: 'Efeitos Dinâmicos e Ressonância (Seção 9 e Anexo G)',
|
||||
intro: 'Explicação detalhada dos fenômenos dinâmicos induzidos pelo vento, com foco especial no desprendimento de vórtices (Vortex Shedding).',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Desprendimento de Vórtices (Vortex Shedding)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Quando o vento sopra perpendicularmente a um obstáculo cilíndrico (como uma chaminé ou torre), o fluxo de ar se divide e cria redemoinhos alternados na traseira da estrutura.
|
||||
</p>
|
||||
<p>
|
||||
Estes redemoinhos, conhecidos como <strong>Vórtices de Von Kármán</strong>, geram forças de sucção alternadas nas laterais perpendiculares à direção do vento. Como consequência, o cilindro sofre forças dinâmicas oscilantes transversais.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Velocidade Crítica de Ressonância (Vcr)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A frequência na qual os vórtices se desprendem é dada por:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
f = St · V / D
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>St</strong> é o número de Strouhal (adimensional, geralmente 0.2 para cilindros), <strong>V</strong> é a velocidade do vento e <strong>D</strong> é o diâmetro.
|
||||
</p>
|
||||
<p>
|
||||
Se a frequência de descolamento de vórtices se igualar a uma das frequências naturais da estrutura (<strong>fn</strong>), ocorre o fenômeno de **ressonância**, gerando grandes amplitudes de oscilação. A velocidade do vento que ativa a ressonância é chamada de **Velocidade Crítica (Vcr)**:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
Vcr = fn · D / St
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Parâmetros Chave',
|
||||
content: (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-border text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">St (Strouhal)</td>
|
||||
<td className="py-2">Número adimensional que depende da geometria da seção. Para cilindro liso, St = 0.2.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">fn (Frequência)</td>
|
||||
<td className="py-2">Frequência natural de vibração da estrutura (em Hz). Estruturas mais esbeltas têm fn menor.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">Vcr (Vel. Crítica)</td>
|
||||
<td className="py-2">Velocidade em que os esforços alternados dos vórtices entram em ressonância com a estrutura.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { bridgeManual } from './bridge';
|
||||
import { towerManual } from './tower';
|
||||
import { signManual } from './sign';
|
||||
import { warehouseManual } from './warehouse';
|
||||
import { cylinderManual } from './cylinder';
|
||||
import { vaultManual } from './vault';
|
||||
import { domeManual } from './dome';
|
||||
import { barManual } from './bar';
|
||||
import { dynamicsManual } from './dynamics';
|
||||
import { isolatedRoofManual } from './isolated-roof';
|
||||
|
||||
export const manuals: Record<string, typeof bridgeManual> = {
|
||||
bridge: bridgeManual,
|
||||
tower: towerManual,
|
||||
sign: signManual,
|
||||
warehouse: warehouseManual,
|
||||
cylinder: cylinderManual,
|
||||
vault: vaultManual,
|
||||
dome: domeManual,
|
||||
bar: barManual,
|
||||
dynamics: dynamicsManual,
|
||||
'isolated-roof': isolatedRoofManual,
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
|
||||
export const isolatedRoofManual = {
|
||||
title: 'Coberturas Isoladas (Tabelas 24 e 25)',
|
||||
intro: 'Ação do vento em coberturas sem paredes (estruturas abertas, hangares abertos, postos de gasolina), abordando coeficientes de pressão líquida e forças resultantes.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Coeficiente de Pressão Líquida (Cp)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Como as coberturas isoladas não possuem paredes para vedação, o vento escoa livremente tanto por cima quanto por baixo do telhado.
|
||||
</p>
|
||||
<p>
|
||||
Por isso, a norma utiliza o coeficiente de **pressão líquida (Cp)**, que já representa a diferença líquida de pressão entre a face superior e inferior da chapa do telhado:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
F = q · Cp · A
|
||||
</div>
|
||||
<p>
|
||||
Dependendo da direção e do ângulo de inclinação do telhado, a chapa pode sofrer sucção (força para cima) ou sobrepressão (força para baixo).
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Efeito da Inclinação (θ)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O ângulo de inclinação do telhado (<strong>θ</strong>) define se o escoamento é suave ou gera turbulência pesada:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li><strong>Ângulos pequenos (θ < 5º):</strong> O escoamento gera sucção quase uniforme em toda a chapa devido ao efeito Venturi do vento passando sob a cobertura.</li>
|
||||
<li><strong>Ângulos elevados (θ > 15º):</strong> A cobertura atua como uma asa de avião com alto ângulo de ataque. O lado de barlavento sofre forte pressão para baixo, enquanto o lado de sotavento sofre sucção severa.</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
|
||||
export const signManual = {
|
||||
title: 'Muros e Placas Isoladas (Tabela 11 e Seção 6.1)',
|
||||
intro: 'Ação do vento em muros, painéis e placas isoladas livres no solo, detalhando arrasto, excentricidades e efeitos de placas de extremidade.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Coeficiente de Força (Cf)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Para muros e placas isoladas, a NBR 6123 define o coeficiente de força (<strong>Cf</strong>) com base na relação de aspecto e folga do solo:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
F = q · Cf · A
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>A</strong> é a área total exposta do muro (Comprimento · Altura). O coeficiente Cf varia conforme o alongamento do muro. Muros muito longos e rente ao solo têm Cf maior porque o fluxo de ar não consegue escapar pelas pontas, gerando maior retenção de energia do vento.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Excentricidade da Força (Torsão)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O vento raramente atinge o muro de forma perfeitamente homogênea. Quando o vento incide com um ângulo oblíquo (ex: α = 50º), ele se concentra mais próximo a uma das bordas.
|
||||
</p>
|
||||
<p>
|
||||
Por este motivo, a norma NBR 6123 exige a aplicação da força resultante com uma <strong>excentricidade horizontal (e)</strong> em relação ao centro geométrico da placa:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||
e = 0.25 · L (comprimento do muro)
|
||||
</div>
|
||||
<p>
|
||||
Esta excentricidade gera um momento torsor nas fundações e suportes estruturais do muro, sendo um fator crítico no dimensionamento de placas e outdoors.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Placas de Extremidade (End Plates)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Placas de extremidade são abas físicas verticais instaladas nas pontas do muro para fins aerodinâmicos ou construtivos:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Sem abas:</strong> O vento bate na frente e escoa com facilidade pelas pontas (efeito de borda tridimensional), o que reduz ligeiramente o Cf local próximo às pontas.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Com abas:</strong> As abas forçam o vento a subir apenas pelo topo, gerando um padrão bidimensional de escoamento. Isso aumenta significativamente a pressão diferencial entre a face da frente e de trás do muro, elevando o valor de Cf para patamares elevados (de 1.3 até 2.0).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
|
||||
export const towerManual = {
|
||||
title: 'Torre Reticulada (Seção 8.5)',
|
||||
intro: 'Ação do vento em torres de seção quadrada ou triangular equilátera, cobrindo o índice de solidez, coeficientes de arrasto e fatores de incidência.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Força de Arrasto Total',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A força de arrasto exercida sobre cada tramo de uma torre reticulada é dada por:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
F = q · Ca · Ae
|
||||
</div>
|
||||
<p>
|
||||
Onde <strong>q</strong> é a pressão de vento de projeto, <strong>Ca</strong> é o coeficiente de arrasto e <strong>Ae</strong> é a área projetada efetiva (soma da projeção das barras de barlavento e sotavento, ajustada pelo coeficiente de proteção de tramos).
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Índice de Solidez (φ)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O índice de solidez (<strong>φ</strong>) é a relação entre a área real exposta pelas barras e a área total envolvente delimitada pelo contorno da face da torre:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||
φ = Área das Barras / (Largura da Base · Altura do Tramo)
|
||||
</div>
|
||||
<p>
|
||||
Uma solidez baixa (φ próximo a 0.05) indica uma torre muito vazada (vento passa direto). Uma solidez alta (φ próximo a 1) indica uma torre quase sólida. O coeficiente de arrasto Ca diminui à medida que a solidez aumenta porque as barras de sotavento ficam mais protegidas do vento.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Ação Direcional (Kα) - Quadrada vs Triangular',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O vento pode atingir a torre de diferentes direções. A norma aborda isso aplicando um fator direcional <strong>Kα</strong>:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Torres Quadradas:</strong> O vento incidindo na diagonal (45º) causa maior arrasto global do que nas faces (0º). Portanto, Kα é maior para vento a 45º.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Torres Triangulares:</strong> Devido à simetria geométrica de um triângulo equilátero, a NBR 6123 assume que o arrasto total é constante independentemente da direção. Assim, <strong>Kα é sempre 1.0</strong> para qualquer direção do vento.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '4. Glossário de Parâmetros',
|
||||
content: (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-border text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 font-semibold">Símbolo</th>
|
||||
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">φ (phi)</td>
|
||||
<td className="py-2">Índice de Solidez</td>
|
||||
<td className="py-2">Razão entre a área das barras e a área total envolvente da face.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">Kα</td>
|
||||
<td className="py-2">Fator de Direção</td>
|
||||
<td className="py-2">Multiplicador que ajusta a força com base no ângulo de incidência do vento.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 font-mono font-semibold">Ca</td>
|
||||
<td className="py-2">Coeficiente de Arrasto</td>
|
||||
<td className="py-2">Coeficiente adimensional que mede a resistência aerodinâmica da estrutura.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
|
||||
export const vaultManual = {
|
||||
title: 'Abóbadas Cilíndricas (Tabela 15 - Seção 6.2)',
|
||||
intro: 'Coeficientes de pressão externa para coberturas cilíndricas convexas (abóbadas) assentadas diretamente no solo ou elevadas.',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Geometria da Abóbada',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O fluxo de vento sobre abóbadas é fortemente influenciado pelas relações geométricas:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li><strong>f (flecha):</strong> Altura do arco da abóbada.</li>
|
||||
<li><strong>d (vão):</strong> Largura da projeção horizontal da abóbada.</li>
|
||||
<li><strong>h (altura da parede de suporte):</strong> Se a abóbada estiver assentada diretamente no solo, h = 0. Se estiver elevada, h representa a altura da parede.</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Distribuição de Pressões (Cpe)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
O perfil curvo da abóbada atua como um aerofólio (perfil de asa), gerando aceleração acentuada do ar no topo:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Barlavento (início do arco):</strong> Pode sofrer pressão positiva se a parede frontal for alta, ou sucção leve caso o vento deslize suavemente pela curvatura suave.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Topo (Zona Central):</strong> É a região de maior aceleração do fluxo de vento. Sofre uma sucção fortíssima (Cpe muito negativo), tendendo a puxar a cobertura para cima.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Sotavento (fim do arco):</strong> O fluxo descola da abóbada, gerando uma zona de recirculação com sucção constante, embora de menor magnitude que no topo.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
export const warehouseManual = {
|
||||
title: 'Galpão Retangular (Tabelas 4 e 5 - Seção 6)',
|
||||
intro: 'Ação do vento em galpões de base retangular e telhados a duas águas. Abrange os coeficientes de pressão externa (Cpe) e interna (Cpi).',
|
||||
sections: [
|
||||
{
|
||||
title: '1. Pressão Resultante (p)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A pressão efetiva do vento em qualquer ponto da superfície externa do galpão é a diferença entre a pressão externa e a interna:
|
||||
</p>
|
||||
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||
p = q · (Cpe - Cpi)
|
||||
</div>
|
||||
<p>
|
||||
Onde:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li><strong>q:</strong> Pressão dinâmica do vento na altura do topo do telhado.</li>
|
||||
<li><strong>Cpe:</strong> Coeficiente de pressão externa (depende da zona do galpão).</li>
|
||||
<li><strong>Cpi:</strong> Coeficiente de pressão interna (depende de aberturas nas paredes).</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '2. Zonas de Pressão Externa (Cpe)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Devido à complexidade aerodinâmica, a NBR 6123 divide o galpão em várias zonas de pressão localizadas:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Paredes (Zonas A, B, C, D, E):</strong> Geralmente, a parede de barlavento sofre sobrepressão (Cpe > 0, Zona D), enquanto as paredes laterais (Zonas A, B, C) e sotavento (Zona E) sofrem sucção (Cpe < 0) devido ao fluxo descolado.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Telhado (Zonas F, G, H, I, J):</strong> O fluxo de ar descola bruscamente no beiral de barlavento, criando sucções locais fortíssimas nas bordas (Zonas F e G). No meio do telhado (Zona H) e na água de sotavento (Zonas I, J), a sucção tende a diminuir levemente.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '3. Pressão Interna (Cpi)',
|
||||
content: (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
A pressão interna surge da passagem do ar através de frestas, janelas, portões e aberturas:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Abertura dominante em barlavento (frente):</strong> O vento entra e "infla" o galpão por dentro. Isso gera uma sobrepressão interna positiva (Cpi até +0.9). Esta pressão positiva empurra as paredes e o telhado de dentro para fora, somando-se à sucção externa e aumentando o risco de colapso do telhado.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Abertura dominante em sotavento ou laterais:</strong> O ar é "sugado" de dentro do galpão, criando vácuo/sucção interna (Cpi até -0.9). Isso ajuda a estabilizar o telhado contra a sucção externa, mas sobrecarrega a parede de barlavento.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { calculateCircleBarForce, reynoldsBar } from '@/lib/nbr-tables/table-27'
|
||||
import Bar3DViewer from '@/components/three/Bar3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const BarSelectorModule: React.FC = () => {
|
||||
@@ -157,6 +158,9 @@ const BarSelectorModule: React.FC = () => {
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="bar" params={{ barType, section, alpha }} />
|
||||
</div>
|
||||
<Bar3DViewer
|
||||
barType={barType}
|
||||
section={barType === 'flat' ? section : undefined}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { classifyBridge, calculateBridgeDeckForces, flutterCheck, gallopingCheck
|
||||
import Bridge3DViewer from '@/components/three/Bridge3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const BridgeModule: React.FC = () => {
|
||||
@@ -149,6 +150,9 @@ const BridgeModule: React.FC = () => {
|
||||
Classe {classification.bridgeClass}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="bridge" params={{ alpha }} />
|
||||
</div>
|
||||
<Bridge3DViewer
|
||||
lp={lp}
|
||||
width={width}
|
||||
@@ -158,6 +162,7 @@ const BridgeModule: React.FC = () => {
|
||||
cz={forces.cz}
|
||||
fxPerLength={forces.fxPerLength}
|
||||
fzPerLength={forces.fzPerLength}
|
||||
alpha={alpha}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder'
|
||||
import Cylinder3DViewer from '@/components/three/Cylinder3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const CylinderModule: React.FC = () => {
|
||||
@@ -140,6 +141,9 @@ const CylinderModule: React.FC = () => {
|
||||
Re = {result.re.toExponential(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="cylinder" params={{ roughness: surface }} />
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Cylinder3DViewer
|
||||
diameter={diameter}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
||||
import Dome3DViewer from '@/components/three/Dome3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const DomeModule: React.FC = () => {
|
||||
@@ -118,6 +119,9 @@ const DomeModule: React.FC = () => {
|
||||
Cpi = {cpi.toFixed(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="dome" params={{ rise, diameter }} />
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Dome3DViewer
|
||||
diameter={diameter}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { TABLE_32 } from '@/lib/nbr-tables/table-32';
|
||||
import Dynamics3DViewer from '@/components/three/Dynamics3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const DynamicsModule: React.FC = () => {
|
||||
@@ -256,8 +257,9 @@ const DynamicsModule: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<Card className="flex-1 min-h-[400px]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-3 flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Visualização 3D</CardTitle>
|
||||
<EducationalManual type="dynamics" params={{ fn: freq }} />
|
||||
</CardHeader>
|
||||
<CardContent className="h-full">
|
||||
<Dynamics3DViewer
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
|
||||
const GalpaoModule: React.FC = () => {
|
||||
const {
|
||||
@@ -103,8 +104,9 @@ const GalpaoModule: React.FC = () => {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
||||
<ExportMenu />
|
||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIso
|
||||
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const IsolatedRoofModule: React.FC = () => {
|
||||
@@ -164,10 +165,14 @@ const IsolatedRoofModule: React.FC = () => {
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||
</div>
|
||||
<IsolatedRoof3DViewer
|
||||
type={type}
|
||||
theta={theta}
|
||||
height={height}
|
||||
width={width}
|
||||
depth={depth}
|
||||
cpeWindward={cpeWindward}
|
||||
cpeLeeward={cpeLeeward}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { calculateSign } from '@/lib/nbr-tables/table-23';
|
||||
import Sign3DViewer from '@/components/three/Sign3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const SignModule: React.FC = () => {
|
||||
@@ -118,6 +119,9 @@ const SignModule: React.FC = () => {
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="sign" params={{ length, height, alpha, hasEndPlates }} />
|
||||
</div>
|
||||
|
||||
<Sign3DViewer
|
||||
length={length}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { calculateTower, type TowerSection, type TowerBarType } from '@/lib/modu
|
||||
import Tower3DViewer from '@/components/three/Tower3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const TowerModule: React.FC = () => {
|
||||
@@ -141,6 +142,9 @@ const TowerModule: React.FC = () => {
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
||||
</div>
|
||||
<Tower3DViewer
|
||||
section={section}
|
||||
barType={barType}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
||||
import Vault3DViewer from '@/components/three/Vault3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const VaultModule: React.FC = () => {
|
||||
@@ -126,6 +127,9 @@ const VaultModule: React.FC = () => {
|
||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="vault" params={{ rise, span }} />
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user