Files
web-cad/frontend/tests/splineTool.test.ts
T

148 lines
5.9 KiB
TypeScript
Raw 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 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');
});
});