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
+275
View File
@@ -0,0 +1,275 @@
/**
* Exportação Ftool (.txt estruturado) — M9.4
*
* Gera um arquivo de texto com nós, barras e cargas lineares no
* formato de importação do Ftool (software livre de análise de
* pórticos planos 2D da PUC-Rio, amplamente usado em escritórios
* brasileiros de cálculo estrutural).
*
* Convenção assumida:
* - Pórtico 2D no plano XY (eixo X horizontal, Y vertical)
* - Vento paralelo ao eixo X (de onde sopra)
* - Cargas distribuídas aplicadas no eixo Y local da barra
* (sinais: + empuxo de baixo p/ cima, sucção de cima p/ baixo)
* - Unidades: kN e m
* - Pórtico típico com 4 colunas + 2 águas (cumeeira)
*
* Saída: arquivo `.txt` pronto para `File → Import` no Ftool.
*/
import { useGalpaoStore } from '../store/galpaoStore';
import { useWindStore } from '../store/appStore';
import {
getColumnLinearLoads,
getRoofLinearLoads,
} from './line-loads';
export interface FtoolNode {
id: number;
x: number;
y: number;
}
export interface FtoolMember {
id: number;
nodeI: number;
nodeJ: number;
section: string;
material: string;
}
export interface FtoolMemberLoad {
memberId: number;
/** Direção da carga: GlobalX ou GlobalY */
direction: 'GlobalX' | 'GlobalY';
/** Tipo de distribuição: Uniform, Point, Linear */
type: 'Uniform' | 'Point' | 'Linear';
/** Valor da carga (kN/m para Uniform, kN para Point) */
value: number;
/** Posição inicial (0..1) para Point/Linear */
startPos?: number;
/** Posição final (0..1) para Linear */
endPos?: number;
}
export interface FtoolModel {
units: { force: 'kN' | 'N' | 'kgf'; length: 'm' | 'cm' | 'mm' };
materials: { id: number; name: string; eKpa: number; nu: number; rho: number }[];
sections: { id: number; name: string; aM2: number; izM4: number }[];
nodes: FtoolNode[];
members: FtoolMember[];
loadCases: { id: number; name: string; loads: FtoolMemberLoad[] }[];
}
/**
* Gera o modelo do pórtico 2D do galpão a partir dos dados do store.
*
* Layout:
* N1 (0, 0) N2 (b/2, h) N3 (b, 0)
* | | |
* | coluna | cumeeira | coluna
* | barlavento | | sotavento
* | | |
* N4 (0, h) N5 (b/2, h+rise) N6 (b, h)
*
* Para vento a 0° (largura perpendicular ao vento):
* - Colunas externas: 4 (vértices)
* - Colunas internas: 0
* - Cumeeira: 2 segmentos (água esquerda e direita)
*
* Para vento a 90° (comprimento perpendicular ao vento), o pórtico
* efetivo vira — usamos o mesmo eixo X.
*/
export function buildFtoolModel(): FtoolModel {
const galpao = useGalpaoStore.getState();
const wind = useWindStore.getState();
const { width: b, height: h, roofPitch, wallCpe, roofCpe } = galpao;
const { q, cpi, windAngle } = wind;
const FRAME_SPACING = 6.0;
const PURLIN_SPACING = 1.5;
const columnLoads = getColumnLinearLoads(cpi, q, wallCpe, FRAME_SPACING, windAngle);
const roofLoads = getRoofLinearLoads(cpi, q, roofCpe, PURLIN_SPACING, roofPitch);
const rise = (b / 2) * Math.tan((roofPitch * Math.PI) / 180);
const nodes: FtoolNode[] = [
{ id: 1, x: 0, y: 0 },
{ id: 2, x: b / 2, y: h },
{ id: 3, x: b, y: 0 },
{ id: 4, x: 0, y: h },
{ id: 5, x: b / 2, y: h + rise },
{ id: 6, x: b, y: h },
];
const members: FtoolMember[] = [
{ id: 1, nodeI: 1, nodeJ: 4, section: 'Coluna', material: 'Aco' },
{ id: 2, nodeI: 4, nodeJ: 5, section: 'TercaE', material: 'Aco' },
{ id: 3, nodeI: 5, nodeJ: 6, section: 'TercaD', material: 'Aco' },
{ id: 4, nodeI: 6, nodeJ: 3, section: 'Coluna', material: 'Aco' },
];
const loadCaseWind: FtoolMemberLoad[] = [
{
memberId: 1,
direction: 'GlobalX',
type: 'Uniform',
value: Number(columnLoads.windward.toFixed(4)),
},
{
memberId: 4,
direction: 'GlobalX',
type: 'Uniform',
value: Number(columnLoads.leeward.toFixed(4)),
},
{
memberId: 2,
direction: 'GlobalY',
type: 'Uniform',
value: Number(roofLoads.E.toFixed(4)),
},
{
memberId: 3,
direction: 'GlobalY',
type: 'Uniform',
value: Number(roofLoads.G.toFixed(4)),
},
];
return {
units: { force: 'kN', length: 'm' },
materials: [
{ id: 1, name: 'Aco', eKpa: 2.0e8, nu: 0.3, rho: 78.5 },
],
sections: [
{ id: 1, name: 'Coluna', aM2: 0.005, izM4: 0.0001 },
{ id: 2, name: 'TercaE', aM2: 0.002, izM4: 0.00003 },
{ id: 3, name: 'TercaD', aM2: 0.002, izM4: 0.00003 },
],
nodes,
members,
loadCases: [
{
id: 1,
name: `Vento ${windAngle}° (q=${q.toFixed(3)} kN/m², Cpi=${cpi.toFixed(2)})`,
loads: loadCaseWind,
},
],
};
}
/**
* Serializa o modelo Ftool em texto compatível com File → Import do Ftool.
*
* Formato de saída (Ftool ASCII):
* - Seções em blocos com palavra-chave de abertura e End.
* - Linhas com `Id valor X valor Y valor` para dados tabulares.
*/
export function serializeFtool(model: FtoolModel): string {
const lines: string[] = [];
lines.push('; =============================================');
lines.push('; VentoApp — Modelo Ftool');
lines.push(`; Gerado em: ${new Date().toISOString()}`);
lines.push('; NBR 6123:2023 — Forças devidas ao vento');
lines.push('; =============================================');
lines.push('');
lines.push('GENERAL');
lines.push(`Units ${model.units.force} ${model.units.length}`);
lines.push('EndGENERAL');
lines.push('');
lines.push('MATERIAL');
model.materials.forEach((m) => {
lines.push(`Id ${m.id}`);
lines.push(`Name "${m.name}"`);
lines.push(`E ${m.eKpa.toExponential(6)}`);
lines.push(`Nu ${m.nu}`);
if (m.rho > 0) lines.push(`Rho ${m.rho}`);
lines.push('EndMATERIAL');
});
lines.push('');
lines.push('SECTION');
model.sections.forEach((s) => {
lines.push(`Id ${s.id}`);
lines.push(`Name "${s.name}"`);
lines.push(`A ${s.aM2.toExponential(6)}`);
lines.push(`Iz ${s.izM4.toExponential(6)}`);
lines.push('EndSECTION');
});
lines.push('');
lines.push('NODE');
model.nodes.forEach((n) => {
lines.push(`Id ${n.id} X ${fmt(n.x)} Y ${fmt(n.y)}`);
});
lines.push('EndNODE');
lines.push('');
const sectionNameById = new Map(model.sections.map((s) => [s.name, s.id]));
const materialNameById = new Map(model.materials.map((m) => [m.name, m.id]));
lines.push('MEMBER');
model.members.forEach((m) => {
const secId = sectionNameById.get(m.section) ?? 1;
const matId = materialNameById.get(m.material) ?? 1;
lines.push(
`Id ${m.id} NodeI ${m.nodeI} NodeJ ${m.nodeJ} SectionId ${secId} MaterialId ${matId}`,
);
});
lines.push('EndMEMBER');
lines.push('');
model.loadCases.forEach((lc) => {
lines.push('LOADCASE');
lines.push(`Id ${lc.id}`);
lines.push(`Name "${lc.name}"`);
lines.push('MEMBERLOAD');
lc.loads.forEach((load) => {
if (load.type === 'Uniform') {
lines.push(
`MemberId ${load.memberId} Dir ${load.direction} Type Uniform Value ${fmt(load.value, 4)}`,
);
} else if (load.type === 'Point') {
lines.push(
`MemberId ${load.memberId} Dir ${load.direction} Type Point Pos ${fmt(load.startPos ?? 0.5)} Value ${fmt(load.value, 4)}`,
);
} else if (load.type === 'Linear') {
lines.push(
`MemberId ${load.memberId} Dir ${load.direction} Type Linear PosIni ${fmt(load.startPos ?? 0)} PosFim ${fmt(load.endPos ?? 1)} ValueIni ${fmt(load.value, 4)} ValueFim ${fmt(load.value, 4)}`,
);
}
});
lines.push('EndMEMBERLOAD');
lines.push('EndLOADCASE');
});
return lines.join('\n') + '\n';
}
function fmt(n: number, decimals = 4): string {
if (!Number.isFinite(n)) return '0';
if (n === 0) return '0';
return n.toFixed(decimals).replace(/\.?0+$/, '');
}
/**
* Exporta o modelo atual como arquivo .txt compatível com Ftool.
*
* Cria um Blob com o conteúdo serializado e dispara download automático.
*/
export function exportGalpaoToFtool(): void {
const model = buildFtoolModel();
const content = serializeFtool(model);
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'galpao_ftool.ftl');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}