Files
SPipe/src/dxfExport.ts
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

179 lines
5.8 KiB
TypeScript

import type { NetworkModel } from './inpModel';
import type { SimulationResult } from './types';
interface DxfExportOptions {
model: NetworkModel;
simulationResult?: SimulationResult | null;
scenarioName?: string;
}
/**
* Generates an AutoCAD ASCII DXF (R12 / AC1009) format drawing file
* with organized CAD layers for pipes, nodes, text labels, and hydraulic results.
*/
export function generateDxf({
model,
simulationResult,
scenarioName = 'Baseline',
}: DxfExportOptions): string {
const lines: string[] = [];
const add = (...tokens: (string | number)[]) => {
for (const t of tokens) {
lines.push(String(t));
}
};
// Compute model extents to determine appropriate text height and circle radius
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const n of Object.values(model.nodes)) {
if (n.x < minX) minX = n.x;
if (n.y < minY) minY = n.y;
if (n.x > maxX) maxX = n.x;
if (n.y > maxY) maxY = n.y;
}
if (minX === Infinity) {
minX = 0; minY = 0; maxX = 100; maxY = 100;
}
const modelSpan = Math.max(maxX - minX, maxY - minY) || 100;
const textHeight = Math.max(0.5, Math.round((modelSpan * 0.015) * 10) / 10);
const nodeRadius = Math.max(0.6, Math.round((modelSpan * 0.018) * 10) / 10);
// 1. HEADER SECTION
add('0', 'SECTION');
add('2', 'HEADER');
add('9', '$ACADVER', '1', 'AC1009'); // AutoCAD Release 12 DXF standard
add('9', '$INSUNITS', '70', '6'); // Meters (or Unitless)
add('0', 'ENDSEC');
// 2. TABLES SECTION (Standard Engineering Layer Definitions)
add('0', 'SECTION');
add('2', 'TABLES');
add('0', 'TABLE');
add('2', 'LAYER');
add('70', '7');
const defineLayer = (name: string, color: number) => {
add('0', 'LAYER');
add('2', name);
add('70', '0');
add('62', color); // AutoCAD ACI Color
add('6', 'CONTINUOUS');
};
defineLayer('PIPE', 4); // Cyan
defineLayer('PIPE_TEXT', 3); // Green (ID, Dia, Len)
defineLayer('PIPE_FLOW', 2); // Yellow (Q, V)
defineLayer('NODE', 1); // Red
defineLayer('NODE_TEXT', 7); // White/Black (ID, Elev)
defineLayer('NODE_PRESS', 5); // Blue (P)
defineLayer('FACILITY', 6); // Magenta (Reservoir, Tank)
add('0', 'ENDTAB');
add('0', 'ENDSEC');
// 3. BLOCKS SECTION (Empty but required)
add('0', 'SECTION');
add('2', 'BLOCKS');
add('0', 'ENDSEC');
// 4. ENTITIES SECTION
add('0', 'SECTION');
add('2', 'ENTITIES');
// Title Text entity
add('0', 'TEXT');
add('8', 'NODE_TEXT');
add('10', minX, '20', maxY + textHeight * 3, '30', '0.0');
add('40', textHeight * 1.5);
add('1', `spipe Network Drawing - ${scenarioName}`);
add('50', '0.0');
// A. Export Nodes (Junctions, Reservoirs, Tanks)
for (const node of Object.values(model.nodes)) {
const isFacility = node.type === 'RESERVOIR' || node.type === 'TANK';
const layer = isFacility ? 'FACILITY' : 'NODE';
const r = isFacility ? nodeRadius * 1.6 : nodeRadius;
// Node Circle entity
add('0', 'CIRCLE');
add('8', layer);
add('10', node.x.toFixed(3), '20', node.y.toFixed(3), '30', '0.0');
add('40', r.toFixed(3));
// Node ID and Elevation Text entity
const elevStr = `EL.${node.elevation.toFixed(1)}`;
add('0', 'TEXT');
add('8', 'NODE_TEXT');
add('10', (node.x + r * 1.2).toFixed(3), '20', (node.y + r * 0.4).toFixed(3), '30', '0.0');
add('40', textHeight.toFixed(3));
add('1', `${node.id} (${elevStr})`);
add('50', '0.0');
// Hydraulic Pressure Text entity
if (simulationResult) {
const res = simulationResult.nodes[node.id];
if (res) {
const pressUnit = simulationResult.units.pressure;
const pressStr = `P=${res.pressure.toFixed(1)}${pressUnit}`;
add('0', 'TEXT');
add('8', 'NODE_PRESS');
add('10', (node.x + r * 1.2).toFixed(3), '20', (node.y - r * 1.0).toFixed(3), '30', '0.0');
add('40', (textHeight * 0.9).toFixed(3));
add('1', pressStr);
add('50', '0.0');
}
}
}
// B. Export Pipes and Pumps
for (const pipe of Object.values(model.pipes)) {
const n1 = model.nodes[pipe.node1];
const n2 = model.nodes[pipe.node2];
if (!n1 || !n2) continue;
// Pipe Line entity
add('0', 'LINE');
add('8', 'PIPE');
add('10', n1.x.toFixed(3), '20', n1.y.toFixed(3), '30', '0.0');
add('11', n2.x.toFixed(3), '21', n2.y.toFixed(3), '31', '0.0');
// Pipe Midpoint and Angle
const midX = (n1.x + n2.x) / 2;
const midY = (n1.y + n2.y) / 2;
let angleDeg = Math.atan2(n2.y - n1.y, n2.x - n1.x) * (180 / Math.PI);
if (angleDeg < -90) angleDeg += 180;
if (angleDeg > 90) angleDeg -= 180;
// Pipe Annotation Text: ID, Diameter, Length
const diaStr = `D${pipe.diameter}`;
const lenStr = `L${Math.round(pipe.length)}`;
add('0', 'TEXT');
add('8', 'PIPE_TEXT');
add('10', midX.toFixed(3), '20', (midY + textHeight * 0.4).toFixed(3), '30', '0.0');
add('40', (textHeight * 0.85).toFixed(3));
add('1', `${pipe.id}:${diaStr}-${lenStr}`);
add('50', angleDeg.toFixed(1));
// Hydraulic Flow and Velocity Text entity
if (simulationResult) {
const res = simulationResult.links[pipe.id];
if (res) {
const flowStr = `Q=${res.flow.toFixed(1)} V=${res.velocity.toFixed(2)}`;
add('0', 'TEXT');
add('8', 'PIPE_FLOW');
add('10', midX.toFixed(3), '20', (midY - textHeight * 1.2).toFixed(3), '30', '0.0');
add('40', (textHeight * 0.75).toFixed(3));
add('1', flowStr);
add('50', angleDeg.toFixed(1));
}
}
}
add('0', 'ENDSEC');
add('0', 'EOF');
return lines.join('\r\n');
}