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

231 lines
8.6 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.
/**
* 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');
});
});