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'); }