/** * DXF Writer – converts CADElement[] to DXF string * Minimal DXF R12 format for compatibility */ import type { CADElement, CADLayer, ProjectData } from '../types/cad.types'; /** * Generate a DXF R12 string from project data. */ export function writeDXF(data: ProjectData): string { const lines: string[] = []; const w = (code: number, value: string | number) => { lines.push(String(code)); lines.push(String(value)); }; // Header w(0, 'SECTION'); w(2, 'HEADER'); w(9, '$ACADVER'); w(1, 'AC1009'); // R12 w(9, '$INSBASE'); w(10, 0); w(20, 0); w(30, 0); w(9, '$EXTMIN'); w(10, 0); w(20, 0); w(9, '$EXTMAX'); w(10, 1000); w(20, 1000); w(0, 'ENDSEC'); // Tables section w(0, 'SECTION'); w(2, 'TABLES'); // Layer table w(0, 'TABLE'); w(2, 'LAYER'); w(70, data.layers.length); for (const layer of data.layers) { w(0, 'LAYER'); w(2, layer.name); w(70, 0); w(62, hexToACI(layer.color)); w(6, lineTypeToDXF(layer.lineType)); } w(0, 'ENDTAB'); w(0, 'ENDSEC'); // Entities section w(0, 'SECTION'); w(2, 'ENTITIES'); for (const el of data.elements) { const layerName = getLayerName(data.layers, el.layerId); writeEntity(w, el, layerName); } w(0, 'ENDSEC'); // EOF w(0, 'EOF'); return lines.join('\n'); } function writeEntity( w: (code: number, value: string | number) => void, el: CADElement, layerName: string, ): void { const p = el.properties; const stroke = (p.stroke as string) || '#ffffff'; const aci = hexToACI(stroke); switch (el.type) { case 'line': { w(0, 'LINE'); w(8, layerName); w(62, aci); w(10, p.x1 ?? el.x); w(20, p.y1 ?? el.y); w(30, 0); w(11, p.x2 ?? el.x + el.width); w(21, p.y2 ?? el.y + el.height); w(31, 0); break; } case 'circle': { const cx = el.x + el.width / 2; const cy = el.y + el.height / 2; const r = p.radius ?? el.width / 2; w(0, 'CIRCLE'); w(8, layerName); w(62, aci); w(10, cx); w(20, cy); w(30, 0); w(40, r); break; } case 'arc': { const cx = el.x + el.width / 2; const cy = el.y + el.height / 2; const r = p.radius ?? el.width / 2; w(0, 'ARC'); w(8, layerName); w(62, aci); w(10, cx); w(20, cy); w(30, 0); w(40, r); w(50, radToDeg(p.startAngle ?? 0)); w(51, radToDeg(p.endAngle ?? 360)); break; } case 'rect': { // Rectangle as LWPOLYLINE w(0, 'LWPOLYLINE'); w(8, layerName); w(62, aci); w(90, 4); w(70, 1); // closed w(10, el.x); w(20, el.y); w(10, el.x + el.width); w(20, el.y); w(10, el.x + el.width); w(20, el.y + el.height); w(10, el.x); w(20, el.y + el.height); break; } case 'polygon': case 'polyline': { const pts = p.points ?? []; w(0, 'LWPOLYLINE'); w(8, layerName); w(62, aci); w(90, pts.length); w(70, el.type === 'polygon' ? 1 : 0); for (const pt of pts) { w(10, pt.x); w(20, pt.y); } break; } case 'text': { w(0, 'TEXT'); w(8, layerName); w(62, aci); w(10, el.x); w(20, el.y); w(30, 0); w(40, p.fontSize ?? 12); w(1, p.text ?? ''); break; } case 'dimension': { const pts = p.points ?? []; if (pts.length >= 2) { // Draw dimension as LINE + TEXT w(0, 'LINE'); w(8, layerName); w(62, aci); w(10, pts[0].x); w(20, pts[0].y); w(30, 0); w(11, pts[1].x); w(21, pts[1].y); w(31, 0); const midX = (pts[0].x + pts[1].x) / 2; const midY = (pts[0].y + pts[1].y) / 2; const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y); w(0, 'TEXT'); w(8, layerName); w(62, aci); w(10, midX); w(20, midY); w(30, 0); w(40, 12); w(1, dist.toFixed(2)); } break; } case 'block_instance': { w(0, 'INSERT'); w(8, layerName); w(62, aci); w(2, p.blockId ?? 'BLOCK'); w(10, el.x); w(20, el.y); w(30, 0); w(41, p.scale ?? 1); w(42, p.scale ?? 1); w(50, radToDeg(p.rotation ?? 0)); break; } default: // For chair, seating-row, etc. — export as LWPOLYLINE bounding box w(0, 'LWPOLYLINE'); w(8, layerName); w(62, aci); w(90, 4); w(70, 1); w(10, el.x); w(20, el.y); w(10, el.x + el.width); w(20, el.y); w(10, el.x + el.width); w(20, el.y + el.height); w(10, el.x); w(20, el.y + el.height); break; } } function getLayerName(layers: CADLayer[], layerId: string): string { return layers.find(l => l.id === layerId)?.name ?? '0'; } function hexToACI(hex: string): number { const h = hex.replace('#', ''); const r = parseInt(h.substring(0, 2), 16); const g = parseInt(h.substring(2, 4), 16); const b = parseInt(h.substring(4, 6), 16); // Map common colors to ACI if (r > 200 && g < 100 && b < 100) return 1; // red if (r > 200 && g > 200 && b < 100) return 2; // yellow if (r < 100 && g > 200 && b < 100) return 3; // green if (r < 100 && g > 200 && b > 200) return 4; // cyan if (r < 100 && g < 100 && b > 200) return 5; // blue if (r > 200 && g < 100 && b > 200) return 6; // magenta if (r > 200 && g > 200 && b > 200) return 7; // white if (r < 100 && g < 100 && b < 100) return 8; // gray return 7; // default white } function lineTypeToDXF(lt: string): string { if (lt === 'dashed') return 'DASHED'; if (lt === 'dotted') return 'DOT'; return 'CONTINUOUS'; } function radToDeg(rad: number): number { return (rad * 180) / Math.PI; }