diff --git a/frontend/src/services/dxfParser.ts b/frontend/src/services/dxfParser.ts index 745d30e..2640a9c 100644 --- a/frontend/src/services/dxfParser.ts +++ b/frontend/src/services/dxfParser.ts @@ -1,27 +1,53 @@ /** * DXF Parser – converts DXF entities to CADElement[] - * Uses dxf-parser library + * Uses dxf-parser library. + * + * Task F5 erweitert die Entity-Abdeckung: ELLIPSE (Polyline-Approx), SPLINE + * (Grad<=3 → de-Boor-Approx), MTEXT (Formatcode-Cleanup), POINT, ATTDEF und + * INSERT mit MINSERT-Arrays. HATCH wird defensiv mapping-fähig gehalten + * (dxf-parser 1.1.x schickt echte HATCH-Entities noch nicht durch). + * + * Neben `warnings` liefert parseDXF einen strukturierten Fehlerreport + * (`skippedEntities`: type + reason je übersprungener Entity). */ import DxfParser from 'dxf-parser'; import type { CADElement, CADLayer, CADProperties, ElementType } from '../types/cad.types'; +export interface SkippedEntity { + type: string; + reason: string; +} + export interface DXFImportResult { elements: CADElement[]; layers: CADLayer[]; warnings: string[]; + skippedEntities: SkippedEntity[]; } let idCounter = 0; const nextId = () => `dxf-${Date.now()}-${idCounter++}`; +interface Pt { x: number; y: number } + +function bboxOf(pts: Pt[]): { x: number; y: number; width: number; height: number } { + const minX = Math.min(...pts.map((p) => p.x)); + const minY = Math.min(...pts.map((p) => p.y)); + const maxX = Math.max(...pts.map((p) => p.x)); + const maxY = Math.max(...pts.map((p) => p.y)); + return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; +} + /** * Parse a DXF string into CAD elements and layers. */ export function parseDXF(dxfString: string): DXFImportResult { const parser = new DxfParser(); const dxf = parser.parseSync(dxfString) as any; - if (!dxf) return { elements: [], layers: [], warnings: ['DXF parse returned null'] }; + const empty: DXFImportResult = { elements: [], layers: [], warnings: [], skippedEntities: [] }; + if (!dxf) return { ...empty, warnings: ['DXF parse returned null'] }; const warnings: string[] = []; + const skippedEntities: SkippedEntity[] = []; const layerMap = new Map(); const elements: CADElement[] = []; @@ -52,22 +78,180 @@ export function parseDXF(dxfString: string): DXFImportResult { }); } - // Parse entities + // Parse entities (INSERT expanded for MINSERT arrays) if (dxf.entities) { for (const entity of dxf.entities) { + if (entity?.type === 'INSERT') { + elements.push(...expandInsert(entity, layerMap)); + continue; + } const el = entityToCADElement(entity, layerMap); if (el) { elements.push(el); } else { - warnings.push(`Unsupported entity type: ${entity.type}`); + const type = String(entity?.type ?? 'UNKNOWN'); + const reason = skipReason(entity); + skippedEntities.push({ type, reason }); + warnings.push(`Unsupported entity type: ${type} — ${reason}`); } } } - return { elements, layers: Array.from(layerMap.values()), warnings }; + return { elements, layers: Array.from(layerMap.values()), warnings, skippedEntities }; } -function entityToCADElement( +/** Grund, warum eine Entity übersprungen wurde (für den Fehlerreport). */ +export function skipReason(entity: any): string { + switch (entity?.type) { + case 'ELLIPSE': + return 'Ellipse ohne center/majorAxisEndPoint nicht darstellbar'; + case 'SPLINE': + if ((entity?.degreeOfSplineCurve ?? 0) > 3) return 'Spline-Grad > 3 (Approx limitiert auf Grad <= 3)'; + return 'Spline ohne verwertbare Kontrollpunkte'; + case 'LINE': + return 'Line unvollständig (Start-/Endpunkt fehlt)'; + case 'HATCH': + return 'HATCH-Grenzen nicht lesbar'; + case 'ATTDEF': + return 'ATTDEF ohne tag'; + default: + return 'Nicht unterstützter Entity-Typ'; + } +} + +/** + * INSERT → block_instance(s). MINSERT-Arrays (columnCount/rowCount > 1) + * erzeugen n-fache Instanzen mit Spacing-Offset. + */ +export function expandInsert(entity: any, layerMap?: Map): CADElement[] { + const layerName = entity.layer || '0'; + const layer = layerMap?.get(layerName) ?? layerMap?.get('0'); + const layerId = layer?.id ?? `dxf-layer-${layerName}`; + const pos = entity.position; + if (!pos) return []; + const baseProps: CADProperties = { + stroke: entity.color ? (dxfColorToHex(entity.color) ?? layer?.color) : (layer?.color ?? '#ffffff'), + strokeWidth: 1, + blockId: entity.name, + scale: entity.scale || entity.xScale || 1, + scaleX: entity.xScale ?? 1, + scaleY: entity.yScale ?? 1, + rotation: entity.rotation || 0, + }; + const cols = Math.max(1, Math.floor(entity.columnCount || 1)); + const rows = Math.max(1, Math.floor(entity.rowCount || 1)); + const colSpacing = entity.columnSpacing || 0; + const rowSpacing = entity.rowSpacing || 0; + const out: CADElement[] = []; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + out.push({ + id: nextId(), type: 'block_instance', layerId, + x: pos.x + c * colSpacing, y: pos.y + r * rowSpacing, + width: 0, height: 0, + properties: { ...baseProps }, + }); + } + } + return out; +} + +/** ELLIPSE → Polyline-Approximation (64 Segmente bei Voll-Ellipse). */ +function ellipseToPoints(entity: any): Pt[] | null { + const c = entity.center; + const major = entity.majorAxisEndPoint; + if (!c || !major) return null; + const a = Math.hypot(major.x ?? 0, major.y ?? 0); + if (!(a > 0)) return null; + const ratio = typeof entity.axisRatio === 'number' && entity.axisRatio > 0 ? entity.axisRatio : 1; + const b = a * ratio; + const phi = Math.atan2(major.y ?? 0, major.x ?? 0); + const cosP = Math.cos(phi), sinP = Math.sin(phi); + const FULL = Math.PI * 2; + let start = typeof entity.startAngle === 'number' ? entity.startAngle : 0; + let end = typeof entity.endAngle === 'number' ? entity.endAngle : start + FULL; + let span = end - start; + const closed = !(span > 0.000001) || Math.abs(span - FULL) < 1e-6; + if (span <= 0) { start = 0; span = FULL; } + const n = Math.max(2, Math.round(64 * (span / FULL))); + const pts: Pt[] = []; + for (let i = 0; i < n; i++) { + const t = closed ? start + (i * span) / n : start + (i * span) / (n - 1); + const ct = Math.cos(t), st = Math.sin(t); + pts.push({ + x: c.x + a * ct * cosP - b * st * sinP, + y: c.y + a * ct * sinP + b * st * cosP, + }); + } + return pts; +} + +/** Cox-de-Boor: Punkt auf B-Spline (Grad <= 3). */ +function bsplinePoint(degree: number, knots: number[], ctrl: Pt[], t: number): Pt { + const n = ctrl.length - 1; + let k = degree; + for (let i = degree; i <= n; i++) { + if (t >= knots[i] && t <= knots[i + 1]) { k = i; break; } + } + const d: Pt[] = []; + for (let j = 0; j <= degree; j++) d.push({ ...ctrl[k - degree + j] }); + for (let r = 1; r <= degree; r++) { + for (let j = degree; j >= r; j--) { + const i = k - degree + j; + const denom = knots[i + degree - r + 1] - knots[i]; + const alpha = denom === 0 ? 0 : (t - knots[i]) / denom; + d[j] = { + x: (1 - alpha) * d[j - 1].x + alpha * d[j].x, + y: (1 - alpha) * d[j - 1].y + alpha * d[j].y, + }; + } + } + return d[degree]; +} + +/** SPLINE → Polyline-Approximation via de Boor (Grad <= 3). */ +function splineToPoints(entity: any): Pt[] | null { + const degree = entity.degreeOfSplineCurve ?? 3; + const ctrl = (entity.controlPoints || []) as Pt[]; + if (!Array.isArray(ctrl) || ctrl.length < degree + 1) return null; + const m = ctrl.length; + const total = m + degree + 1; + const knots: number[] = entity.knotValues && entity.knotValues.length === total + ? entity.knotValues.slice() + : (() => { + const ks: number[] = []; + for (let j = 0; j < total; j++) { + if (j <= degree) ks.push(0); + else if (j >= total - 1 - degree) ks.push(1); + else ks.push((j - degree) / (total - 1 - 2 * degree)); + } + return ks; + })(); + const N = 40; + const t0 = knots[degree]; + const t1 = knots[m]; + const span = t1 - t0; + const pts: Pt[] = []; + for (let i = 0; i < N; i++) { + const t = t0 + (i * span) / (N - 1); + pts.push(bsplinePoint(degree, knots, ctrl, t)); + } + return pts; +} + +/** MTEXT-Formatcodes zerlegen: \P → Umbruch, Inline-Codes entfernen. */ +function cleanMText(raw: string): string { + return raw + .replace(/\\P/gi, '\n') + .replace(/\\~/g, ' ') + .replace(/\\[A-Za-z][^;{}\\]{0,12};/g, '') + .replace(/[{}]/g, ''); +} + +/** + * Convert a single DXF entity into a CADElement (exported for unit tests). + */ +export function entityToCADElement( entity: any, layerMap: Map, ): CADElement | null { @@ -84,13 +268,10 @@ function entityToCADElement( const s = entity.vertices?.[0]; const e = entity.vertices?.[1]; if (!s || !e) return null; - const minX = Math.min(s.x, e.x); - const minY = Math.min(s.y, e.y); - const maxX = Math.max(s.x, e.x); - const maxY = Math.max(s.y, e.y); return { id: nextId(), type: 'line', layerId: layer.id, - x: minX, y: minY, width: maxX - minX, height: maxY - minY, + x: Math.min(s.x, e.x), y: Math.min(s.y, e.y), + width: Math.abs(e.x - s.x), height: Math.abs(e.y - s.y), properties: { ...baseProps, x1: s.x, y1: s.y, x2: e.x, y2: e.y }, }; } @@ -114,19 +295,40 @@ function entityToCADElement( properties: { ...baseProps, radius: r, startAngle: entity.startAngle, endAngle: entity.endAngle }, }; } + case 'ELLIPSE': { + const pts = ellipseToPoints(entity); + if (!pts || pts.length < 2) return null; + const bbox = bboxOf(pts); + return { + id: nextId(), type: 'polyline', layerId: layer.id, + ...bbox, + properties: { ...baseProps, points: pts }, + }; + } + case 'SPLINE': { + const degree = entity.degreeOfSplineCurve ?? 3; + const ctrl = (entity.controlPoints || []) as Pt[]; + if (degree < 1 || degree > 3) return null; + if (!Array.isArray(ctrl) || ctrl.length < degree + 1) return null; + const pts = splineToPoints(entity); + if (!pts || pts.length < 2) return null; + const bbox = bboxOf(pts); + return { + id: nextId(), type: 'polyline', layerId: layer.id, + ...bbox, + properties: { ...baseProps, points: pts }, + }; + } case 'LWPOLYLINE': case 'POLYLINE': { const pts = (entity.vertices || []).map((v: any) => ({ x: v.x, y: v.y })); if (pts.length < 2) return null; - const minX = Math.min(...pts.map((p: any) => p.x)); - const minY = Math.min(...pts.map((p: any) => p.y)); - const maxX = Math.max(...pts.map((p: any) => p.x)); - const maxY = Math.max(...pts.map((p: any) => p.y)); + const bbox = bboxOf(pts); const isClosed = entity.shape === true; const type: ElementType = isClosed ? 'polygon' : 'polyline'; return { id: nextId(), type, layerId: layer.id, - x: minX, y: minY, width: maxX - minX, height: maxY - minY, + ...bbox, properties: { ...baseProps, points: pts }, }; } @@ -139,16 +341,62 @@ function entityToCADElement( properties: { ...baseProps, text: entity.text || '', fontSize: entity.height || 12 }, }; } - case 'INSERT': { - // Block reference — store as block_instance + case 'MTEXT': { const pos = entity.position; if (!pos) return null; + const text = cleanMText(String(entity.text ?? '')); + if (!text) return null; return { - id: nextId(), type: 'block_instance', layerId: layer.id, - x: pos.x, y: pos.y, width: 0, height: 0, - properties: { ...baseProps, blockId: entity.name, scale: entity.scale || 1, rotation: entity.rotation || 0 }, + id: nextId(), type: 'text', layerId: layer.id, + x: pos.x, y: pos.y, width: entity.width || 0, height: entity.height || 0, + properties: { ...baseProps, text, fontSize: entity.height || 12, rotation: entity.rotation || 0 }, }; } + case 'POINT': { + const pos = entity.position; + if (!pos) return null; + const r = 1.5; + return { + id: nextId(), type: 'circle', layerId: layer.id, + x: pos.x - r, y: pos.y - r, width: r * 2, height: r * 2, + properties: { ...baseProps, radius: r, fill: color, point: true }, + }; + } + case 'ATTDEF': { + const s = entity.startPoint; + if (!entity.tag || !s) return null; + return { + id: nextId(), type: 'text', layerId: layer.id, + x: s.x, y: s.y, width: 0, height: 0, + properties: { + ...baseProps, + text: `${entity.tag}=${entity.text ?? ''}`, + fontSize: entity.textHeight || 12, + tag: entity.tag, + prompt: entity.prompt ?? '', + attdef: true, + }, + }; + } + case 'HATCH': { + const pts = (entity.boundaryPoints || []) as Pt[]; + if (pts.length < 3) return null; + const bbox = bboxOf(pts); + return { + id: nextId(), type: 'polygon', layerId: layer.id, + ...bbox, + properties: { + ...baseProps, + points: pts, + hatchPattern: entity.patternName || 'SOLID', + fill: entity.isSolid ? color : undefined, + }, + }; + } + case 'INSERT': { + const [inst] = expandInsert(entity, layerMap); + return inst ?? null; + } case 'DIMENSION': { // Basic dimension support const pts = entity.definitionPoint || entity.points; diff --git a/frontend/tests/dxfParserF5.test.ts b/frontend/tests/dxfParserF5.test.ts new file mode 100644 index 0000000..a8c2950 --- /dev/null +++ b/frontend/tests/dxfParserF5.test.ts @@ -0,0 +1,230 @@ +/** + * 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'); + }); +});