Files

126 lines
6.1 KiB
TypeScript
Raw Permalink 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 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('<svg xmlns="http://www.w3.org/2000/svg"><path d="M 0 0 L 100 0 L 100 100 Z" stroke="#fff" fill="none"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><path d="m 10 10 l 20 0 l 0 20 z"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><path d="M 0 0 C 50 100 100 0 100 100"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><path d="M 0 0 H 50 V 30 L 10 30 Z"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><path d="M 0 0 A 50 50 0 0 1 100 0"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><path d="M 0 0 L 10 0 M 100 100 L 110 100"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="100" height="50"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="30" cy="40" r="10"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="50" rx="100" ry="40"/></svg>');
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('<svg xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="80" height="40"/></svg>');
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('<svg');
});
it('leeres SVG ohne Geometrie liefert null blockData (Caller nutzt {type:svg}-Fallback)', () => {
const payload = importSVGAsBlockPayload('<svg xmlns="http://www.w3.org/2000/svg"><defs/></svg>');
expect(payload.blockData).toBeNull();
expect(payload.svg).toContain('<svg');
});
});