993 lines
36 KiB
TypeScript
993 lines
36 KiB
TypeScript
/**
|
||
* Pilot V2-Tools Tests (Task A3): line + select.
|
||
* line: Handler-Sequenz gegen echtes CADDocument (In-Memory), Undo-Verhalten.
|
||
* select: Bridge-basierte Auswahl mit Shift/Ctrl.
|
||
*/
|
||
import { describe, it, expect, vi } from 'vitest';
|
||
import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing';
|
||
import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify';
|
||
import { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate';
|
||
import { coreMeasurePlugin } from '../src/plugins/builtin/core-measure';
|
||
import { eventToolsV2Plugin } from '../src/plugins/builtin/event-tools';
|
||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||
import type { ToolPointerEvent, FullToolContext } from '../src/plugins/types';
|
||
import type { CADElement } from '../src/types/cad.types';
|
||
|
||
function pe(x: number, y: number, mods: Partial<Pick<ToolPointerEvent, 'shift' | 'ctrl'>> = {}): ToolPointerEvent {
|
||
return {
|
||
world: { x, y },
|
||
screen: { x: x / 2, y: y / 2 },
|
||
button: 0,
|
||
shift: mods.shift ?? false,
|
||
alt: false,
|
||
ctrl: mods.ctrl ?? false,
|
||
};
|
||
}
|
||
|
||
function makeCtx(overrides: Partial<FullToolContext> = {}): FullToolContext & { previews: (CADElement | null)[]; statuses: string[] } {
|
||
const previews: (CADElement | null)[] = [];
|
||
const statuses: string[] = [];
|
||
return {
|
||
options: {},
|
||
setStatus: (m) => statuses.push(m),
|
||
setPreview: (el) => previews.push(el),
|
||
previews,
|
||
statuses,
|
||
...overrides,
|
||
} as FullToolContext & { previews: (CADElement | null)[]; statuses: string[] };
|
||
}
|
||
|
||
function makeEl(id: string): CADElement {
|
||
return { id, type: 'rect', layerId: 'l', x: 0, y: 0, width: 1, height: 1, properties: {} } as unknown as CADElement;
|
||
}
|
||
|
||
describe('A3 pilot: line tool', () => {
|
||
it('erzeugt EINE Linie per down→up; ein undo entfernt sie', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
|
||
lineTool.handlers.down!(pe(0, 0), ctx);
|
||
lineTool.handlers.move!(pe(100, 50), ctx);
|
||
expect(ctx.previews.length).toBe(1); // Live-Vorschau war da
|
||
lineTool.handlers.up!(pe(100, 50), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('line');
|
||
expect(all[0].properties.x1).toBe(0);
|
||
expect(all[0].properties.y2).toBe(50);
|
||
|
||
// Kern-Akzeptanz aus ROADMAP: Ein Werkzeug-Klick = ein Undo-Schritt
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('cancel verwirft die laufende Linie ohne Element', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
|
||
lineTool.handlers.down!(pe(5, 5), ctx);
|
||
lineTool.handlers.cancel!(ctx);
|
||
lineTool.handlers.up!(pe(9, 9), ctx); // up nach cancel: kein Start mehr
|
||
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('Plugin-Manifest ist registrierbar und tagged basic', () => {
|
||
expect(coreDrawingPlugin.manifest.enabledByDefault).toBe(true);
|
||
const line = coreDrawingPlugin.tools?.find((t) => t.manifest.id === 'line');
|
||
expect(line?.manifest.tags).toContain('basic');
|
||
});
|
||
});
|
||
|
||
describe('A4.3 pilot: circle tool', () => {
|
||
it('erzeugt Kreis Zentrum+Radius; ein undo entfernt ihn', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const circle = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'circle')!;
|
||
|
||
circle.handlers.down!(pe(100, 100), ctx);
|
||
circle.handlers.move!(pe(200, 100), ctx);
|
||
expect(ctx.previews.length).toBe(1);
|
||
circle.handlers.up!(pe(200, 100), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('circle');
|
||
expect(all[0].x).toBe(100);
|
||
expect(all[0].properties.radius).toBe(100); // Distanz wie Legacy
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.3 pilot: arc tool', () => {
|
||
it('klick-progressiv: Zentrum→Start→Ende committet beim 3. Klick mit Grad-Winkeln', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const arc = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'arc')!;
|
||
|
||
// Klick 1: Zentrum (up gehört zum Klick, committet NICHT — klick-progressiv)
|
||
arc.handlers.down!(pe(0, 0), ctx);
|
||
expect(doc.getAllElements().length).toBe(0); // Phase 1/2 committet nie
|
||
|
||
// Klick 2: Startpunkt auf Radius 50 nach Osten (0°)
|
||
arc.handlers.down!(pe(50, 0), ctx);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
|
||
// Klick 3: Endpunkt oben (90°) → JETZT committet der dritte down
|
||
arc.handlers.down!(pe(0, 50), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('arc');
|
||
expect(all[0].x).toBe(0);
|
||
expect(all[0].y).toBe(0);
|
||
expect(all[0].properties.radius).toBe(50);
|
||
expect(all[0].properties.startAngle).toBeCloseTo(0);
|
||
expect(all[0].properties.endAngle).toBeCloseTo(90);
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('cancel bricht mehrphasige Sitzung vollstaendig ab', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const arc = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'arc')!;
|
||
|
||
arc.handlers.down!(pe(0, 0), ctx); // phase 1
|
||
arc.handlers.down!(pe(30, 0), ctx); // phase 2
|
||
arc.handlers.cancel!(ctx); // reset
|
||
arc.handlers.down!(pe(99, 99), ctx); // startet neu bei phase 1
|
||
|
||
// Kein Commit erfolgt (nur phase 1), nichts im Doc:
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.3 pilot: polyline/polygon tools', () => {
|
||
it('polyline sammelt Punkte und committet per ENTER ab 2', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const pl = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'polyline')!;
|
||
|
||
pl.handlers.down!(pe(0, 0), ctx);
|
||
pl.handlers.move!(pe(10, 10), ctx);
|
||
expect(ctx.previews.length).toBeGreaterThan(0);
|
||
pl.handlers.down!(pe(20, 0), ctx);
|
||
pl.handlers.down!(pe(30, 30), ctx);
|
||
|
||
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||
const consumed = pl.handlers.key!(kev, ctx);
|
||
expect(consumed).toBe(true);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('polyline');
|
||
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||
expect(all[0].x).toBe(15); // BBox-Zentrum min0 max30
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('polygon braucht mind. 3 Punkte; ENTER davor meldet Hinweis', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const pg = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'polygon')!;
|
||
const ctx = makeCtx({ doc });
|
||
|
||
pg.handlers.down!(pe(0, 0), ctx);
|
||
pg.handlers.down!(pe(10, 10), ctx);
|
||
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||
pg.handlers.key!(kev, ctx); // nur 2 Punkte → abgelehnt
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
expect(ctx.statuses.some((s) => s.includes('mindestens 3'))).toBe(true);
|
||
|
||
pg.handlers.down!(pe(20, -5), ctx);
|
||
pg.handlers.key!(kev, ctx); // jetzt 3 Punkte → ok
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('polygon');
|
||
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||
});
|
||
});
|
||
|
||
describe('A4.4 pilot: annotate tools', () => {
|
||
it('dimension: down→up committet mit x1..y2-Properties; undo entfernt', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const dim = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'dimension')!;
|
||
|
||
dim.handlers.down!(pe(0, 0), ctx);
|
||
dim.handlers.move!(pe(60, 0), ctx); // Preview war da
|
||
expect(ctx.previews.length).toBe(1);
|
||
dim.handlers.up!(pe(60, 0), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('dimension');
|
||
expect(all[0].x).toBe(30); // Mitte
|
||
expect(all[0].properties.x2).toBe(60);
|
||
expect(all[0].width).toBeCloseTo(60); // dist als Breite wie Legacy
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('leader: übernimmt Text/fontSize-Konvention aus Legacy', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const leader = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'leader')!;
|
||
|
||
leader.handlers.down!(pe(10, 20), ctx);
|
||
leader.handlers.up!(pe(80, 90), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('leader');
|
||
expect(all[0].properties.x1).toBe(10);
|
||
expect(all[0].properties.y2).toBe(90);
|
||
expect(all[0].properties.text).toBe('');
|
||
expect(all[0].properties.fontSize).toBe(12);
|
||
});
|
||
|
||
it('text: Einzelklick platziert leeres Textelement mit fontSize-Option', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc, options: { fontSize: 18 } });
|
||
const txt = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'text')!;
|
||
|
||
txt.handlers.down!(pe(40, 70), ctx); // kein up nötig — single-point tool
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('text');
|
||
expect(all[0].x).toBe(40);
|
||
expect(all[0].y).toBe(70);
|
||
expect(all[0].width).toBe(100); // Legacy-Default-BBox
|
||
expect(all[0].properties.fontSize).toBe(18); // aus options statt Default 12
|
||
expect(all[0].properties.text).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('A4.5 pilot: revcloud tool', () => {
|
||
it('committet per ENTER ab 3 Punkten mit Legacy-Konvention arcHeight=8', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const cloud = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'revcloud')!;
|
||
|
||
cloud.handlers.down!(pe(0, 0), ctx);
|
||
cloud.handlers.down!(pe(100, 0), ctx);
|
||
cloud.handlers.down!(pe(100, 50), ctx);
|
||
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||
const consumed = cloud.handlers.key!(kev, ctx);
|
||
expect(consumed).toBe(true);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('revcloud');
|
||
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||
expect(all[0].properties.arcHeight).toBe(8); // Legacy-Konvention
|
||
expect(all[0].properties.fill).toBe('none');
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.5 pilot: measure tool', () => {
|
||
it('2 Klicks zeigen Distanz+Winkel im Status; erzeugt KEIN Element', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const meas = coreMeasurePlugin.tools!.find((t) => t.manifest.id === 'measure')!;
|
||
|
||
meas.handlers.down!(pe(0, 0), ctx);
|
||
expect(ctx.statuses.some((s) => s.includes('Endpunkt'))).toBe(true);
|
||
|
||
meas.handlers.down!(pe(30, 40), ctx); // dist=50, angle≈53.13°
|
||
const last = ctx.statuses[ctx.statuses.length - 1];
|
||
expect(last).toContain('distance:');
|
||
expect(last).toContain('angle:');
|
||
expect(last).toContain('53.1'); // atan2(40,30)≈53.13°
|
||
// KernLegacy-Verhalten: Messung erzeugt kein Element
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.5 pilot: hatch tool', () => {
|
||
it('setzt hatch-Properties via updateElement; undo macht rückgängig', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
// Seed-Rect im Doc, das beim hitTest zurückkommt:
|
||
const rectEl = { id: 'r1', type: 'rect', layerId: 'l', x: 50, y: 50, width: 10, height: 10, properties: {} } as unknown as CADElement;
|
||
doc.addElement(rectEl);
|
||
const bridge = {
|
||
getIds: () => [],
|
||
setIds: () => {},
|
||
hitTest: () => rectEl,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge, options: { hatchPattern: 'cross', hatchSpacing: 12 } });
|
||
const hatch = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'hatch')!;
|
||
|
||
hatch.handlers.down!(pe(50, 50), ctx);
|
||
|
||
const updated = doc.getElement('r1')!;
|
||
expect(updated.properties.hatch).toBe(true);
|
||
expect(updated.properties.hatchPattern).toBe('cross'); // aus Options
|
||
expect(updated.properties.hatchSpacing).toBe(12);
|
||
|
||
expect(doc.undo()).toBe(true); // A2b-Update ist selbst Undo-Schritt
|
||
expect(doc.getElement('r1')!.properties.hatch).toBeUndefined();
|
||
});
|
||
|
||
it('lehnt offene Elementtypen (line) mit Hinweis ab', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const lineEl = { id: 'l1', type: 'line', layerId: 'l', x: 0, y: 0, width: 5, height: 5, properties: { x1: 0, y1: 0, x2: 5, y2: 5 } } as unknown as CADElement;
|
||
doc.addElement(lineEl);
|
||
const bridge = { getIds: () => [], setIds: () => {}, hitTest: () => lineEl };
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const hatch = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'hatch')!;
|
||
|
||
hatch.handlers.down!(pe(2, 2), ctx);
|
||
expect(doc.getElement('l1')!.properties.hatch).toBeUndefined(); // nichts geändert
|
||
expect(ctx.statuses.some((s) => s.includes('rect/circle/polygon'))).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('A4.9 pilot: event tools', () => {
|
||
it('chair platziert Einzel-Element per Klick', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const chair = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'chair')!;
|
||
|
||
chair.handlers.down!(pe(100, 200), ctx);
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(1);
|
||
expect(all[0].type).toBe('chair');
|
||
expect(all[0].x).toBe(100);
|
||
expect(all[0].y).toBe(200);
|
||
});
|
||
|
||
it('seating-block erzeugt rows×cols Elemente in EINER Transaktion (ein undo = ganzer Block weg)', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const block = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-block')!;
|
||
|
||
// E1: Drag-Interaktion (down + up) mit Distanz 200×100, spacing 50
|
||
block.handlers.down!(pe(0, 0), ctx);
|
||
block.handlers.up!(pe(200, 100), ctx); // 4 cols × 2 rows = 8 Stühle
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBeGreaterThan(1); // Multi-Element-Satz
|
||
expect(all.every((e) => e.type === 'chair')).toBe(true);
|
||
|
||
// BUG-1-Regressionsschutz: alle Reihen im Block haben eigene rowIds
|
||
const rowIds = new Set(all.map((e) => e.properties.rowId));
|
||
expect(rowIds.size).toBe(2); // Drag 100mm bei spacing 50 → 2 Reihen
|
||
|
||
// Kern-Akzeptanz: EIN undo entfernt den GANZEN Block
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('seating-row platziert mehrere Stühle mit gemeinsamer rowId; undo entfernt Reihe', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const row = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-row')!;
|
||
|
||
// E1: Drag-Interaktion (down + up statt nur down)
|
||
row.handlers.down!(pe(0, 0), ctx);
|
||
row.handlers.up!(pe(200, 0), ctx); // Länge 200, spacing 50 → 4 Stühle
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBeGreaterThanOrEqual(2);
|
||
const rowIds = new Set(all.map((e) => e.properties.rowId));
|
||
expect(rowIds.size).toBe(1); // eine Reihe → EINE rowId
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.6 pilot: move/copy tools', () => {
|
||
it('move verschiebt selektierte Elemente in EINER Transaktion (undo entfernt Versatz)', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
doc.addElement(makeEl('m1'));
|
||
doc.addElement(makeEl('m2'));
|
||
let selIds = ['m1', 'm2'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => {
|
||
selIds = ids;
|
||
},
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const move = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'move')!;
|
||
const origM1x = doc.getElement('m1')!.x;
|
||
const origY = doc.getElement('m1')!.y;
|
||
|
||
move.handlers.down!(pe(0, 0), ctx); // Basispunkt
|
||
move.handlers.down!(pe(50, -20), ctx); // Zielklick → Commit
|
||
|
||
expect(doc.getElement('m1')!.x).toBe(origM1x + 50);
|
||
expect(doc.getElement('m2')!.y).toBe(origY - 20);
|
||
// EINE Transaktion für beide → ein undo stellt beide wieder her:
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getElement('m1')!.x).toBe(origM1x);
|
||
expect(doc.getElement('m2')!.y).toBe(origY);
|
||
});
|
||
|
||
it('copy erzeugt neue Elemente (IDs neu) ohne Original zu veraendern', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
doc.addElement(makeEl('o1'));
|
||
let selIds = ['o1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => {
|
||
selIds = ids;
|
||
},
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const copy = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'copy')!;
|
||
|
||
copy.handlers.down!(pe(0, 0), ctx);
|
||
copy.handlers.down!(pe(100, 100), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(2); // Original + Kopie
|
||
expect(all[0].id).toBe('o1'); // Original unberührt
|
||
expect(all[0].x).not.toBe(all[1].x); // Kopie versetzt
|
||
expect(all[1].id).not.toBe('o1'); // neue ID
|
||
expect(all[1].type).toBe(all[0].type); // gleicher Typ
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1); // nur Original bleibt
|
||
});
|
||
|
||
it('Komfort: bei leerer Selektion wird Element unter Klick auto-gewählt', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const target = makeEl('auto');
|
||
target.x = 10;
|
||
target.y = 10;
|
||
doc.addElement(target);
|
||
let selIds2: string[] = [];
|
||
const bridge = {
|
||
getIds: () => selIds2,
|
||
setIds: (ids: string[]) => {
|
||
selIds2 = ids;
|
||
},
|
||
hitTest: () => target,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const move = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'move')!;
|
||
|
||
move.handlers.down!(pe(10, 10), ctx); // Basis + Auto-Select
|
||
expect(selIds2).toEqual(['auto']);
|
||
move.handlers.down!(pe(30, 30), ctx); // Zielklick (Δ=+20)
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('A4.10 pilot: geometry tools', () => {
|
||
function makeLineEl(id: string): CADElement {
|
||
return {
|
||
id,
|
||
type: 'line',
|
||
layerId: 'l',
|
||
x: 100,
|
||
y: 0,
|
||
width: 200,
|
||
height: 0,
|
||
properties: { x1: 0, y1: 0, x2: 200, y2: 0 },
|
||
} as unknown as CADElement;
|
||
}
|
||
|
||
function makeCircleEl(id: string): CADElement {
|
||
return {
|
||
id,
|
||
type: 'circle',
|
||
layerId: 'l',
|
||
x: 100,
|
||
y: 0,
|
||
width: 80,
|
||
height: 80,
|
||
properties: { radius: 40 },
|
||
} as unknown as CADElement;
|
||
}
|
||
|
||
it('trim: Kante+Ziel kürzt die Linie am Schnittpunkt; undo stellt her', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const boundary = makeCircleEl('bnd');
|
||
const target = makeLineEl('tgt');
|
||
doc.addElement(boundary);
|
||
doc.addElement(target);
|
||
|
||
// Sequenzieller Brücken-HitTest: 1. Klick → boundary, 2. Klick → target
|
||
const pickQueue = [boundary, target];
|
||
let pickIdx = 0;
|
||
const bridge = {
|
||
getIds: () => [] as string[],
|
||
setIds: () => {},
|
||
hitTest: () => pickQueue[Math.min(pickIdx++, pickQueue.length - 1)],
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const trim = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'trim')!;
|
||
|
||
trim.handlers.down!(pe(140, 0), ctx); // Kante
|
||
trim.handlers.down!(pe(180, 0), ctx); // Ziel-Linie rechts vom Kreis
|
||
|
||
const trimmed = doc.getElement('tgt')!;
|
||
expect(trimmed.properties.x2).toBeLessThan(200); // gekürzt
|
||
// Legacy-Semantik: das NÄHERE Ende wird auf den ERSTEN Schnittpunkt
|
||
// gesetzt (findIntersection wählt den nächsten zu p1 → x=60):
|
||
expect(Math.abs(trimmed.properties.x2 as number)).toBeCloseTo(60); // Schnitt bei x=140
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getElement('tgt')!.properties.x2).toBe(200);
|
||
});
|
||
|
||
it('extend: Linie wird zur Kante hin verlängert', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
// kurze Linie (10→50); Kreis (100,0,r=40) hat Schnitte bei x=60/140
|
||
// → Extend muss das nahe Ende x2 auf 60 strecken
|
||
const boundary = makeCircleEl('bnd');
|
||
const shortLine = {
|
||
id: 'sl', type: 'line', layerId: 'l', x: 30, y: 0,
|
||
width: 40, height: 0,
|
||
properties: { x1: 10, y1: 0, x2: 50, y2: 0 },
|
||
} as unknown as CADElement;
|
||
doc.addElement(boundary);
|
||
doc.addElement(shortLine);
|
||
|
||
const pickQueue = [boundary, shortLine];
|
||
let pickIdx = 0;
|
||
const bridge = {
|
||
getIds: () => [] as string[],
|
||
setIds: () => {},
|
||
hitTest: () => pickQueue[Math.min(pickIdx++, pickQueue.length - 1)],
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const extend = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'extend')!;
|
||
|
||
extend.handlers.down!(pe(100, 40), ctx);
|
||
extend.handlers.down!(pe(45, 0), ctx);
|
||
|
||
const ext = doc.getElement('sl')!;
|
||
// Extend richtet das nahe Ende exakt auf den Schnittpunkt aus:
|
||
expect(ext.properties.x2).toBeCloseTo(60);
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getElement('sl')!.properties.x2).toBe(50); // Original-Länge
|
||
});
|
||
});
|
||
|
||
describe('A4.7 pilot: transform tools', () => {
|
||
it('rotate dreht um Pivot des ersten Elements; ein undo stellt zurück', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const rotEl = makeEl('r1');
|
||
doc.addElement(rotEl);
|
||
let selIds: string[] = ['r1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const rotate = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'rotate')!;
|
||
|
||
// Basispunkt rechts vom Zentrum (0°-Richtung), Element x=10
|
||
rotate.handlers.down!(pe(60, 10), ctx);
|
||
// Referenzklick oben → Δwinkel ≈ +90° (CCW)
|
||
rotate.handlers.down!(pe(10, 60), ctx);
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1);
|
||
});
|
||
|
||
it('scale vergrößert width/height um Faktor dist(pivot,zweiteKlick)/dist(pivot,basis)', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const sq = makeEl('s1');
|
||
sq.width = 40;
|
||
sq.height = 40;
|
||
sq.x = 0;
|
||
sq.y = 0;
|
||
doc.addElement(sq);
|
||
let selIds: string[] = ['s1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const scale = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'scale')!;
|
||
|
||
// Basis bei Distanz 50 vom Pivot, Ziel bei 100 → Faktor 2
|
||
scale.handlers.down!(pe(50, 0), ctx);
|
||
scale.handlers.down!(pe(100, 0), ctx);
|
||
|
||
expect(doc.getElement('s1')!.width).toBeCloseTo(80); // 40×2
|
||
expect(doc.getElement('s1')!.height).toBeCloseTo(80);
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getElement('s1')!.width).toBeCloseTo(40);
|
||
});
|
||
|
||
it('mirror spiegelt Element an Achse basis→referenz', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const el = makeEl('mi');
|
||
el.x = 30;
|
||
el.y = 0;
|
||
doc.addElement(el);
|
||
let selIds: string[] = ['mi'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const mirror = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'mirror')!;
|
||
|
||
// Spiegelachse = Y-Achse (basis (0,0)→ref (0,100)): Punkt (30,0)→(−30,0)
|
||
mirror.handlers.down!(pe(0, 0), ctx);
|
||
mirror.handlers.down!(pe(0, 100), ctx);
|
||
|
||
const mirrored = doc.getElement('mi')!;
|
||
expect(mirrored.x).toBeLessThan(0); // jenseits der Achse
|
||
expect(Math.abs(mirrored.x)).toBeGreaterThanOrEqual(29); // ~spiegelsymmetrisch
|
||
expect(doc.undo()).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('A4.8 pilot: delete tool', () => {
|
||
it('löscht alle selektierten Elemente in EINER Transaktion; undo stellt her', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
doc.addElement(makeEl('d1'));
|
||
doc.addElement(makeEl('d2'));
|
||
doc.addElement(makeEl('keep'));
|
||
let selIds: string[] = ['d1', 'd2'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const del = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'delete')!;
|
||
|
||
del.handlers.down!(pe(0, 0), ctx);
|
||
const remaining = doc.getAllElements().map((e) => e.id);
|
||
expect(remaining).toEqual(['keep']);
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(3);
|
||
});
|
||
|
||
it('Direktklick ohne Selektion löscht getroffenes Element', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const el = makeEl('hitme');
|
||
el.x = 20;
|
||
el.y = 20;
|
||
doc.addElement(el);
|
||
const bridge = {
|
||
getIds: () => [] as string[],
|
||
setIds: () => {},
|
||
hitTest: () => el,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const del = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'delete')!;
|
||
|
||
del.handlers.down!(pe(20, 20), ctx);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('A4.8 pilot: offset tool', () => {
|
||
it('erzeugt Versatz-Kopie mit Seitenparameter; Original bleibt', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const line = {
|
||
id: 'src1', type: 'line', layerId: 'l', x: 50, y: 0,
|
||
width: 100, height: 0,
|
||
properties: { x1: 0, y1: 0, x2: 100, y2: 0 },
|
||
} as unknown as CADElement;
|
||
doc.addElement(line);
|
||
let selIds: string[] = [];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => line,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const off = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'offset')!;
|
||
|
||
off.handlers.down!(pe(50, 0), ctx); // Klick1: Quelle wählen (Basis)
|
||
off.handlers.down!(pe(50, -30), ctx); // Klick2: Versatz nach oben (side=-1)
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(2); // Original + Kopie
|
||
expect(all[0].id).toBe('src1'); // Original unberührt
|
||
expect(all[1].id.startsWith('off_')).toBe(true);
|
||
// side=-1 bei Rechtslaufender Linie → nach OBEN (y kleiner):
|
||
expect(all[1].properties.y1).toBe(-30);
|
||
expect(all[1].properties.y2).toBe(-30);
|
||
expect(all[1].properties.x1).toBe(0); // x bleibt
|
||
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('A3 pilot: select tool', () => {
|
||
function makeBridge(elements: CADElement[]) {
|
||
let ids: string[] = [];
|
||
return {
|
||
bridge: {
|
||
getIds: () => ids,
|
||
setIds: (next: string[]) => {
|
||
ids = next;
|
||
},
|
||
hitTest: (wx: number, wy: number, tol: number): CADElement | null =>
|
||
elements.find((el) => Math.abs(el.x - wx) <= tol && Math.abs(el.y - wy) <= tol) ?? null,
|
||
},
|
||
getIds: () => ids,
|
||
};
|
||
}
|
||
|
||
it('wählt getroffenes Element ohne Modifier exklusiv', () => {
|
||
const els = [makeEl('a'), makeEl('b')];
|
||
els[0].x = 10; els[0].y = 10;
|
||
els[1].x = 500; els[1].y = 500;
|
||
const { bridge, getIds } = makeBridge(els);
|
||
const ctx = makeCtx({ selection: bridge });
|
||
|
||
selectTool.handlers.down!(pe(10, 10), ctx);
|
||
expect(getIds()).toEqual(['a']);
|
||
|
||
// Klick auf b ohne Shift ersetzt Selektion
|
||
selectTool.handlers.down!(pe(500, 500), ctx);
|
||
expect(getIds()).toEqual(['b']);
|
||
});
|
||
|
||
it('Shift addiert, Ctrl subtrahiert', () => {
|
||
const els = [makeEl('a'), makeEl('b'), makeEl('c')];
|
||
els[0].x = 10; els[0].y = 10;
|
||
els[1].x = 50; els[1].y = 50;
|
||
els[2].x = 90; els[2].y = 90;
|
||
const { bridge, getIds } = makeBridge(els);
|
||
const ctx = makeCtx({ selection: bridge });
|
||
|
||
selectTool.handlers.down!(pe(10, 10), ctx);
|
||
expect(getIds()).toEqual(['a']);
|
||
|
||
selectTool.handlers.down!(pe(50, 50, { shift: true }), ctx);
|
||
expect(getIds().sort()).toEqual(['a', 'b']);
|
||
|
||
selectTool.handlers.down!(pe(90, 90, { shift: true }), ctx);
|
||
expect(getIds().sort()).toEqual(['a', 'b', 'c']);
|
||
|
||
selectTool.handlers.down!(pe(50, 50, { ctrl: true }), ctx);
|
||
expect(getIds().sort()).toEqual(['a', 'c']);
|
||
});
|
||
|
||
it('Leerklick ohne Modifier leert die Selektion; Plugin tagged basic', () => {
|
||
const els = [makeEl('a')];
|
||
els[0].x = 10; els[0].y = 10;
|
||
const { bridge, getIds } = makeBridge(els);
|
||
const ctx = makeCtx({ selection: bridge });
|
||
|
||
selectTool.handlers.down!(pe(10, 10), ctx);
|
||
expect(getIds()).toEqual(['a']);
|
||
selectTool.handlers.down!(pe(999, 999), ctx);
|
||
expect(getIds()).toEqual([]);
|
||
|
||
expect(coreModifyPlugin.manifest.enabledByDefault).toBe(true);
|
||
const sel = coreModifyPlugin.tools?.find((t) => t.manifest.id === 'select');
|
||
expect(sel?.manifest.tags).toContain('basic');
|
||
});
|
||
});
|
||
|
||
describe('D-Ext: array-rect tool', () => {
|
||
it('vervielfaeltigt Selektion rows×cols in EINER Transaktion', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
doc.addElement(makeEl('a1'));
|
||
let selIds: string[] = ['a1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge, options: { rows: 2, cols: 2, dx: 60, dy: 60 } });
|
||
const arr = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'array-rect')!;
|
||
|
||
arr.handlers.down!(pe(0, 0), ctx);
|
||
arr.handlers.down!(pe(0, 0), ctx);
|
||
|
||
expect(doc.getAllElements().length).toBe(4);
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('D-Ext: array-polar tool', () => {
|
||
it('verteilt count Kopien gleichmaessig um das Zentrum', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const elem = makeEl('p1');
|
||
elem.x = 100;
|
||
elem.y = 0;
|
||
doc.addElement(elem);
|
||
let selIds: string[] = ['p1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge, options: { count: 4 } });
|
||
const polar = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'array-polar')!;
|
||
|
||
polar.handlers.down!(pe(0, 0), ctx);
|
||
polar.handlers.down!(pe(0, 0), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(4);
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('C3fin interaktiv: handle-drag on select tool', () => {
|
||
function makeRect(id: string, x: number, y: number, w: number, h: number): CADElement {
|
||
return {
|
||
id, type: 'rect', layerId: 'l',
|
||
x: x + w / 2, y: y + h / 2,
|
||
width: w, height: h,
|
||
properties: { fill: '#abc' },
|
||
} as unknown as CADElement;
|
||
}
|
||
|
||
it('down auf Handle startet Drag, move skaliert, up committet', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
// Rect: (0,0) bis (50,50)
|
||
doc.addElement(makeRect('r1', 0, 0, 50, 50));
|
||
let selIds: string[] = ['r1'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
hitTest: () => null,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const select = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'select')!;
|
||
|
||
// down auf Handle der oberen linken Ecke (0,0):
|
||
select.handlers.down!(pe(0, 0), ctx);
|
||
// move zu neuem Punkt (-20, -20):
|
||
select.handlers.move!(pe(-20, -20), ctx);
|
||
// up → Commit (Skalierung auf neue BBox von (-20,-20) bis (50,50)):
|
||
select.handlers.up!(pe(-20, -20), ctx);
|
||
|
||
const updated = doc.getElement('r1')!;
|
||
// Neue BBox: 70×70 statt 50×50:
|
||
expect(updated.width).toBe(70);
|
||
expect(updated.height).toBe(70);
|
||
expect(doc.undo()).toBe(true); // Skalierung ist undo-bar
|
||
expect(doc.getElement('r1')!.width).toBe(50);
|
||
});
|
||
|
||
it('down auf NICHT-Handle startet normale Selektion (kein Drag)', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
doc.addElement(makeRect('r2', 0, 0, 50, 50));
|
||
doc.addElement(makeRect('r3', 200, 200, 30, 30));
|
||
let selIds: string[] = ['r2'];
|
||
const bridge = {
|
||
getIds: () => selIds,
|
||
setIds: (ids: string[]) => { selIds = ids; },
|
||
// Klick bei (210,210) trifft r3:
|
||
hitTest: () => doc.getElement('r3')!,
|
||
};
|
||
const ctx = makeCtx({ doc, selection: bridge });
|
||
const select = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'select')!;
|
||
|
||
// Klick weit weg von Handles von r2 (r2-Ecken sind 0/50):
|
||
select.handlers.down!(pe(210, 210), ctx);
|
||
|
||
// Normal-Selektion: r3 ist jetzt alleine selektiert (kein Drag gestartet):
|
||
expect(selIds).toEqual(['r3']);
|
||
});
|
||
});
|
||
|
||
describe('E1: seating-row drag', () => {
|
||
it('berechnet count aus Laenge und committet in EINER Transaktion', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc, options: { spacing: 50 } });
|
||
const row = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-row')!;
|
||
|
||
// Start bei (0,0), Ende bei (300,0): Länge 300, spacing 50 → 6 Stühle
|
||
row.handlers.down!(pe(0, 0), ctx);
|
||
row.handlers.move!(pe(150, 0), ctx); // Live-Preview
|
||
expect(ctx.previews.length).toBe(1);
|
||
row.handlers.up!(pe(300, 0), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(6);
|
||
expect(all.every((e) => e.type === 'chair')).toBe(true);
|
||
// Eine rowId für alle:
|
||
const rowIds = new Set(all.map((e) => e.properties.rowId));
|
||
expect(rowIds.size).toBe(1);
|
||
|
||
// EIN Undo entfernt die ganze Reihe:
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
|
||
it('cancel verwirft die laufende Reihe', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc });
|
||
const row = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-row')!;
|
||
|
||
row.handlers.down!(pe(0, 0), ctx);
|
||
row.handlers.cancel!(ctx);
|
||
row.handlers.up!(pe(100, 0), ctx); // nach cancel: kein dragStart
|
||
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('E1: seating-block drag', () => {
|
||
it('berechnet rows×cols aus Distanz und committet Multi-Element-Set', () => {
|
||
const { ydoc } = createInMemoryDoc();
|
||
const doc = new CADDocument(ydoc);
|
||
const ctx = makeCtx({ doc, options: { colSpacing: 50, rowSpacing: 50 } });
|
||
const block = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-block')!;
|
||
|
||
// Start (0,0), Ende (150,100): w=150/50=3 cols, h=100/50=2 rows → 6 Stühle
|
||
block.handlers.down!(pe(0, 0), ctx);
|
||
block.handlers.up!(pe(150, 100), ctx);
|
||
|
||
const all = doc.getAllElements();
|
||
expect(all.length).toBe(6);
|
||
expect(all.every((e) => e.type === 'chair')).toBe(true);
|
||
|
||
// BUG-1-Regressionsschutz: rowIds pro Reihe eindeutig
|
||
const rowIds = new Set(all.map((e) => e.properties.rowId));
|
||
expect(rowIds.size).toBe(2);
|
||
|
||
// EIN Undo entfernt den ganzen Block:
|
||
expect(doc.undo()).toBe(true);
|
||
expect(doc.getAllElements().length).toBe(0);
|
||
});
|
||
});
|