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

220 lines
8.3 KiB
TypeScript

/**
* Task CAD-7 - Magnetisches Traversen-Tool.
*
* Traversen als rechteckige Balken (Laengen-Option 100/200/290cm,
* Standard 200). MAGNETISCH: Nahe dem Ende einer bestehenden
* Traverse dockt die neue exakt an (Position + Rotation uebernommen)
* - trussEndpoints (Endpunkte in Weltkoordinaten, rotationsbewusst)
* - snapTrussToDock (Dock-Suche + Andock-Geometrie, rein)
* - trussTool: Klick platziert (gedockt oder frei), Drag = Richtung.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
trussTool,
trussEndpoints,
snapTrussToDock,
type TrussDock,
} from '../src/plugins/builtin/event-tools';
import { CADDocument } from '../src/kernel/document/CADDocument';
import { createInMemoryDoc } from './helpers/inMemoryDoc';
import type { CADElement, Pt } from '../src/types/cad.types';
function mkCtx(elements: CADElement[] = [], options: Record<string, unknown> = {}) {
const { ydoc } = createInMemoryDoc();
const doc = new CADDocument(ydoc);
for (const el of elements) doc.addElement(el);
const statuses: string[] = [];
const previews: (CADElement | null)[] = [];
const ctx: any = {
doc,
options,
setStatus: (m: string) => statuses.push(m),
setPreview: (el: CADElement | null) => previews.push(el),
};
return { ctx, doc, statuses, previews };
}
function pe(x: number, y: number, mods: { shift?: boolean } = {}): { world: Pt; shift: boolean } {
return { world: { x, y }, shift: mods.shift ?? false } as never;
}
function makeTruss(id: string, x: number, y: number, length: number, rotationDeg = 0): CADElement {
return {
id, type: 'truss', layerId: 'layer-0',
x, y,
width: length, height: 20,
properties: { length, rotation: rotationDeg, truss: true },
} as unknown as CADElement;
}
beforeEach(() => {
trussTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
});
// ─── trussEndpoints (reine Funktion) ──────────────────────────
describe('CAD-7: trussEndpoints', () => {
it('horizontale Traverse: Endpunkte links/rechts vom Zentrum', () => {
const el = makeTruss('t1', 100, 100, 200, 0);
const [a, b] = trussEndpoints(el);
expect(a).toEqual({ x: 0, y: 100 });
expect(b).toEqual({ x: 200, y: 100 });
});
it('90-Grad-Traverse: Endpunkte vertikal', () => {
const el = makeTruss('t1', 100, 100, 200, 90);
const [a, b] = trussEndpoints(el);
expect(a.x).toBeCloseTo(100, 5);
expect(a.y).toBeCloseTo(0, 5);
expect(b.x).toBeCloseTo(100, 5);
expect(b.y).toBeCloseTo(200, 5);
});
it('45-Grad-Traverse: diagonale Endpunkte', () => {
const el = makeTruss('t1', 100, 100, 200, 45);
const [a, b] = trussEndpoints(el);
const half = 100;
const cos45 = Math.cos(Math.PI / 4) * half;
expect(a.x).toBeCloseTo(100 - cos45, 3);
expect(a.y).toBeCloseTo(100 - cos45, 3);
expect(b.x).toBeCloseTo(100 + cos45, 3);
});
});
// ─── snapTrussToDock (magnetisches Andocken, rein) ──────────
describe('CAD-7: snapTrussToDock (magnetisch)', () => {
const existing = makeTruss('t1', 100, 100, 200, 0); // Enden (0,100) und (200,100)
const tol = 25;
it('Cursor nahe Endpunkt (200,100): dockt exakt an mit Rotation 0', () => {
const result = snapTrussToDock([existing], { x: 210, y: 105 }, 200, tol);
expect(result).not.toBeNull();
expect(result!.rotationDeg).toBeCloseTo(0, 5);
// Neue Traverse startet am Dock (200,100) → Zentrum bei (200+100, 100)
expect(result!.center.x).toBeCloseTo(300, 3);
expect(result!.center.y).toBeCloseTo(100, 3);
expect(result!.dockedTo).toBe('t1');
});
it('Cursor nahe Startpunkt (0,100): dockt in GEGENRICHTUNG (Rotation 180)', () => {
const result = snapTrussToDock([existing], { x: -10, y: 98 }, 200, tol);
expect(result).not.toBeNull();
expect(result!.rotationDeg).toBeCloseTo(180, 5);
expect(result!.center.x).toBeCloseTo(-100, 3);
});
it('Cursor weiter weg als Toleranz: kein Dock (null)', () => {
const result = snapTrussToDock([existing], { x: 500, y: 500 }, 200, tol);
expect(result).toBeNull();
});
it('andocken an rotierte Traverse (90 Grad): Rotation wird uebernommen', () => {
const vert = makeTruss('v1', 100, 100, 200, 90); // Enden (100,0) und (100,200)
const result = snapTrussToDock([vert], { x: 105, y: 210 }, 200, tol);
expect(result).not.toBeNull();
expect(result!.rotationDeg).toBeCloseTo(90, 5);
expect(result!.center.x).toBeCloseTo(100, 3);
expect(result!.center.y).toBeCloseTo(300, 3);
});
it('mehrere Traversen: naechstgelegener Dock gewinnt', () => {
const other = makeTruss('t2', 400, 100, 200, 0); // Enden (300,100),(500,100)
const result = snapTrussToDock([existing, other], { x: 305, y: 100 }, 200, tol);
expect(result!.dockedTo).toBe('t2');
});
});
// ─── trussTool (Platzierung + Magnetismus) ──────────────────
describe('CAD-7: trussTool', () => {
it('Klick platziert freie Traverse (200cm Standard) als truss-Element', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(300, 300), ctx);
trussTool.handlers.up!(pe(400, 300), ctx);
const els = doc.getAllElements();
expect(els).toHaveLength(1);
expect(els[0].type).toBe('truss');
expect(els[0].properties.length).toBe(200);
expect(els[0].properties.truss).toBe(true);
});
it('Drag definiert die Richtung: horizontale Traverse bei Drag nach rechts', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.up!(pe(300, 100), ctx);
const el = doc.getAllElements()[0];
expect(el.properties.rotation).toBeCloseTo(0, 5);
// Laenge aus Drag-Distanz? Nein: Standardlaenge bleibt 200 (Option)
expect(el.properties.length).toBe(200);
});
it('Drag nach oben: Rotation 90', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.up!(pe(100, 300), ctx);
expect(doc.getAllElements()[0].properties.rotation).toBeCloseTo(90, 1);
});
it('MAGNETISCH: Klick nahe bestehendem Traversen-Ende dockt exakt an', () => {
const existing = makeTruss('t1', 100, 100, 200, 0);
const { ctx, doc } = mkCtx([existing]);
// Klick nahe (200,100) → andocken
trussTool.handlers.down!(pe(210, 105), ctx);
trussTool.handlers.up!(pe(300, 100), ctx);
const els = doc.getAllElements().filter((e) => e.properties.truss);
expect(els).toHaveLength(2);
const newTruss = els.find((e) => e.id !== 't1')!;
// Exakt angedockt: Rotation 0, Start am Ende der ersten
expect(newTruss.properties.rotation).toBeCloseTo(0, 5);
expect(newTruss.x).toBeCloseTo(300, 3);
expect(newTruss.y).toBeCloseTo(100, 3);
});
it('ohne bestehende Traversen: freie Platzierung (kein Crash)', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.up!(pe(200, 100), ctx);
expect(doc.getAllElements()).toHaveLength(1);
});
it('Laengen-Option: 290 statt 200', () => {
const { ctx, doc } = mkCtx([], { length: 290 });
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.up!(pe(300, 100), ctx);
expect(doc.getAllElements()[0].properties.length).toBe(290);
});
it('ALLES in EINER Transaktion (1 Undo entfernt die Traverse)', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.up!(pe(300, 100), ctx);
expect(doc.getAllElements()).toHaveLength(1);
doc.undo();
expect(doc.getAllElements()).toHaveLength(0);
});
it('cancel resettet die Drag-Sitzung', () => {
const { ctx, doc } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.cancel?.(ctx);
trussTool.handlers.up!(pe(300, 100), ctx);
expect(doc.getAllElements()).toHaveLength(0);
});
it('Preview bei move zwischen down und up', () => {
const { ctx, previews } = mkCtx();
trussTool.handlers.down!(pe(100, 100), ctx);
trussTool.handlers.move!(pe(250, 100), ctx);
expect(previews.length).toBeGreaterThanOrEqual(1);
const last = previews[previews.length - 1];
expect(last).not.toBeNull();
});
it('Manifest: id=truss, Optionen length/rotation', () => {
expect(trussTool.manifest.id).toBe('truss');
const keys = (trussTool.optionsSchema ?? []).map((f) => f.key);
expect(keys).toContain('length');
});
});