2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* DXF Writer – converts CADElement[] to DXF string
|
2026-08-29 01:32:36 +02:00
|
|
|
|
* Zielversionen: AC1009 (R12, Default) und AC1015 (AutoCAD 2000).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Task F6: BLOCKS+INSERTS mit Name-Mapping/Sanitizing, ATTDEF in
|
|
|
|
|
|
* Blockdefinitionen, ATTRIB/SEQEND an Instanzen mit attrValues,
|
|
|
|
|
|
* R12-korrektes POLYLINE+VERTEX+SEQEND statt LWPOLYLINE sowie die
|
|
|
|
|
|
* Center-/Winkel-Konventionen der App (el.x/el.y = Zentrum, Grad-Winkel).
|
2026-06-26 10:50:24 +02:00
|
|
|
|
*/
|
|
|
|
|
|
import type { CADElement, CADLayer, ProjectData } from '../types/cad.types';
|
|
|
|
|
|
|
2026-08-29 01:32:36 +02:00
|
|
|
|
export interface DXFWriterOptions {
|
|
|
|
|
|
/** Zielversion: AC1009 (R12, Default) oder AC1015. */
|
|
|
|
|
|
version?: 'AC1009' | 'AC1015';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Pt { x: number; y: number }
|
|
|
|
|
|
type Write = (code: number, value: string | number) => void;
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
2026-08-29 01:32:36 +02:00
|
|
|
|
* Generate a DXF string from project data (R12-kompatibel).
|
2026-06-26 10:50:24 +02:00
|
|
|
|
*/
|
2026-08-29 01:32:36 +02:00
|
|
|
|
export function writeDXF(data: ProjectData, options: DXFWriterOptions = {}): string {
|
|
|
|
|
|
const version = options.version ?? 'AC1009';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const lines: string[] = [];
|
2026-08-29 01:32:36 +02:00
|
|
|
|
const w: Write = (code, value) => {
|
2026-06-26 10:50:24 +02:00
|
|
|
|
lines.push(String(code));
|
|
|
|
|
|
lines.push(String(value));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// ─── Name-Mapping: Block-ID → sanitierter DXF-Blockname ───
|
|
|
|
|
|
const usedNames = new Set<string>(['0']); // '0' ist in DXF reserviert
|
|
|
|
|
|
const nameMap = new Map<string, string>();
|
|
|
|
|
|
for (const block of data.blocks) {
|
|
|
|
|
|
nameMap.set(block.id, sanitizeBlockName(block.name || block.id, usedNames));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
// Header
|
|
|
|
|
|
w(0, 'SECTION');
|
|
|
|
|
|
w(2, 'HEADER');
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w(9, '$ACADVER'); w(1, version);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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');
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// ─── BLOCKS section (Task F6) ───
|
|
|
|
|
|
w(0, 'SECTION');
|
|
|
|
|
|
w(2, 'BLOCKS');
|
|
|
|
|
|
for (const block of data.blocks) {
|
|
|
|
|
|
const dxfName = nameMap.get(block.id)!;
|
|
|
|
|
|
w(0, 'BLOCK');
|
|
|
|
|
|
w(2, dxfName);
|
|
|
|
|
|
w(70, 0);
|
|
|
|
|
|
w(10, 0); w(20, 0); w(30, 0);
|
|
|
|
|
|
w(3, dxfName);
|
|
|
|
|
|
w(1, block.description || '');
|
|
|
|
|
|
for (const el of block.elements) {
|
|
|
|
|
|
writeEntity(w, el, getLayerName(data.layers, el.layerId), nameMap, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
w(0, 'ENDBLK');
|
|
|
|
|
|
}
|
|
|
|
|
|
w(0, 'ENDSEC');
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
// Entities section
|
|
|
|
|
|
w(0, 'SECTION');
|
|
|
|
|
|
w(2, 'ENTITIES');
|
|
|
|
|
|
for (const el of data.elements) {
|
|
|
|
|
|
const layerName = getLayerName(data.layers, el.layerId);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
writeEntity(w, el, layerName, nameMap, false);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
w(0, 'ENDSEC');
|
|
|
|
|
|
|
|
|
|
|
|
// EOF
|
|
|
|
|
|
w(0, 'EOF');
|
|
|
|
|
|
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 01:32:36 +02:00
|
|
|
|
/** Blocknamen für DXF sanitizen: nur [A-Za-z0-9_$-], Kollisionen deduplizieren. */
|
|
|
|
|
|
function sanitizeBlockName(name: string, used: Set<string>): string {
|
|
|
|
|
|
const base = (name.replace(/[^\w$-]/g, '_') || 'BLOCK');
|
|
|
|
|
|
let candidate = base;
|
|
|
|
|
|
let i = 2;
|
|
|
|
|
|
while (used.has(candidate.toUpperCase())) {
|
|
|
|
|
|
candidate = `${base}_${i++}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
used.add(candidate.toUpperCase());
|
|
|
|
|
|
return candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** R12-korrektes POLYLINE+VERTEX+SEQEND (LWPOLYLINE erst ab R13). */
|
|
|
|
|
|
function writePolylineR12(
|
|
|
|
|
|
w: Write,
|
|
|
|
|
|
layerName: string,
|
|
|
|
|
|
aci: number,
|
|
|
|
|
|
pts: Pt[],
|
|
|
|
|
|
closed: boolean,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
w(0, 'POLYLINE');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(62, aci);
|
|
|
|
|
|
w(66, 1); // Vertices folgen
|
|
|
|
|
|
w(70, closed ? 1 : 0);
|
|
|
|
|
|
w(10, 0); w(20, 0); w(30, 0);
|
|
|
|
|
|
for (const pt of pts) {
|
|
|
|
|
|
w(0, 'VERTEX');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(10, pt.x); w(20, pt.y); w(30, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
w(0, 'SEQEND');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** BBox-Fallback für Elemente ohne native DXF-Repräsentation. */
|
|
|
|
|
|
function writeBBoxPolyline(w: Write, el: CADElement, layerName: string, aci: number): void {
|
|
|
|
|
|
const x0 = el.x;
|
|
|
|
|
|
const y0 = el.y;
|
|
|
|
|
|
writePolylineR12(w, layerName, aci, [
|
|
|
|
|
|
{ x: x0, y: y0 },
|
|
|
|
|
|
{ x: x0 + el.width, y: y0 },
|
|
|
|
|
|
{ x: x0 + el.width, y: y0 + el.height },
|
|
|
|
|
|
{ x: x0, y: y0 + el.height },
|
|
|
|
|
|
], true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
function writeEntity(
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w: Write,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
el: CADElement,
|
|
|
|
|
|
layerName: string,
|
2026-08-29 01:32:36 +02:00
|
|
|
|
nameMap: Map<string, string>,
|
|
|
|
|
|
inBlockDefinition: boolean,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
): 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': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// App-Konvention: el.x/el.y IST das Zentrum (beide Renderer lesen es direkt)
|
2026-06-26 10:50:24 +02:00
|
|
|
|
w(0, 'CIRCLE');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(62, aci);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w(10, el.x); w(20, el.y); w(30, 0);
|
|
|
|
|
|
w(40, p.radius ?? el.width / 2);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'arc': {
|
|
|
|
|
|
w(0, 'ARC');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(62, aci);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w(10, el.x); w(20, el.y); w(30, 0);
|
|
|
|
|
|
w(40, p.radius ?? el.width / 2);
|
|
|
|
|
|
// App-Konvention: Winkel in Grad (0° = 3 Uhr, CCW) — direkt schreiben
|
|
|
|
|
|
w(50, p.startAngle ?? 0);
|
|
|
|
|
|
w(51, p.endAngle ?? 360);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'rect': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// App-Konvention: rect x/y = Zentrum der BBox
|
|
|
|
|
|
const x0 = el.x - el.width / 2;
|
|
|
|
|
|
const y0 = el.y - el.height / 2;
|
|
|
|
|
|
writePolylineR12(w, layerName, aci, [
|
|
|
|
|
|
{ x: x0, y: y0 },
|
|
|
|
|
|
{ x: x0 + el.width, y: y0 },
|
|
|
|
|
|
{ x: x0 + el.width, y: y0 + el.height },
|
|
|
|
|
|
{ x: x0, y: y0 + el.height },
|
|
|
|
|
|
], true);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'polygon':
|
|
|
|
|
|
case 'polyline': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
const pts = (p.points as Pt[] | undefined) ?? [];
|
|
|
|
|
|
if (pts.length >= 2) {
|
|
|
|
|
|
writePolylineR12(w, layerName, aci, pts.map((pt) => ({ x: pt.x, y: pt.y })), el.type === 'polygon');
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'text': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// ATTDEF: Text-Element als Attribut-Definition in Blockdefinitionen (Task F6)
|
|
|
|
|
|
if (inBlockDefinition && p.attdef === true && typeof p.tag === 'string' && p.tag) {
|
|
|
|
|
|
w(0, 'ATTDEF');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(10, el.x); w(20, el.y); w(30, 0);
|
|
|
|
|
|
w(40, p.fontSize ?? 12);
|
|
|
|
|
|
w(1, (p.text as string) ?? '');
|
|
|
|
|
|
w(2, p.tag);
|
|
|
|
|
|
w(3, (p.prompt as string) ?? '');
|
|
|
|
|
|
w(70, 0);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w(1, (p.text as string) ?? '');
|
2026-06-26 10:50:24 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'dimension': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
const pts = (p.points as Pt[] | undefined) ?? [];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
if (pts.length >= 2) {
|
|
|
|
|
|
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': {
|
2026-08-29 01:32:36 +02:00
|
|
|
|
const mapped = nameMap.get(String(p.blockId ?? ''));
|
|
|
|
|
|
if (!mapped) {
|
|
|
|
|
|
writeBBoxPolyline(w, el, layerName, aci);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
w(0, 'INSERT');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(62, aci);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
w(2, mapped);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
w(10, el.x); w(20, el.y); w(30, 0);
|
2026-08-29 01:32:36 +02:00
|
|
|
|
const sx = typeof p.scaleX === 'number' ? p.scaleX : (typeof p.scale === 'number' ? p.scale : 1);
|
|
|
|
|
|
const sy = typeof p.scaleY === 'number' ? p.scaleY : (typeof p.scale === 'number' ? p.scale : 1);
|
|
|
|
|
|
w(41, sx);
|
|
|
|
|
|
w(42, sy);
|
|
|
|
|
|
w(50, p.rotation ?? 0); // Grad, direkt (keine radToDeg-Doppelkonvertierung)
|
|
|
|
|
|
// Attributwerte der Instanz → ATTRIB/SEQEND (Task F6)
|
|
|
|
|
|
const attrValues = p.attrValues as Record<string, string> | undefined;
|
|
|
|
|
|
if (attrValues && Object.keys(attrValues).length > 0) {
|
|
|
|
|
|
w(66, 1); // Attribute folgen
|
|
|
|
|
|
for (const [tag, value] of Object.entries(attrValues)) {
|
|
|
|
|
|
w(0, 'ATTRIB');
|
|
|
|
|
|
w(8, layerName);
|
|
|
|
|
|
w(10, el.x); w(20, el.y); w(30, 0);
|
|
|
|
|
|
w(40, 12);
|
|
|
|
|
|
w(1, value);
|
|
|
|
|
|
w(2, tag);
|
|
|
|
|
|
w(70, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
w(0, 'SEQEND');
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
2026-08-29 01:32:36 +02:00
|
|
|
|
// chair, seating-row, table, stage, revcloud, leader, image, … → BBox
|
|
|
|
|
|
writeBBoxPolyline(w, el, layerName, aci);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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);
|
|
|
|
|
|
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
|
2026-08-29 01:32:36 +02:00
|
|
|
|
return 7;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function lineTypeToDXF(lt: string): string {
|
|
|
|
|
|
if (lt === 'dashed') return 'DASHED';
|
|
|
|
|
|
if (lt === 'dotted') return 'DOT';
|
|
|
|
|
|
return 'CONTINUOUS';
|
|
|
|
|
|
}
|