/** * 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 = {}) { 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'); }); });