188 lines
7.1 KiB
TypeScript
188 lines
7.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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);
|
|||
|
|
});
|
|||
|
|
});
|