/** * Task F8 – SVG-Import als Block: Geometrie-Konvertierung. * * Path-Parser (M/L/H/V/C/S/Q/T/A/Z, relative + absolute), Bézier-Flattening, * Arc→Polyline, Konventions-Fixes (rect=Zentrum, circle=cx/cy direkt), * ellipse→Polyline statt verstümmeltem Kreis, sowie Block-Payload-Erzeugung * (SVG → Element-Array + Thumbnail) für die globale Bibliothek. */ import { describe, it, expect } from 'vitest'; import { importSVG, importSVGAsBlockPayload } from '../src/services/importService'; describe('F8: SVG path → Polyline-Approximation', () => { it('M + L + Z erzeugt geschlossene Polyline mit exakten Punkten', () => { const res = importSVG(''); expect(res.success).toBe(true); const poly = res.elements.find((e) => e.type === 'polygon'); expect(poly).toBeDefined(); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts.length).toBeGreaterThanOrEqual(3); expect(pts[0]).toEqual({ x: 0, y: 0 }); expect(pts[1]).toEqual({ x: 100, y: 0 }); expect(pts[2]).toEqual({ x: 100, y: 100 }); }); it('kleine Befehle sind relativ (m/l): Startpunkt wirkt', () => { const res = importSVG(''); const poly = res.elements.find((e) => e.type === 'polygon'); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts[0]).toEqual({ x: 10, y: 10 }); expect(pts[1]).toEqual({ x: 30, y: 10 }); expect(pts[2]).toEqual({ x: 30, y: 30 }); }); it('kubischer Bézier (C) wird geflattet: Kurvenpunkte + Endpunkt exakt', () => { const res = importSVG(''); const poly = res.elements.find((e) => e.type === 'polyline'); expect(poly).toBeDefined(); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts.length).toBeGreaterThan(10); // geflattet, nicht nur Endpunkte expect(pts[0]).toEqual({ x: 0, y: 0 }); expect(pts[pts.length - 1]).toEqual({ x: 100, y: 100 }); // Mittelpunkt der Kubik-Bézier bei t=0.5: exakt berechenbar expect(pts[Math.floor(pts.length / 2)].y).toBeGreaterThan(20); }); it('H/V-Befehle (horizontal/vertical) funktionieren', () => { const res = importSVG(''); const poly = res.elements.find((e) => e.type === 'polygon'); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts[0]).toEqual({ x: 0, y: 0 }); expect(pts[1]).toEqual({ x: 50, y: 0 }); expect(pts[2]).toEqual({ x: 50, y: 30 }); }); it('Arc (A) wird approximiert: Halbkreis über (100,0) mit korrektem Bogenverlauf', () => { const res = importSVG(''); const poly = res.elements.find((e) => e.type === 'polyline'); expect(poly).toBeDefined(); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts.length).toBeGreaterThan(8); expect(pts[0]).toEqual({ x: 0, y: 0 }); expect(pts[pts.length - 1]).toEqual({ x: 100, y: 0 }); // Scheitel des Halbkreises: (50, -50) bei sweep=1 (y-up im Bogen oben) const maxY = Math.max(...pts.map((p) => p.y)); const minY = Math.min(...pts.map((p) => p.y)); expect(minY).toBeLessThan(-40); // Bogen wölbt sich nach oben (negativ) expect(maxY).toBeCloseTo(0, 5); }); it('Mehrfach-Subpaths ergeben mehrere Elemente', () => { const res = importSVG(''); const polys = res.elements.filter((e) => e.type === 'polyline'); expect(polys.length).toBe(2); }); }); describe('F8: Konventions-Fixes', () => { it('rect: x/y = Zentrum (App-Konvention, nicht Ecke)', () => { const res = importSVG(''); const rect = res.elements.find((e) => e.type === 'rect'); expect(rect).toBeDefined(); expect(rect!.x).toBe(50); expect(rect!.y).toBe(25); expect(rect!.width).toBe(100); }); it('circle: el.x/el.y = Zentrum direkt (Renderer-Konvention)', () => { const res = importSVG(''); const circ = res.elements.find((e) => e.type === 'circle'); expect(circ).toBeDefined(); expect(circ!.x).toBe(30); expect(circ!.y).toBe(40); expect(circ!.properties.radius).toBe(10); }); it('ellipse → geschlossenes Polygon mit rx/ry korrekt (nicht verstümmelter Kreis)', () => { const res = importSVG(''); const poly = res.elements.find((e) => e.type === 'polygon'); expect(poly).toBeDefined(); const pts = poly!.properties.points as Array<{ x: number; y: number }>; expect(pts.length).toBeGreaterThanOrEqual(32); expect(Math.max(...pts.map((p) => p.x))).toBeCloseTo(150, 5); expect(Math.min(...pts.map((p) => p.x))).toBeCloseTo(-50, 5); expect(Math.max(...pts.map((p) => p.y))).toBeCloseTo(90, 5); expect(Math.min(...pts.map((p) => p.y))).toBeCloseTo(10, 5); }); }); describe('F8: SVG als Block-Payload', () => { it('importSVGAsBlockPayload liefert Element-Array als JSON-String + svg als Thumbnail', () => { const payload = importSVGAsBlockPayload(''); expect(payload.blockData).toBeTruthy(); const els = JSON.parse(payload.blockData); expect(Array.isArray(els)).toBe(true); expect(els.length).toBe(1); expect(els[0].type).toBe('rect'); expect(payload.svg).toContain(' { const payload = importSVGAsBlockPayload(''); expect(payload.blockData).toBeNull(); expect(payload.svg).toContain('