feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+540
View File
@@ -0,0 +1,540 @@
function pressureColor(cpe: number, cpi: number): string {
const p = cpe - cpi;
const intensity = Math.min(1, Math.abs(p) / 1.2);
if (p > 0) return `hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, 65 - intensity * 25)}%)`;
return `hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`;
}
function cpeColor(cpe: number): string {
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
const t = (clamped + 2.5) / 4.0;
const h = 240 - t * 240;
return `hsl(${h}, 70%, 50%)`;
}
function forceLen(kN: number): number {
return Math.min(Math.max(Math.abs(kN) * 8, 15), 80);
}
interface WarehouseProps {
width: number;
length: number;
height: number;
roofPitch: number;
wallCpe: { A: number; B: number; C: number; D: number };
roofCpe: { E: number; F: number; G: number; H: number };
windAngle: 0 | 90;
cpi: number;
}
function WarehouseDiagram({ width, length, height, roofPitch, wallCpe, roofCpe, cpi }: WarehouseProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 80) / length, (vh - 100) / (height + (width / 2) * Math.tan((roofPitch * Math.PI) / 180)));
const ox = vw / 2, oy = vh - 40;
const wS = width * s, lS = length * s, hS = height * s;
const roofH = (width / 2) * Math.tan((roofPitch * Math.PI) / 180) * s;
const hx = lS / 2, hy = hS;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
{/* Chão */}
<line x1={ox - hx - 20} y1={oy} x2={ox + hx + 20} y2={oy} stroke="#94a3b8" strokeWidth={1} />
{/* Parede frontal */}
<rect x={ox - hx} y={oy - hy} width={lS} height={hy} fill={pressureColor(wallCpe.C, cpi)} opacity={0.8} stroke="#334155" strokeWidth={1.5} />
<text x={ox} y={oy - hy / 2} textAnchor="middle" fontSize={10} fill="#1e293b" fontWeight="bold">C</text>
{/* Parede lateral esquerda (projeção) */}
<polygon points={`${ox - hx},${oy - hy} ${ox - hx - wS * 0.4},${oy - hy - wS * 0.2} ${ox - hx - wS * 0.4},${oy - wS * 0.2} ${ox - hx},${oy}`}
fill={pressureColor(wallCpe.D, cpi)} opacity={0.6} stroke="#334155" strokeWidth={1} />
<text x={ox - hx - wS * 0.2 - 5} y={oy - hy / 2 - wS * 0.1} fontSize={9} fill="#1e293b" fontWeight="bold">D</text>
{/* Telhado */}
<polygon points={`${ox - hx},${oy - hy} ${ox},${oy - hy - roofH} ${ox + hx},${oy - hy} ${ox + hx - wS * 0.4},${oy - hy - wS * 0.2} ${ox},${oy - hy - roofH - wS * 0.2} ${ox - hx - wS * 0.4},${oy - hy - wS * 0.2}`}
fill={roofCpe.G ? cpeColor(roofCpe.G) : '#94a3b8'} opacity={0.7} stroke="#334155" strokeWidth={1} />
<text x={ox + 15} y={oy - hy - roofH / 2} fontSize={9} fill="#7c3aed" fontWeight="bold">G/H</text>
{/* Rótulos de dimensão */}
<text x={ox} y={oy + 18} textAnchor="middle" fontSize={9} fill="#475569">L = {length}m</text>
<text x={ox - hx - 15} y={oy - hy / 2} textAnchor="middle" fontSize={9} fill="#475569" transform={`rotate(-90, ${ox - hx - 15}, ${oy - hy / 2})`}>h = {height}m</text>
{/* Seta de vento */}
<defs><marker id="warrow" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#22c55e" /></marker></defs>
<line x1={30} y1={oy - hS / 2} x2={70} y2={oy - hS / 2} stroke="#22c55e" strokeWidth={2} markerEnd="url(#warrow)" />
<text x={50} y={oy - hS / 2 - 8} textAnchor="middle" fontSize={8} fill="#22c55e">Vento</text>
</svg>
);
}
interface CylinderProps {
diameter: number;
height: number;
cpi: number;
cpeProfile: { angle: number; cpe: number }[];
}
function CylinderDiagram({ diameter, height, cpeProfile }: CylinderProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 100) / diameter, (vh - 80) / height);
const cx = vw / 2, cy = vh - 40;
const r = (diameter / 2) * s;
const h = height * s;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - r - 30} y1={cy} x2={cx + r + 30} y2={cy} stroke="#94a3b8" strokeWidth={1} />
{/* Cilindro — perfil lateral com cores */}
{cpeProfile.slice(0, -1).map((p, i) => {
const next = cpeProfile[i + 1];
const x0 = cx - r + (i / (cpeProfile.length - 1)) * r * 2;
const x1 = cx - r + ((i + 1) / (cpeProfile.length - 1)) * r * 2;
const mid = (p.cpe + next.cpe) / 2;
return <rect key={i} x={x0} y={cy - h} width={x1 - x0} height={h} fill={cpeColor(mid)} opacity={0.85} />;
})}
{/* Outline */}
<rect x={cx - r} y={cy - h} width={r * 2} height={h} fill="none" stroke="#334155" strokeWidth={1.5} rx={2} />
{/* Tampa superior */}
<ellipse cx={cx} cy={cy - h} rx={r} ry={6} fill={cpeColor(cpeProfile[cpeProfile.length - 1]?.cpe ?? -1)} opacity={0.7} stroke="#334155" strokeWidth={1} />
<text x={cx} y={cy - h - 10} textAnchor="middle" fontSize={9} fill="#475569">d = {diameter}m</text>
<text x={cx - r - 15} y={cy - h / 2} textAnchor="middle" fontSize={9} fill="#475569" transform={`rotate(-90, ${cx - r - 15}, ${cy - h / 2})`}>h = {height}m</text>
<text x={cx} y={cy + 18} textAnchor="middle" fontSize={9} fill="#475569">Cpe: {cpeProfile[0]?.cpe.toFixed(1)} (0°) {cpeProfile[Math.floor(cpeProfile.length / 2)]?.cpe.toFixed(1)} (90°)</text>
</svg>
);
}
interface VaultProps {
span: number;
length: number;
rise: number;
cpi: number;
cpeProfile: Record<string, number>;
}
function VaultDiagram({ span, rise, cpeProfile }: VaultProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 80) / span, (vh - 80) / rise);
const ox = vw / 2, oy = vh - 40;
const spanS = span * s, riseS = rise * s;
const archPoints: string[] = [];
const segments = 32;
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const x = ox - spanS / 2 + t * spanS;
const y = oy - riseS * Math.sin(t * Math.PI);
archPoints.push(`${x},${y}`);
}
const zones = [
{ idx: 0, label: '1', key: 'zone1' },
{ idx: 5, label: '2', key: 'zone2' },
{ idx: 11, label: '3', key: 'zone3' },
{ idx: 16, label: '4', key: 'zone4' },
{ idx: 21, label: '5', key: 'zone5' },
{ idx: 27, label: '6', key: 'zone6' },
];
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={ox - spanS / 2 - 20} y1={oy} x2={ox + spanS / 2 + 20} y2={oy} stroke="#94a3b8" strokeWidth={1} />
{/* Arco */}
<polygon points={`${ox - spanS / 2},${oy} ${archPoints.join(' ')} ${ox + spanS / 2},${oy}`}
fill="none" stroke="#334155" strokeWidth={1.5} />
{/* Zonas coloridas */}
{zones.map((z, i) => {
const nextIdx = i < zones.length - 1 ? zones[i + 1].idx : segments;
const pts: string[] = [];
for (let j = z.idx; j <= nextIdx; j++) {
const t = j / segments;
pts.push(`${ox - spanS / 2 + t * spanS},${oy - riseS * Math.sin(t * Math.PI)}`);
}
const lastT = nextIdx / segments;
pts.push(`${ox - spanS / 2 + lastT * spanS},${oy}`);
const firstT = z.idx / segments;
pts.push(`${ox - spanS / 2 + firstT * spanS},${oy}`);
const cpeVal = cpeProfile[z.key] ?? -0.5;
const midX = ox - spanS / 2 + ((z.idx + nextIdx) / 2 / segments) * spanS;
const midY = oy - riseS * 0.6;
return (
<g key={z.key}>
<polygon points={pts.join(' ')} fill={cpeColor(cpeVal)} opacity={0.7} />
<text x={midX} y={midY} textAnchor="middle" fontSize={10} fill="#1e293b" fontWeight="bold">{z.label}</text>
</g>
);
})}
<text x={ox} y={oy + 18} textAnchor="middle" fontSize={9} fill="#475569">vão = {span}m</text>
<text x={ox - spanS / 2 - 15} y={oy - riseS / 2} textAnchor="middle" fontSize={9} fill="#475569" transform={`rotate(-90, ${ox - spanS / 2 - 15}, ${oy - riseS / 2})`}>flecha = {rise}m</text>
</svg>
);
}
interface DomeProps {
diameter: number;
rise: number;
wallHeight: number;
cpi: number;
cpeBarlavento: number;
cpeTopo: number;
cpeLateral: number;
}
function DomeDiagram({ diameter, rise, wallHeight, cpeBarlavento, cpeTopo }: DomeProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 80) / diameter, (vh - 80) / (wallHeight + rise));
const cx = vw / 2, cy = vh - 40;
const r = (diameter / 2) * s;
const wh = wallHeight * s;
const rh = rise * s;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - r - 30} y1={cy} x2={cx + r + 30} y2={cy} stroke="#94a3b8" strokeWidth={1} />
{/* Parede cilíndrica */}
<rect x={cx - r} y={cy - wh} width={r * 2} height={wh} fill="#94a3b8" opacity={0.5} stroke="#334155" strokeWidth={1.5} />
{/* Cúpula — 3 zonas */}
<path d={`M ${cx - r} ${cy - wh} Q ${cx - r} ${cy - wh - rh * 0.6} ${cx} ${cy - wh - rh} Q ${cx + r} ${cy - wh - rh * 0.6} ${cx + r} ${cy - wh}`}
fill={cpeColor(cpeBarlavento)} opacity={0.7} stroke="#334155" strokeWidth={1.5} />
<path d={`M ${cx - r * 0.5} ${cy - wh - rh * 0.9} Q ${cx} ${cy - wh - rh} ${cx + r * 0.5} ${cy - wh - rh * 0.9}`}
fill={cpeColor(cpeTopo)} opacity={0.7} stroke="#334155" strokeWidth={1} />
{/* Labels */}
<text x={cx - r * 0.6} y={cy - wh - rh * 0.3} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Barlavento</text>
<text x={cx} y={cy - wh - rh - 5} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Topo</text>
<text x={cx + r * 0.6} y={cy - wh - rh * 0.3} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Lateral</text>
<text x={cx} y={cy + 18} textAnchor="middle" fontSize={9} fill="#475569">d = {diameter}m | h = {wallHeight}m | flecha = {rise}m</text>
</svg>
);
}
interface SignProps {
length: number;
height: number;
groundClearance: number;
alpha: number;
cf: number;
forceKN: number;
applicationPoint: number;
}
function SignDiagram({ length, height, groundClearance, cf, forceKN }: SignProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 100) / length, (vh - 80) / (height + groundClearance));
const cx = vw / 2, ground = vh - 40;
const gc = groundClearance * s;
const h = height * s;
const w = length * s;
const plateY = ground - gc - h;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - w - 30} y1={ground} x2={cx + w + 30} y2={ground} stroke="#94a3b8" strokeWidth={1} />
{/* Placa */}
<rect x={cx - w / 2} y={plateY} width={w} height={h} fill={cpeColor(cf)} opacity={0.7} stroke="#334155" strokeWidth={1.5} />
{/* Suporte */}
<line x1={cx} y1={plateY + h} x2={cx} y2={ground} stroke="#475569" strokeWidth={3} />
<text x={cx} y={plateY + h / 2 + 4} textAnchor="middle" fontSize={10} fill="#1e293b" fontWeight="bold">Cf = {cf.toFixed(2)}</text>
{/* Seta de força */}
{forceKN > 0 && (
<g>
<defs><marker id="sarrow" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#ef4444" /></marker></defs>
<line x1={cx + w / 2 + 10} y1={plateY + h / 2} x2={cx + w / 2 + 10 + forceLen(forceKN)} y2={plateY + h / 2}
stroke="#ef4444" strokeWidth={2} markerEnd="url(#sarrow)" />
<text x={cx + w / 2 + 10 + forceLen(forceKN) / 2} y={plateY + h / 2 - 6} textAnchor="middle" fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
</g>
)}
{/* Dimensões */}
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569"> = {length}m | h = {height}m | e = {groundClearance}m</text>
</svg>
);
}
interface BarProps {
barType: 'flat' | 'circular';
section?: string;
diameter?: number;
width?: number;
length: number;
alpha: number;
fxKN: number;
fyKN: number;
cx: number;
}
function BarDiagram({ barType, length, alpha, fxKN, fyKN, cx: cxVal }: BarProps) {
const vw = 400, vh = 300;
const cx = vw / 2, cy = vh / 2;
const barLen = Math.min(length * 8, vw - 100);
const forceMag = Math.sqrt(fxKN * fxKN + fyKN * fyKN);
const forceAngle = Math.atan2(fyKN, fxKN);
void barType;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<g transform={`rotate(${-alpha * 180 / Math.PI}, ${cx}, ${cy})`}>
{/* Barra */}
<line x1={cx - barLen / 2} y1={cy} x2={cx + barLen / 2} y2={cy} stroke={cpeColor(cxVal)} strokeWidth={barType === 'circular' ? 6 : 10} strokeLinecap="round" />
<text x={cx} y={cy - 12} textAnchor="middle" fontSize={9} fill="#475569">{barType === 'circular' ? `d=${length}m` : `=${length}m`}</text>
</g>
{/* Eixo */}
<line x1={cx - barLen / 2 - 15} y1={cy} x2={cx + barLen / 2 + 15} y2={cy} stroke="#94a3b8" strokeWidth={0.5} strokeDasharray="4" />
{/* Seta de força */}
{forceMag > 0.01 && (
<g>
<defs><marker id="barrow" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#ef4444" /></marker></defs>
<line x1={cx} y1={cy} x2={cx + Math.cos(forceAngle) * forceLen(forceMag)} y2={cy + Math.sin(forceAngle) * forceLen(forceMag)}
stroke="#ef4444" strokeWidth={2} markerEnd="url(#barrow)" />
<text x={cx + Math.cos(forceAngle) * forceLen(forceMag) / 2} y={cy + Math.sin(forceAngle) * forceLen(forceMag) / 2 - 6}
textAnchor="middle" fontSize={8} fill="#ef4444">{forceMag.toFixed(1)} kN</text>
</g>
)}
<text x={cx} y={vh - 15} textAnchor="middle" fontSize={9} fill="#475569">α = {alpha}° | Cx = {cxVal.toFixed(2)}</text>
</svg>
);
}
interface BridgeProps {
lp: number;
width: number;
deckHeight: number;
heg: number;
cx: number;
cz: number;
fxPerLength: number;
fzPerLength: number;
}
function BridgeDiagram({ lp, width, deckHeight, heg, cx: cxVal, fxPerLength, fzPerLength }: BridgeProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 80) / lp, (vh - 80) / (deckHeight + heg));
const ox = vw / 2, ground = vh - 40;
const lpS = lp * s;
const dh = deckHeight * s;
const deckT = Math.max(heg, 0.8) * s;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={ox - lpS / 2 - 30} y1={ground} x2={ox + lpS / 2 + 30} y2={ground} stroke="#94a3b8" strokeWidth={1} />
{/* Água/solo */}
<rect x={ox - lpS / 2 - 20} y={ground - 5} width={lpS + 40} height={10} fill="#60a5fa" opacity={0.3} rx={2} />
{/* Pilares */}
{[-0.35, 0, 0.35].map((frac, i) => (
<rect key={i} x={ox + frac * lpS - 5} y={ground - dh} width={10} height={dh} fill="#64748b" opacity={0.7} />
))}
{/* Tabuleiro */}
<rect x={ox - lpS / 2} y={ground - dh - deckT} width={lpS} height={deckT} fill={cpeColor(cxVal)} opacity={0.8} stroke="#334155" strokeWidth={1.5} />
<text x={ox} y={ground - dh - deckT / 2 + 4} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Cx = {cxVal.toFixed(2)}</text>
{/* Guarda-rodas */}
<line x1={ox - lpS / 2} y1={ground - dh - deckT - 3} x2={ox + lpS / 2} y2={ground - dh - deckT - 3} stroke="#94a3b8" strokeWidth={2} />
{/* Setas de força */}
<defs>
<marker id="bga" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#ef4444" /></marker>
<marker id="bgb" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#3b82f6" /></marker>
</defs>
{fxPerLength !== 0 && (
<line x1={ox - lpS / 2 - 5} y1={ground - dh - deckT / 2} x2={ox - lpS / 2 - 5 + forceLen(fxPerLength)} y2={ground - dh - deckT / 2}
stroke="#ef4444" strokeWidth={2} markerEnd="url(#bga)" />
)}
{fzPerLength !== 0 && (
<line x1={ox + lpS / 2 + 5} y1={ground - dh - deckT} x2={ox + lpS / 2 + 5} y2={ground - dh - deckT - forceLen(fzPerLength)}
stroke="#3b82f6" strokeWidth={2} markerEnd="url(#bgb)" />
)}
<text x={ox} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">Lp = {lp}m | B = {width}m | z = {deckHeight}m</text>
</svg>
);
}
interface TowerProps {
section: 'square' | 'triangular';
baseWidth: number;
height: number;
panels: number;
phi: number;
alphaWind: number;
forceKN: number;
}
function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 80) / baseWidth, (vh - 80) / height);
const cx = vw / 2, ground = vh - 40;
const bw = baseWidth * s;
const h = height * s;
const lines: React.ReactElement[] = [];
for (let p = 0; p < panels; p++) {
const y0 = ground - (p / panels) * h;
const y1 = ground - ((p + 1) / panels) * h;
const shrink = p / panels;
const nextShrink = (p + 1) / panels;
const w0 = bw * (1 - shrink * 0.6);
const w1 = bw * (1 - nextShrink * 0.6);
// Montantes
lines.push(<line key={`l${p}`} x1={cx - w0 / 2} y1={y0} x2={cx - w1 / 2} y2={y1} stroke="#1e293b" strokeWidth={2} />);
lines.push(<line key={`r${p}`} x1={cx + w0 / 2} y1={y0} x2={cx + w1 / 2} y2={y1} stroke="#1e293b" strokeWidth={2} />);
// Diagonais
lines.push(<line key={`d1${p}`} x1={cx - w0 / 2} y1={y0} x2={cx + w1 / 2} y2={y1} stroke={cpeColor(phi)} strokeWidth={1} opacity={0.7} />);
lines.push(<line key={`d2${p}`} x1={cx + w0 / 2} y1={y0} x2={cx - w1 / 2} y2={y1} stroke={cpeColor(phi)} strokeWidth={1} opacity={0.7} />);
// Travessa
lines.push(<line key={`h${p}`} x1={cx - w1 / 2} y1={y1} x2={cx + w1 / 2} y2={y1} stroke="#64748b" strokeWidth={1} />);
}
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - bw - 20} y1={ground} x2={cx + bw + 20} y2={ground} stroke="#94a3b8" strokeWidth={1} />
{lines}
{/* Seta de força */}
{forceKN > 0 && (
<g>
<defs><marker id="tarrow" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#ef4444" /></marker></defs>
<line x1={cx} y1={ground - h - 5} x2={cx + forceLen(forceKN)} y2={ground - h - 5} stroke="#ef4444" strokeWidth={2} markerEnd="url(#tarrow)" />
<text x={cx + forceLen(forceKN) / 2} y={ground - h - 12} textAnchor="middle" fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
</g>
)}
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">h = {height}m | base = {baseWidth}m | φ = {phi.toFixed(2)}</text>
</svg>
);
}
interface IsolatedRoofProps {
type: 'shed' | 'gable';
theta: number;
height: number;
depth: number;
cpeWindward: number;
cpeLeeward: number;
cpeTop: number;
forceKN: number;
}
function IsolatedRoofDiagram({ type, theta, height, 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;
const h = height * s;
const d = depth * s;
const rise = d * Math.tan((theta * Math.PI) / 180);
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - d - 30} y1={ground} x2={cx + d + 30} y2={ground} stroke="#94a3b8" strokeWidth={1} />
{/* Pilares */}
<line x1={cx - d / 2} y1={ground} x2={cx - d / 2} y2={ground - h} stroke="#475569" strokeWidth={3} />
<line x1={cx + d / 2} y1={ground} x2={cx + d / 2} y2={ground - h} stroke="#475569" strokeWidth={3} />
{type === 'gable' && <line x1={cx} y1={ground} x2={cx} y2={ground - h} stroke="#475569" strokeWidth={3} />}
{/* Cobertura */}
{type === 'shed' ? (
<polygon points={`${cx - d / 2},${ground - h} ${cx + d / 2},${ground - h - rise} ${cx + d / 2},${ground - h - rise + 3} ${cx - d / 2},${ground - h + 3}`}
fill={cpeColor(cpeTop)} opacity={0.8} stroke="#334155" strokeWidth={1.5} />
) : (
<>
<line x1={cx - d / 2} y1={ground - h} x2={cx} y2={ground - h - rise} stroke="#334155" strokeWidth={2} />
<line x1={cx} y1={ground - h - rise} x2={cx + d / 2} y2={ground - h} stroke="#334155" strokeWidth={2} />
<polygon points={`${cx - d / 2},${ground - h} ${cx},${ground - h - rise} ${cx + d / 2},${ground - h}`}
fill={cpeColor(cpeTop)} opacity={0.6} stroke="#334155" strokeWidth={1.5} />
</>
)}
{/* Labels de zona */}
<text x={cx - d / 3} y={ground - h - rise * 0.3} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Barl. {cpeWindward.toFixed(1)}</text>
<text x={cx + d / 3} y={ground - h - rise * 0.3} textAnchor="middle" fontSize={9} fill="#1e293b" fontWeight="bold">Sot. {cpeLeeward.toFixed(1)}</text>
<text x={cx} y={ground - h - rise - 8} textAnchor="middle" fontSize={9} fill="#7c3aed" fontWeight="bold">Topo {cpeTop.toFixed(1)}</text>
{/* Seta de força */}
{forceKN > 0 && (
<g>
<defs><marker id="iroof" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#ef4444" /></marker></defs>
<line x1={cx} y1={ground - h - rise - 12} x2={cx} y2={ground - h - rise - 12 - forceLen(forceKN)}
stroke="#ef4444" strokeWidth={2} markerEnd="url(#iroof)" />
<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>
</svg>
);
}
interface DynamicsProps {
height: number;
freq: number;
windSpeed: number;
scruton: number;
sectionShape: string;
sectionSize: number;
showVortexStreet: boolean;
showModeShape: boolean;
}
function DynamicsDiagram({ height, freq, scruton, sectionShape, sectionSize, showVortexStreet, showModeShape }: DynamicsProps) {
const vw = 400, vh = 300;
const s = Math.min((vw - 100) / sectionSize, (vh - 80) / height);
const cx = vw / 2, ground = vh - 40;
const h = height * s;
const w = sectionSize * s;
void sectionShape;
return (
<svg viewBox={`0 0 ${vw} ${vh}`} className="w-full h-full">
<rect width={vw} height={vh} fill="none" />
<line x1={cx - w - 40} y1={ground} x2={cx + w + 80} y2={ground} stroke="#94a3b8" strokeWidth={1} />
{/* Estrutura */}
{sectionShape === 'circle' ? (
<ellipse cx={cx} cy={ground - h / 2} rx={w / 2} ry={h / 2} fill="#3b82f6" opacity={0.6} stroke="#1e40af" strokeWidth={1.5} />
) : (
<rect x={cx - w / 2} y={ground - h} width={w} height={h} fill="#3b82f6" opacity={0.6} stroke="#1e40af" strokeWidth={1.5} rx={2} />
)}
{/* Modo de oscilação */}
{showModeShape && (
<path d={`M ${cx} ${ground} Q ${cx + 8} ${ground - h * 0.5} ${cx} ${ground - h}`}
fill="none" stroke="#ef4444" strokeWidth={2} strokeDasharray="4" />
)}
{/* Rua de vórtices */}
{showVortexStreet && (
<g>
{[0.2, 0.4, 0.6, 0.8, 1.0].map((_, i) => (
<circle key={i} cx={cx + w / 2 + 20 + i * 18} cy={ground - h / 2 + (i % 2 === 0 ? -1 : 1) * (10 + i * 3)}
r={4 - i * 0.5} fill="#a855f7" opacity={0.8 - i * 0.12} />
))}
</g>
)}
{/* Seta de vento */}
<defs><marker id="darrow" markerWidth={8} markerHeight={6} refX={8} refY={3} orient="auto"><polygon points="0,0 8,3 0,6" fill="#22c55e" /></marker></defs>
<line x1={30} y1={ground - h / 2} x2={70} y2={ground - h / 2} stroke="#22c55e" strokeWidth={2} markerEnd="url(#darrow)" />
<text x={50} y={ground - h / 2 - 8} textAnchor="middle" fontSize={8} fill="#22c55e">Vento</text>
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">h = {height}m | f = {freq}Hz | Sc = {scruton.toFixed(1)}</text>
</svg>
);
}
export type FallbackDiagramProps =
| { type: 'warehouse'; props: WarehouseProps }
| { type: 'cylinder'; props: CylinderProps }
| { type: 'vault'; props: VaultProps }
| { type: 'dome'; props: DomeProps }
| { type: 'sign'; props: SignProps }
| { type: 'bar'; props: BarProps }
| { type: 'bridge'; props: BridgeProps }
| { type: 'tower'; props: TowerProps }
| { type: 'isolatedRoof'; props: IsolatedRoofProps }
| { type: 'dynamics'; props: DynamicsProps };
export default function FallbackDiagram(input: FallbackDiagramProps) {
return (
<div style={{ width: '100%', height: '100%', minHeight: '300px', borderRadius: 'var(--radius-lg)', overflow: 'hidden' }}
className="glass-panel flex items-center justify-center bg-muted/10">
{input.type === 'warehouse' && <WarehouseDiagram {...input.props} />}
{input.type === 'cylinder' && <CylinderDiagram {...input.props} />}
{input.type === 'vault' && <VaultDiagram {...input.props} />}
{input.type === 'dome' && <DomeDiagram {...input.props} />}
{input.type === 'sign' && <SignDiagram {...input.props} />}
{input.type === 'bar' && <BarDiagram {...input.props} />}
{input.type === 'bridge' && <BridgeDiagram {...input.props} />}
{input.type === 'tower' && <TowerDiagram {...input.props} />}
{input.type === 'isolatedRoof' && <IsolatedRoofDiagram {...input.props} />}
{input.type === 'dynamics' && <DynamicsDiagram {...input.props} />}
</div>
);
}