Files
web-cad/frontend/tests/dxfWriterF6.test.ts
T

188 lines
7.1 KiB
TypeScript
Raw Normal View History

/**
* 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> = {}): 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> = {}): 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);
});
});