148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
/**
|
||
* Task D2 – Spline-Tool (CV-basiert): Kontrollpunkt-Klick-Sequenz,
|
||
* ENTER committet ab 3 CVs. Render als interpolierte Polyline mit
|
||
* Catmull-Rom-Glättung (Kurve läuft DURCH alle CVs), CVs in
|
||
* properties.controlPoints gespeichert (DXF-SPLINE-Export-ready).
|
||
*/
|
||
import { describe, it, expect, beforeEach } from 'vitest';
|
||
import { splineTool } 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(() => {
|
||
splineTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||
});
|
||
|
||
describe('D2: splineTool', () => {
|
||
it('4 CVs + ENTER erzeugt EINE geglaettete Polyline mit mehr Punkten als CVs', () => {
|
||
const { ctx, doc } = mkCtx();
|
||
const cvs = [[0, 0], [100, 100], [200, 0], [300, 100]];
|
||
for (const [x, y] of cvs) splineTool.handlers.down!(pe(x, y), ctx);
|
||
const committed = splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
expect(committed).toBe(true);
|
||
const els = doc.getAllElements();
|
||
expect(els).toHaveLength(1);
|
||
const el = els[0];
|
||
expect(el.type).toBe('polyline');
|
||
const pts = el.properties.points as Pt[];
|
||
expect(pts.length).toBeGreaterThan(8); // geglaettet, nicht nur CVs
|
||
});
|
||
|
||
it('Kurve interpoliert DURCH die CVs: Start/Ende exakt, Zwischen-CVs liegen auf der Kurve', () => {
|
||
const { ctx, doc } = mkCtx();
|
||
for (const [x, y] of [[0, 0], [100, 100], [200, 0], [300, 100]]) {
|
||
splineTool.handlers.down!(pe(x, y), ctx);
|
||
}
|
||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
const el = doc.getAllElements()[0];
|
||
const pts = el.properties.points as Pt[];
|
||
expect(pts[0]).toEqual({ x: 0, y: 0 });
|
||
const last = pts[pts.length - 1];
|
||
expect(last.x).toBeCloseTo(300, 5);
|
||
expect(last.y).toBeCloseTo(100, 5);
|
||
// Zwischenpunkt (100,100) muss auf der Kurve liegen (Catmull-Rom interpoliert):
|
||
const near = pts.find((p) => Math.abs(p.x - 100) < 1.5 && Math.abs(p.y - 100) < 1.5);
|
||
expect(near).toBeDefined();
|
||
});
|
||
|
||
it('CVs werden in properties.controlPoints gespeichert', () => {
|
||
const { ctx, doc } = mkCtx();
|
||
for (const [x, y] of [[0, 0], [50, 80], [120, 40]]) {
|
||
splineTool.handlers.down!(pe(x, y), ctx);
|
||
}
|
||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
const el = doc.getAllElements()[0];
|
||
const cvs = el.properties.controlPoints as Pt[];
|
||
expect(cvs).toHaveLength(3);
|
||
expect(cvs[1]).toEqual({ x: 50, y: 80 });
|
||
});
|
||
|
||
it('Glättung: Kurve weicht von der CV-Sehnen-Linie ab (keine Polyline durch CVs)', () => {
|
||
const { ctx, doc } = mkCtx();
|
||
// S-artige CVs: (0,0) (0,100) (100,100) (100,0) — Catmull-Rom schwingt um die Ecken
|
||
for (const [x, y] of [[0, 0], [0, 100], [100, 100], [100, 0]]) {
|
||
splineTool.handlers.down!(pe(x, y), ctx);
|
||
}
|
||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
const el = doc.getAllElements()[0];
|
||
const pts = el.properties.points as Pt[];
|
||
// Es muss Punkte geben, die ausserhalb der BBox der Sehnen-Verbindung liegen
|
||
// (Glattung schwingt): minY < 0 oder maxY > 100 an irgendeiner Stelle
|
||
const ys = pts.map((p) => p.y);
|
||
const swingsBelow = Math.min(...ys) < -0.5;
|
||
const swingsAbove = Math.max(...ys) > 100.5;
|
||
expect(swingsBelow || swingsAbove).toBe(true);
|
||
});
|
||
|
||
it('ENTER mit weniger als 3 CVs committet NICHT (Status erklaert Mindestzahl)', () => {
|
||
const { ctx, doc, statuses } = mkCtx();
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||
const consumed = splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
expect(doc.getAllElements()).toHaveLength(0);
|
||
expect(statuses[statuses.length - 1]).toContain('3');
|
||
});
|
||
|
||
it('Nicht-ENTER-Keys werden nicht konsumiert (return false)', () => {
|
||
const { ctx } = mkCtx();
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
const consumed = splineTool.handlers.key!({ key: 'a' } as never, ctx);
|
||
expect(consumed).toBe(false);
|
||
});
|
||
|
||
it('Preview bei move ab 1 CV', () => {
|
||
const { ctx, previews } = mkCtx();
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
splineTool.handlers.move!(pe(50, 50), ctx);
|
||
expect(previews.length).toBeGreaterThanOrEqual(1);
|
||
const last = previews[previews.length - 1];
|
||
expect(last).not.toBeNull();
|
||
});
|
||
|
||
it('cancel resettet: naechste Sitzung startet frisch', () => {
|
||
const { ctx, doc } = mkCtx();
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||
splineTool.handlers.cancel?.(ctx);
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||
splineTool.handlers.down!(pe(100, 0), ctx);
|
||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||
// 3 CVs der NEUEN Sitzung — controlPoints == 3 (alte 2 nicht gemischt)
|
||
const el = doc.getAllElements()[0];
|
||
expect((el.properties.controlPoints as Pt[]).length).toBe(3);
|
||
});
|
||
|
||
it('Status zaehlt CVs mit', () => {
|
||
const { ctx, statuses } = mkCtx();
|
||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||
expect(statuses[0]).toContain('1');
|
||
expect(statuses[0]).toContain('Spline');
|
||
});
|
||
|
||
it('Manifest: id=spline, ribbonTab=canvas, tags basic', () => {
|
||
expect(splineTool.manifest.id).toBe('spline');
|
||
expect(splineTool.manifest.ribbonTab).toBe('canvas');
|
||
expect((splineTool.manifest.tags ?? [])).toContain('basic');
|
||
});
|
||
});
|