/** * Task D10-Rest – array-path: Elemente entlang einer Polyline verteilen. * * Klick1 = Quelle (Auto-HitSelect bei leerer Selektion), Klick2 auf * Pfad-Polyline → Element-Kopien gleichmaessig entlang der Pfad- * laenge verteilt (count aus Options, Default 5), Original bleibt, * ALLES in EINER Transaktion = 1 Undo-Schritt. */ import { describe, it, expect, beforeEach } from 'vitest'; import { arrayPathTool } from '../src/plugins/builtin/core-modify'; 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 selectedIds = new Set(); const statuses: string[] = []; const ctx: any = { doc, options, setStatus: (m: string) => statuses.push(m), setPreview: (_el: CADElement | null) => {}, selection: { getIds: () => Array.from(selectedIds), setIds: (ids: string[]) => { selectedIds.clear(); for (const id of ids) selectedIds.add(id); }, hitTest: (x: number, y: number, _tol: number) => { // trifft das Element, dessen BBox den Punkt enthaelt (erstes Gewinnt) for (const el of elements) { if ( x >= el.x - el.width / 2 && x <= el.x + el.width / 2 && y >= el.y - el.height / 2 && y <= el.y + el.height / 2 ) return el; } return null; }, }, }; return { ctx, doc, selectedIds, statuses }; } function pe(x: number, y: number): { world: Pt } { return { world: { x, y } } as never; } const CHAIR = { id: 'chair-1', type: 'chair', layerId: 'layer-0', x: 0, y: 0, width: 40, height: 40, properties: {}, } as unknown as CADElement; const PATH = { id: 'path-1', type: 'polyline', layerId: 'layer-0', x: 250, y: 0, width: 500, height: 0, properties: { points: [{ x: 0, y: 0 }, { x: 500, y: 0 }] }, } as unknown as CADElement; beforeEach(() => { arrayPathTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never); }); describe('D10: arrayPathTool', () => { it('verteilt count Kopien gleichmaessig entlang der Pfadlaenge (Original bleibt)', () => { const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]); selectedIds.add('chair-1'); // Klick1: Quelle ist schon selektiert -> Basis direkt setzen arrayPathTool.handlers.down!(pe(0, 0), ctx); // Klick2: auf den Pfad klicken (Pfad-Mitte 250,0) arrayPathTool.handlers.down!(pe(250, 0), ctx); const els = doc.getAllElements(); // Original + Pfad + 5 Kopien const chairs = els.filter((e) => e.type === 'chair'); expect(chairs.length).toBe(1 + 5); // Kopien getrennt vom Original (Original bleibt bei x=0 unangetastet): const copies = chairs.filter((c) => c.id !== 'chair-1'); expect(copies).toHaveLength(5); const copyXs = copies.map((c) => c.x).sort((a, b) => a - b); // count=5 gleichmaessig ueber Pfadlaenge 500: s = i/(count-1)*total expect(copyXs[0]).toBeCloseTo(0, 0); expect(copyXs[1]).toBeCloseTo(125, 0); expect(copyXs[2]).toBeCloseTo(250, 0); expect(copyXs[3]).toBeCloseTo(375, 0); expect(copyXs[4]).toBeCloseTo(500, 0); }); it('Auto-HitSelect bei leerer Selektion (Klick1 auf Element)', () => { const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]); // Selektion ist leer: Klick auf den Stuhl waehlt ihn automatisch arrayPathTool.handlers.down!(pe(0, 0), ctx); expect(selectedIds.has('chair-1')).toBe(true); // Klick2 auf Pfad -> Commit arrayPathTool.handlers.down!(pe(100, 0), ctx); expect(doc.getAllElements().filter((e) => e.type === 'chair').length).toBe(6); }); it('Klick2 trifft KEINEN Pfad -> kein Commit, Status erklaert', () => { const { ctx, doc, selectedIds, statuses } = mkCtx([CHAIR, PATH]); selectedIds.add('chair-1'); arrayPathTool.handlers.down!(pe(0, 0), ctx); // Klick weit weg vom Pfad (kein Element getroffen) arrayPathTool.handlers.down!(pe(9999, 9999), ctx); expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1); expect(statuses[statuses.length - 1]).toContain('Pfad'); }); it('Kopien in EINER Transaktion = 1 Undo entfernt alle', () => { const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]); selectedIds.add('chair-1'); arrayPathTool.handlers.down!(pe(0, 0), ctx); arrayPathTool.handlers.down!(pe(250, 0), ctx); expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(6); doc.undo(); expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1); }); it('count-Option steuert die Anzahl (3 statt 5)', () => { const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH], { count: 3 }); selectedIds.add('chair-1'); arrayPathTool.handlers.down!(pe(0, 0), ctx); arrayPathTool.handlers.down!(pe(250, 0), ctx); expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(4); // Original + 3 // Kopien getrennt vom Original: count=3 -> s = 0, 250, 500 const copies3 = doc.getAllElements().filter((e) => e.type === 'chair' && e.id !== 'chair-1'); const copyXs3 = copies3.map((c) => c.x).sort((a, b) => a - b); expect(copyXs3[0]).toBeCloseTo(0, 0); expect(copyXs3[1]).toBeCloseTo(250, 0); expect(copyXs3[2]).toBeCloseTo(500, 0); }); it('gebogener Pfad (3 Punkte): Kopien folgen der Geometrie', () => { const cornerPath = { id: 'path-2', type: 'polyline', layerId: 'layer-0', x: 100, y: 100, width: 200, height: 200, properties: { points: [{ x: 0, y: 0 }, { x: 200, y: 0 }, { x: 200, y: 200 }] }, } as unknown as CADElement; const { ctx, doc, selectedIds } = mkCtx([CHAIR, cornerPath], { count: 4 }); selectedIds.add('chair-1'); arrayPathTool.handlers.down!(pe(0, 0), ctx); arrayPathTool.handlers.down!(pe(100, 10), ctx); const chairs = doc.getAllElements().filter((e) => e.type === 'chair'); expect(chairs.length).toBe(5); // Original + 4 // Kopien muessen auf dem L-foermigen Pfad liegen: alle x<=200, y>=0 for (const c of chairs.slice(1)) { expect(c.x).toBeLessThanOrEqual(200.5); expect(c.y).toBeGreaterThanOrEqual(-0.5); } }); it('cancel resettet die Sitzung', () => { const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]); selectedIds.add('chair-1'); arrayPathTool.handlers.down!(pe(0, 0), ctx); arrayPathTool.handlers.cancel?.(ctx); // Nach cancel: Klick startet frisch (Auto-HitSelect-Pfad wieder aktiv) arrayPathTool.handlers.down!(pe(9999, 9999), ctx); expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1); }); it('Manifest: id=array-path, tags pro', () => { expect(arrayPathTool.manifest.id).toBe('array-path'); expect((arrayPathTool.manifest.tags ?? [])).toContain('pro'); const keys = (arrayPathTool.optionsSchema ?? []).map((f) => f.key); expect(keys).toContain('count'); }); });