/** * 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); }