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

231 lines
8.6 KiB
TypeScript
Raw Normal View History

/**
* Task F5 DXF-Parser-Erweiterungstests.
*
* Neue Entity-Abdeckung: ELLIPSE (als Polyline-Approx), SPLINE (grad<=3 →
* Polyline via de-Boor), MTEXT (Formatcodes gecleanup), POINT, ATTDEF,
* INSERT mit MINSERT-Arrays und HATCH (forward-kompatibel_via Fake-Entity).
* Plus strukturierter Fehlerreport (skippedEntities) neben warnings.
*/
import { describe, it, expect } from 'vitest';
import { parseDXF, entityToCADElement, expandInsert } from '../src/services/dxfParser';
import type { CADLayer } from '../src/types/cad.types';
const layer0: CADLayer = {
id: 'dxf-layer-0', name: '0', visible: true, locked: false,
color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
};
const layers = new Map([['0', layer0]]);
function dxfOf(entityBody: string): string {
return `0\nSECTION\n2\nENTITIES\n${entityBody}0\nENDSEC\n0\nEOF\n`;
}
function ptsOf(el: { properties: { points?: Array<{ x: number; y: number }> } }): Array<{ x: number; y: number }> {
return el.properties.points ?? [];
}
describe('F5: ELLIPSE', () => {
it('volle Ellipse → geschlossene Polyline mit 64 Punkten und korrekter BBox', () => {
const el = entityToCADElement({
type: 'ELLIPSE', layer: '0',
center: { x: 0, y: 0 },
majorAxisEndPoint: { x: 100, y: 0 },
axisRatio: 0.5,
startAngle: 0, endAngle: Math.PI * 2,
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('polyline');
const pts = ptsOf(el!);
expect(pts.length).toBe(64);
const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
expect(Math.max(...xs)).toBeCloseTo(100, 0);
expect(Math.min(...xs)).toBeCloseTo(-100, 0);
expect(Math.max(...ys)).toBeCloseTo(50, 0);
expect(Math.min(...ys)).toBeCloseTo(-50, 0);
});
it('rotierte Ellipse respektiert majorAxisEndPoint-Winkel', () => {
const el = entityToCADElement({
type: 'ELLIPSE', layer: '0',
center: { x: 0, y: 0 },
majorAxisEndPoint: { x: 0, y: 100 }, // 90° rotiert
axisRatio: 0.5,
startAngle: 0, endAngle: Math.PI * 2,
} as any, layers);
const pts = ptsOf(el!);
expect(Math.max(...pts.map((p) => p.y))).toBeCloseTo(100, 0);
expect(Math.max(...pts.map((p) => p.x))).toBeCloseTo(50, 0);
});
it('partielle Ellipse → offene Polyline', () => {
const el = entityToCADElement({
type: 'ELLIPSE', layer: '0',
center: { x: 0, y: 0 },
majorAxisEndPoint: { x: 100, y: 0 },
axisRatio: 1,
startAngle: 0, endAngle: Math.PI,
} as any, layers);
const pts = ptsOf(el!);
expect(pts.length).toBe(32);
});
it('Integration über parseDXF mit echtem ELLIPSE-DXF-String', () => {
const res = parseDXF(dxfOf(
'0\nELLIPSE\n8\n0\n10\n0\n20\n0\n30\n0\n11\n100\n21\n0\n31\n0\n41\n0.5\n42\n0\n51\n6.283185307179586\n',
));
expect(res.skippedEntities).toHaveLength(0);
const el = res.elements.find((e) => e.type === 'polyline');
expect(el).toBeDefined();
expect(ptsOf(el as any).length).toBeGreaterThan(32);
});
});
describe('F5: SPLINE', () => {
const controlPoints = [
{ x: 0, y: 0 }, { x: 33, y: 200 }, { x: 66, y: -100 }, { x: 100, y: 0 },
];
it('Grad 3 → Polyline-Approx mit mehr Punkten als Kontrollpunkte', () => {
const el = entityToCADElement({
type: 'SPLINE', layer: '0',
degreeOfSplineCurve: 3, controlPoints, fitPoints: [],
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('polyline');
const pts = ptsOf(el!);
expect(pts.length).toBeGreaterThan(16);
// Approximation startet/endet bei exakten Kontrollpunkten
expect(pts[0].x).toBeCloseTo(0, 3);
expect(pts[0].y).toBeCloseTo(0, 3);
expect(pts[pts.length - 1].x).toBeCloseTo(100, 3);
});
it('Grad 4 → skipped mit Grund', () => {
const el = entityToCADElement({
type: 'SPLINE', layer: '0',
degreeOfSplineCurve: 4, controlPoints, fitPoints: [],
} as any, layers);
expect(el).toBeNull();
});
it('ohne Kontrollpunkte → skipped', () => {
const el = entityToCADElement({ type: 'SPLINE', layer: '0', degreeOfSplineCurve: 3 } as any, layers);
expect(el).toBeNull();
});
});
describe('F5: MTEXT', () => {
it('mappt auf text-Element, \\P → Zeilenumbruch, Formatcodes entfernt', () => {
const el = entityToCADElement({
type: 'MTEXT', layer: '0',
position: { x: 10, y: 20 },
height: 15, width: 200,
text: 'Zeile1\\PZeile2\\A1;\\H2x;Titel',
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('text');
expect(el!.properties.text).toBe('Zeile1\nZeile2Titel');
expect(el!.properties.fontSize).toBe(15);
});
});
describe('F5: POINT', () => {
it('mappt auf kleinen gefüllten Kreis mit point-Marker', () => {
const el = entityToCADElement({
type: 'POINT', layer: '0',
position: { x: 55, y: 66 },
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('circle');
expect(el!.x).toBeCloseTo(55 - 1.5, 5);
expect(el!.properties.radius).toBeCloseTo(1.5, 5);
expect(el!.properties.point).toBe(true);
});
});
describe('F5: ATTDEF', () => {
it('mappt auf text-Element mit tag=Wert und Metadaten', () => {
const el = entityToCADElement({
type: 'ATTDEF', layer: '0',
tag: 'NAME', text: 'Buehne', prompt: 'Name der Buehne',
startPoint: { x: 5, y: 5 }, textHeight: 12,
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('text');
expect(el!.properties.text).toBe('NAME=Buehne');
expect(el!.properties.fontSize).toBe(12);
expect(el!.properties.tag).toBe('NAME');
expect(el!.properties.prompt).toBe('Name der Buehne');
});
it('leerer ATTDEF-Text ohne Tag → skipped', () => {
const el = entityToCADElement({ type: 'ATTDEF', layer: '0', startPoint: { x: 0, y: 0 } } as any, layers);
expect(el).toBeNull();
});
});
describe('F5: INSERT (MINSERT-Arrays)', () => {
it('columnCount/rowCount > 1 erzeugt n-fache Instanzen mit Spacing-Offset', () => {
const els = [] as any[];
// Der Parser-Einstieg ist parseDXF; INSERT-Expansion passiert dort je Entity — hier via Helper
const base = {
type: 'INSERT', layer: '0', name: 'stuhl',
position: { x: 0, y: 0 }, xScale: 2, yScale: 3, rotation: 0,
columnCount: 3, rowCount: 2, columnSpacing: 100, rowSpacing: 50,
} as any;
const out = expandInsert(base);
expect(out).toHaveLength(6);
const uniqXs = [...new Set(out.map((e) => e.x))].sort((a, b) => a - b);
expect(uniqXs).toEqual([0, 100, 200]);
const ys = [...new Set(out.map((e) => e.y))].sort((a, b) => a - b);
expect(ys).toEqual([0, 50]);
expect(out.every((e) => e.properties.scaleX === 2 && e.properties.scaleY === 3)).toBe(true);
});
it('einfacher INSERT bleibt einzelne Instanz mit kombiniertem scale', () => {
const out = expandInsert({
type: 'INSERT', layer: '0', name: 'b',
position: { x: 5, y: 5 }, xScale: 1, yScale: 1, rotation: 90,
} as any);
expect(out).toHaveLength(1);
expect(out[0].properties.blockId).toBe('b');
expect(out[0].properties.rotation).toBe(90);
});
});
describe('F5: HATCH (forward-kompatibel_via Entity-Injektion)', () => {
it('HATCH solid → polygon mit hatchPattern', () => {
const el = entityToCADElement({
type: 'HATCH', layer: '0',
patternName: 'SOLID', isSolid: true,
boundaryPoints: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }, { x: 0, y: 100 }],
} as any, layers);
expect(el).not.toBeNull();
expect(el!.type).toBe('polygon');
expect(el!.properties.hatchPattern).toBe('SOLID');
expect(ptsOf(el as any)).toHaveLength(4);
});
});
describe('F5: Fehlerreport (skippedEntities)', () => {
// Hinweis: dxf-parser 1.1.x verschluckt voellig unbekannte Typen (FOOBAR) schlicht;
// valide SPLINE-Entities mit Grad > 3 kommen dagegen durch und werden vom
// Mapper mit Grund uebersprungen — genau dafuer ist der Report da.
const DEG4_SPLINE = '0\nSPLINE\n8\n0\n71\n4\n11\n0\n21\n0\n11\n10\n21\n0\n11\n20\n21\n0\n11\n30\n21\n0\n';
it('unbrauchbare Entities (SPLINE Grad 4) landen im Report', () => {
const res = parseDXF(dxfOf(DEG4_SPLINE));
const types = res.skippedEntities.map((s) => s.type);
expect(types).toContain('SPLINE');
expect(res.warnings.length).toBeGreaterThan(0);
expect(res.skippedEntities[0].reason).toContain('> 3');
});
it('Result enthält skippedEntities-Feld mit type+reason (Design-Vertrag)', () => {
const res = parseDXF(dxfOf(DEG4_SPLINE));
expect(res.skippedEntities).toHaveLength(1);
expect(res.skippedEntities[0].type).toBe('SPLINE');
expect(typeof res.skippedEntities[0].reason).toBe('string');
});
});