138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
/**
|
||
* 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<string, unknown> = {}) {
|
||
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');
|
||
});
|
||
});
|