/** * Task F6 – DXF-Writer-Erweiterungstests. * * Deckt ab: BLOCKS+INSERTS mit Name-Mapping/Sanitizing, ATTRIB/SEQEND, * Versionsoption AC1009/AC1015, R12-korrektes POLYLINE statt LWPOLYLINE, * Center-Konvention (circle/arc el.x/el.y = Zentrum) und Grad-Winkel ohne * Doppelkonvertierung — abgesichert über echte write→parse-Roundtrips. */ import { describe, it, expect } from 'vitest'; import { writeDXF } from '../src/services/dxfWriter'; import { parseDXF } from '../src/services/dxfParser'; import type { CADElement, CADLayer, ProjectData, BlockDefinition } from '../src/types/cad.types'; const layer: CADLayer = { id: 'layer-1', name: 'L1', visible: true, locked: false, color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null, }; function mkEl(over: Partial = {}): CADElement { return { id: 'e1', type: 'line', layerId: 'layer-1', x: 0, y: 0, width: 0, height: 0, properties: {}, ...over, } as CADElement; } function mkBlock(over: Partial = {}): BlockDefinition { return { id: 'blk-1', name: 'Stuhl Reihe', description: '', category: 'event', elements: [mkEl({ type: 'line', properties: { x1: 0, y1: 0, x2: 100, y2: 0 } })], ...over, }; } function mkProj(elements: CADElement[], blocks: BlockDefinition[] = []): ProjectData { return { version: '1', name: 'T', layers: [layer], elements, blocks } as ProjectData; } describe('F6: Version', () => { it('Default ist AC1009 (R12)', () => { expect(writeDXF(mkProj([]))).toContain('AC1009'); }); it('Option AC1015 setzt Header-Version', () => { expect(writeDXF(mkProj([]), { version: 'AC1015' })).toContain('AC1015'); }); }); describe('F6: BLOCKS + INSERTS', () => { it('BLOCKS-Sektion emittiert sanitisierten Blocknamen, INSERT referenziert gemappten Namen', () => { const out = writeDXF(mkProj([ mkEl({ type: 'block_instance', properties: { blockId: 'blk-1' } }), ], [mkBlock()])); expect(out).toContain('BLOCKS'); expect(out).toContain('BLOCK'); expect(out).toContain('ENDBLK'); // Name 'Stuhl Reihe' → 'Stuhl_Reihe' (Leerzeichen sanitizing) expect(out.split('Stuhl_Reihe').length).toBeGreaterThanOrEqual(3); // BLOCK 2/3 + INSERT 2 expect(out).toContain('INSERT'); }); it('block_instance ohne Definition bleibt BBox-Fallback (kein INSERT)', () => { const out = writeDXF(mkProj([ mkEl({ type: 'block_instance', x: 0, y: 0, width: 100, height: 50, properties: { blockId: 'unknown-block' } }), ])); expect(out).not.toContain('INSERT'); expect(out).toContain('POLYLINE'); }); it('Roundtrip: INSERT landet als block_instance mit gemapptem Namen', () => { const out = writeDXF(mkProj([ mkEl({ type: 'block_instance', x: 10, y: 20, properties: { blockId: 'blk-1' } }), ], [mkBlock()])); const res = parseDXF(out); const inst = res.elements.find((e) => e.type === 'block_instance'); expect(inst).toBeDefined(); expect(inst!.properties.blockId).toBe('Stuhl_Reihe'); expect(inst!.x).toBe(10); expect(inst!.y).toBe(20); }); it('ATTDEF aus Blockelement (attdef:true + tag) wird in der BLOCK-Definition emittiert', () => { const blk = mkBlock({ elements: [ mkEl({ type: 'line', properties: { x1: 0, y1: 0, x2: 50, y2: 0 } }), mkEl({ type: 'text', x: 5, y: 5, properties: { text: 'Buehne', tag: 'NAME', attdef: true, fontSize: 12 } }), ], }); const out = writeDXF(mkProj([], [blk])); expect(out).toContain('ATTDEF'); expect(out).toContain('NAME'); }); it('Instanz mit attrValues emittiert 66/ATTRIB/SEQEND', () => { const out = writeDXF(mkProj([ mkEl({ type: 'block_instance', x: 0, y: 0, properties: { blockId: 'blk-1', attrValues: { NAME: 'Halle A' } }, }), ], [mkBlock()])); expect(out).toContain('ATTRIB'); expect(out).toContain('SEQEND'); expect(out).toContain('Halle A'); expect(out).toContain('66'); }); }); describe('F6: R12-korrektes POLYLINE', () => { it('polygon → POLYLINE+VERTEX+SEQEND, kein LWPOLYLINE', () => { const out = writeDXF(mkProj([ mkEl({ type: 'polygon', properties: { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }] }, }), ])); expect(out).toContain('POLYLINE'); expect(out).toContain('VERTEX'); expect(out).toContain('SEQEND'); expect(out).not.toContain('LWPOLYLINE'); }); it('Roundtrip: polygon bleibt geschlossen (shape-Flag 70=1)', () => { const out = writeDXF(mkProj([ mkEl({ type: 'polygon', properties: { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }, { x: 0, y: 100 }] }, }), ])); const res = parseDXF(out); const poly = res.elements.find((e) => e.type === 'polygon'); expect(poly).toBeDefined(); expect(res.elements.find((e) => e.type === 'polyline')).toBeUndefined(); }); }); describe('F6: Center- & Winkel-Konvention', () => { it('Writer: circle Zentrum el.x/el.y direkt (kein +width/2)', () => { const out = writeDXF(mkProj([ mkEl({ type: 'circle', x: 100, y: 50, width: 60, height: 60, properties: { radius: 30 } }), ])); const res = parseDXF(out); const circ = res.elements.find((e) => e.type === 'circle'); expect(circ).toBeDefined(); expect(circ!.x).toBeCloseTo(100, 5); expect(circ!.y).toBeCloseTo(50, 5); expect(circ!.properties.radius).toBeCloseTo(30, 5); }); it('Parser: CIRCLE center landet direkt auf el.x/el.y (Renderer-Konvention)', () => { const dxf = [ '0', 'SECTION', '2', 'ENTITIES', '0', 'CIRCLE', '8', '0', '10', '10', '20', '20', '30', '0', '40', '5', '0', 'ENDSEC', '0', 'EOF', ].join(String.fromCharCode(10)); const res = parseDXF(dxf); const circ = res.elements.find((e) => e.type === 'circle'); expect(circ).toBeDefined(); expect(circ!.x).toBe(10); expect(circ!.y).toBe(20); expect(circ!.properties.radius).toBe(5); expect(circ!.width).toBe(10); }); it('arc/rotation Winkel in Grad ohne Doppelkonvertierung', () => { const out = writeDXF(mkProj([ mkEl({ type: 'arc', x: 0, y: 0, width: 20, height: 20, properties: { radius: 10, startAngle: 30, endAngle: 120 } }), mkEl({ type: 'block_instance', x: 0, y: 0, properties: { blockId: 'blk-1', rotation: 45 } }), ], [mkBlock()])); expect(out).toContain('50' + String.fromCharCode(10) + '30'); expect(out).toContain('51' + String.fromCharCode(10) + '120'); expect(out).toContain('50' + String.fromCharCode(10) + '45'); const res = parseDXF(out); const arc = res.elements.find((e) => e.type === 'arc'); expect(arc!.properties.startAngle).toBeCloseTo(30, 5); }); it('rect als geschlossene POLYLINE mit Ecken aus Zentrumskonvention', () => { const out = writeDXF(mkProj([ mkEl({ type: 'rect', x: 0, y: 0, width: 100, height: 50, properties: {} }), ])); const res = parseDXF(out); const poly = res.elements[0]; const pts = (poly!.properties.points ?? []) as Array<{ x: number; y: number }>; expect(pts.length).toBe(4); expect(pts[0].x).toBeCloseTo(-50, 5); expect(pts[0].y).toBeCloseTo(-25, 5); }); });