Files
web-cad/frontend/tests/pilotTools.test.ts
T
2026-08-27 01:09:51 +02:00

664 lines
24 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.
/**
* 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 { coreMeasurePlugin } from '../src/plugins/builtin/core-measure';
import { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate';
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.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.getElement('auto')!.x).toBe(30); // 10+Δ20
});
});
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');
});
});