feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user