/** * Task D1 – Ellipse+Bogen-Tool (core-drawing). * * 3-Klick-Interaktion: Klick1=Zentrum, Klick2=RadiusX (Richtung * definiert die Rotation), Klick3=RadiusY. Commit als geschlossene * Polyline (64 Punkte, F5-Parser-Konvention) mit cx/cy/rx/ry/rotation * Metadaten. Ellipsenbogen via startAngle/endAngle-Options → offene * Polyline mit proportionalen Punkten. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ellipseTool } from '../src/plugins/builtin/core-drawing'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import type { CADElement, Pt } from '../src/types/cad.types'; function mkCtx(options: Record = {}) { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); const previews: (CADElement | null)[] = []; const statuses: string[] = []; const ctx: any = { doc, options, setStatus: (m: string) => statuses.push(m), setPreview: (el: CADElement | null) => previews.push(el), }; return { ctx, doc, previews, statuses }; } function pe(x: number, y: number): { world: Pt } { return { world: { x, y } } as never; } beforeEach(() => { ellipseTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never); }); describe('D1: ellipseTool', () => { it('3 Klicks: Zentrum→RadiusX→RadiusY erzeugt geschlossene Ellipse (64 Punkte)', () => { const { ctx, doc } = mkCtx(); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.down!(pe(100, 0), ctx); // RadiusX=100, Rotation=0 ellipseTool.handlers.down!(pe(0, 50), ctx); // RadiusY=50 const els = doc.getAllElements(); expect(els).toHaveLength(1); const el = els[0]; expect(el.type).toBe('polygon'); const pts = el.properties.points as Pt[]; expect(pts.length).toBe(64); // BBox: rx=100, ry=50 const xs = pts.map((p) => p.x); const 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); // Metadaten expect(el.properties.cx).toBe(0); expect(el.properties.cy).toBe(0); expect(el.properties.rx).toBe(100); expect(el.properties.ry).toBe(50); expect(el.properties.rotation).toBe(0); }); it('Zugrichtung definiert Rotation: Klick2 nach (0,100) → rotation=90°', () => { const { ctx, doc } = mkCtx(); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.down!(pe(0, 100), ctx); // RadiusX zeigt nach unten ellipseTool.handlers.down!(pe(50, 0), ctx); // RadiusY const el = doc.getAllElements()[0]; expect(el.properties.rotation).toBeCloseTo(90, 1); // Rotierte Ellipse: Hauptachse vertikal → maxY ≈ 100 const ys = (el.properties.points as Pt[]).map((p) => p.y); expect(Math.max(...ys)).toBeCloseTo(100, 0); }); it('Preview bei move nach Phase 2 zeigt Ellipse', () => { const { ctx, previews } = mkCtx(); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.move!(pe(80, 0), ctx); // RadiusX-Linie ellipseTool.handlers.down!(pe(80, 0), ctx); // RadiusX fixiert ellipseTool.handlers.move!(pe(0, 40), ctx); // Ellipsen-Preview mit live ry expect(previews.length).toBeGreaterThanOrEqual(2); const last = previews[previews.length - 1]; expect(last).not.toBeNull(); expect(last!.type).toBe('polygon'); }); it('Status führt durch die Phasen', () => { const { ctx, statuses } = mkCtx(); ellipseTool.handlers.down!(pe(0, 0), ctx); expect(statuses[0]).toContain('Zentrum'); ellipseTool.handlers.down!(pe(100, 0), ctx); expect(statuses[1]).toContain('Radius'); }); it('cancel resettet die Messung', () => { const { ctx, doc } = mkCtx(); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.down!(pe(100, 0), ctx); ellipseTool.handlers.cancel?.(ctx); // Nach cancel startet frisch: nächster Klick = Zentrum (kein Commit) ellipseTool.handlers.down!(pe(0, 0), ctx); expect(doc.getAllElements()).toHaveLength(0); }); }); describe('D1: Ellipsenbogen (startAngle/endAngle)', () => { it('Options startAngle=0/endAngle=180 erzeugen OFFENE Polyline (32 Punkte)', () => { const { ctx, doc } = mkCtx({ startAngle: 0, endAngle: 180 }); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.down!(pe(100, 0), ctx); ellipseTool.handlers.down!(pe(0, 50), ctx); const el = doc.getAllElements()[0]; expect(el.type).toBe('polyline'); // offen const pts = el.properties.points as Pt[]; expect(pts.length).toBe(32); // halbe Ellipse = 64/2 }); it('Winkel-Metadaten in properties', () => { const { ctx, doc } = mkCtx({ startAngle: 45, endAngle: 225 }); ellipseTool.handlers.down!(pe(0, 0), ctx); ellipseTool.handlers.down!(pe(100, 0), ctx); ellipseTool.handlers.down!(pe(0, 50), ctx); const el = doc.getAllElements()[0]; expect(el.properties.startAngle).toBe(45); expect(el.properties.endAngle).toBe(225); }); }); describe('D1: Manifest', () => { it('id=ellipse, ribbonTab=canvas, tags basic, Version in tools-Array', () => { expect(ellipseTool.manifest.id).toBe('ellipse'); expect(ellipseTool.manifest.ribbonTab).toBe('canvas'); expect((ellipseTool.manifest.tags ?? [])).toContain('basic'); }); });