task(F5): DXF parser - ELLIPSE/SPLINE polyline approximation, MTEXT, POINT, ATTDEF, MINSERT arrays, structured skippedEntities report

This commit is contained in:
Agent Zero
2026-08-29 01:17:21 +02:00
parent a9c3ba786b
commit 6857694f64
2 changed files with 499 additions and 21 deletions
+269 -21
View File
@@ -1,27 +1,53 @@
/** /**
* DXF Parser converts DXF entities to CADElement[] * 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 DxfParser from 'dxf-parser';
import type { CADElement, CADLayer, CADProperties, ElementType } from '../types/cad.types'; import type { CADElement, CADLayer, CADProperties, ElementType } from '../types/cad.types';
export interface SkippedEntity {
type: string;
reason: string;
}
export interface DXFImportResult { export interface DXFImportResult {
elements: CADElement[]; elements: CADElement[];
layers: CADLayer[]; layers: CADLayer[];
warnings: string[]; warnings: string[];
skippedEntities: SkippedEntity[];
} }
let idCounter = 0; let idCounter = 0;
const nextId = () => `dxf-${Date.now()}-${idCounter++}`; 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. * Parse a DXF string into CAD elements and layers.
*/ */
export function parseDXF(dxfString: string): DXFImportResult { export function parseDXF(dxfString: string): DXFImportResult {
const parser = new DxfParser(); const parser = new DxfParser();
const dxf = parser.parseSync(dxfString) as any; 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 warnings: string[] = [];
const skippedEntities: SkippedEntity[] = [];
const layerMap = new Map<string, CADLayer>(); const layerMap = new Map<string, CADLayer>();
const elements: CADElement[] = []; 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) { if (dxf.entities) {
for (const entity of dxf.entities) { for (const entity of dxf.entities) {
if (entity?.type === 'INSERT') {
elements.push(...expandInsert(entity, layerMap));
continue;
}
const el = entityToCADElement(entity, layerMap); const el = entityToCADElement(entity, layerMap);
if (el) { if (el) {
elements.push(el); elements.push(el);
} else { } 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<string, CADLayer>): 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, entity: any,
layerMap: Map<string, CADLayer>, layerMap: Map<string, CADLayer>,
): CADElement | null { ): CADElement | null {
@@ -84,13 +268,10 @@ function entityToCADElement(
const s = entity.vertices?.[0]; const s = entity.vertices?.[0];
const e = entity.vertices?.[1]; const e = entity.vertices?.[1];
if (!s || !e) return null; 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 { return {
id: nextId(), type: 'line', layerId: layer.id, 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 }, 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 }, 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 'LWPOLYLINE':
case 'POLYLINE': { case 'POLYLINE': {
const pts = (entity.vertices || []).map((v: any) => ({ x: v.x, y: v.y })); const pts = (entity.vertices || []).map((v: any) => ({ x: v.x, y: v.y }));
if (pts.length < 2) return null; if (pts.length < 2) return null;
const minX = Math.min(...pts.map((p: any) => p.x)); const bbox = bboxOf(pts);
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 isClosed = entity.shape === true; const isClosed = entity.shape === true;
const type: ElementType = isClosed ? 'polygon' : 'polyline'; const type: ElementType = isClosed ? 'polygon' : 'polyline';
return { return {
id: nextId(), type, layerId: layer.id, id: nextId(), type, layerId: layer.id,
x: minX, y: minY, width: maxX - minX, height: maxY - minY, ...bbox,
properties: { ...baseProps, points: pts }, properties: { ...baseProps, points: pts },
}; };
} }
@@ -139,16 +341,62 @@ function entityToCADElement(
properties: { ...baseProps, text: entity.text || '', fontSize: entity.height || 12 }, properties: { ...baseProps, text: entity.text || '', fontSize: entity.height || 12 },
}; };
} }
case 'INSERT': { case 'MTEXT': {
// Block reference — store as block_instance
const pos = entity.position; const pos = entity.position;
if (!pos) return null; if (!pos) return null;
const text = cleanMText(String(entity.text ?? ''));
if (!text) return null;
return { return {
id: nextId(), type: 'block_instance', layerId: layer.id, id: nextId(), type: 'text', layerId: layer.id,
x: pos.x, y: pos.y, width: 0, height: 0, x: pos.x, y: pos.y, width: entity.width || 0, height: entity.height || 0,
properties: { ...baseProps, blockId: entity.name, scale: entity.scale || 1, rotation: entity.rotation || 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': { case 'DIMENSION': {
// Basic dimension support // Basic dimension support
const pts = entity.definitionPoint || entity.points; const pts = entity.definitionPoint || entity.points;
+230
View File
@@ -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');
});
});