Files
BrainWind/app/src/lib/export-ftool.ts
T

275 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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('; BrainWind v1.0 — 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);
}