Files
web-cad/frontend/src/services/dxfWriter.ts
T

308 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DXF Writer converts CADElement[] to DXF string
* 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).
*/
import type { CADElement, CADLayer, ProjectData } from '../types/cad.types';
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;
/**
* Generate a DXF string from project data (R12-kompatibel).
*/
export function writeDXF(data: ProjectData, options: DXFWriterOptions = {}): string {
const version = options.version ?? 'AC1009';
const lines: string[] = [];
const w: Write = (code, value) => {
lines.push(String(code));
lines.push(String(value));
};
// ─── 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));
}
// Header
w(0, 'SECTION');
w(2, 'HEADER');
w(9, '$ACADVER'); w(1, version);
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');
// ─── 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');
// 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, nameMap, false);
}
w(0, 'ENDSEC');
// EOF
w(0, 'EOF');
return lines.join('\n');
}
/** 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);
}
function writeEntity(
w: Write,
el: CADElement,
layerName: string,
nameMap: Map<string, string>,
inBlockDefinition: boolean,
): 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': {
// App-Konvention: el.x/el.y IST das Zentrum (beide Renderer lesen es direkt)
w(0, 'CIRCLE');
w(8, layerName);
w(62, aci);
w(10, el.x); w(20, el.y); w(30, 0);
w(40, p.radius ?? el.width / 2);
break;
}
case 'arc': {
w(0, 'ARC');
w(8, layerName);
w(62, aci);
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);
break;
}
case 'rect': {
// 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);
break;
}
case 'polygon':
case 'polyline': {
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');
}
break;
}
case 'text': {
// 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;
}
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 as string) ?? '');
break;
}
case 'dimension': {
const pts = (p.points as Pt[] | undefined) ?? [];
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': {
const mapped = nameMap.get(String(p.blockId ?? ''));
if (!mapped) {
writeBBoxPolyline(w, el, layerName, aci);
break;
}
w(0, 'INSERT');
w(8, layerName);
w(62, aci);
w(2, mapped);
w(10, el.x); w(20, el.y); w(30, 0);
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');
}
break;
}
default:
// chair, seating-row, table, stage, revcloud, leader, image, … → BBox
writeBBoxPolyline(w, el, layerName, aci);
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
return 7;
}
function lineTypeToDXF(lt: string): string {
if (lt === 'dashed') return 'DASHED';
if (lt === 'dotted') return 'DOT';
return 'CONTINUOUS';
}