task(A4.6): migrate move/copy tools to v2 with selection-based workflow
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
* Marquee/Box-Auswahl Migration folgt in A4 (Legacy bleibt parallel aktiv).
|
* Marquee/Box-Auswahl Migration folgt in A4 (Legacy bleibt parallel aktiv).
|
||||||
*/
|
*/
|
||||||
import type { PluginV2, ToolExtensionV2 } from '../../types';
|
import type { PluginV2, ToolExtensionV2 } from '../../types';
|
||||||
|
import type { CADElement } from '../../../types/cad.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* select-Werkzeug: Klick wählt Element direkt (Shift=additiv, Ctrl=subtraktiv).
|
* select-Werkzeug: Klick wählt Element direkt (Shift=additiv, Ctrl=subtraktiv).
|
||||||
@@ -85,16 +86,98 @@ export const hatchTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** V2-Plugin: Kern-Bearbeitungswerkzeuge (Pilot: select; A4.5: hatch). */
|
/**
|
||||||
|
* Move/Copy-Basispunkt (A4.6): erster down setzt ihn, zweiter down committet.
|
||||||
|
* Gemeinsam für beide Tools (klick-progressiv wie arc-Lektion).
|
||||||
|
*/
|
||||||
|
let modBasePoint: import('../../../tools/modification/geometry').Pt | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fabrik für move/copy-Werkzeuge (A4.6).
|
||||||
|
* Wirkt auf die AKTUELLE SELEKTION (SelectionBridge):
|
||||||
|
* - Klick 1: Basispunkt setzen
|
||||||
|
* - Klick 2: Commit — move: updateElement(x±dx,y±dy); copy: addElement der
|
||||||
|
* versetzten Kopien (neue IDs). Alles in EINER Transaktion = 1 Undo-Schritt.
|
||||||
|
* Legacy-Konvention: Versatzvektor = zweiterKlick − basis.
|
||||||
|
*/
|
||||||
|
function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
||||||
|
const label = mode === 'move' ? 'Verschieben' : 'Kopieren';
|
||||||
|
return {
|
||||||
|
manifest: {
|
||||||
|
id: mode,
|
||||||
|
label,
|
||||||
|
icon: mode === 'move' ? '\u27a4' : '\u29c9',
|
||||||
|
ribbonTab: 'canvas',
|
||||||
|
tags: ['basic'],
|
||||||
|
description: `${label}: Auswahl treffen, dann Basis→Ziel klicken`,
|
||||||
|
},
|
||||||
|
optionsSchema: [],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as import('../../types').FullToolContext;
|
||||||
|
if (!c.doc || !c.selection) return;
|
||||||
|
|
||||||
|
if (!modBasePoint) {
|
||||||
|
const ids = c.selection.getIds();
|
||||||
|
if (ids.length === 0) {
|
||||||
|
// Komfort: nichts selektiert → hitTest und sofort auswählen
|
||||||
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
if (!hit) {
|
||||||
|
ctx.setStatus(`${label}: zuerst Element(e) auswählen`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.selection.setIds([hit.id]);
|
||||||
|
}
|
||||||
|
modBasePoint = e.world;
|
||||||
|
ctx.setStatus(`${label}: Zielklick zum ${mode === 'move' ? 'Verschieben' : 'Kopieren'} (${c.selection.getIds().length} Elemente)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zweiter Klick → Commit
|
||||||
|
const dx = e.world.x - modBasePoint.x;
|
||||||
|
const dy = e.world.y - modBasePoint.y;
|
||||||
|
const ids = c.selection.getIds();
|
||||||
|
c.doc.transact(() => {
|
||||||
|
for (const id of ids) {
|
||||||
|
const el = c.doc!.getElement(id);
|
||||||
|
if (!el) continue;
|
||||||
|
if (mode === 'move') {
|
||||||
|
c.doc!.updateElement(id, { x: el.x + dx, y: el.y + dy });
|
||||||
|
} else {
|
||||||
|
const copy: CADElement = {
|
||||||
|
...el,
|
||||||
|
id: `cp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}_${ids.indexOf(id)}`,
|
||||||
|
x: el.x + dx,
|
||||||
|
y: el.y + dy,
|
||||||
|
};
|
||||||
|
c.doc!.addElement(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.setStatus(`${label}: ${ids.length} Element(e), \u0394(${dx.toFixed(0)}, ${dy.toFixed(0)})`);
|
||||||
|
modBasePoint = null;
|
||||||
|
},
|
||||||
|
cancel(ctx) {
|
||||||
|
modBasePoint = null;
|
||||||
|
ctx.setStatus(`${label} abgebrochen`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moveTool: ToolExtensionV2 = makeMoveCopyTool('move');
|
||||||
|
export const copyTool: ToolExtensionV2 = makeMoveCopyTool('copy');
|
||||||
|
|
||||||
|
/** V2-Plugin: Kern-Bearbeitungswerkzeuge (select, hatch; A4.6: move/copy). */
|
||||||
export const coreModifyPlugin: PluginV2 = {
|
export const coreModifyPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-modify',
|
id: 'core-modify',
|
||||||
name: 'Bearbeiten (Kern)',
|
name: 'Bearbeiten (Kern)',
|
||||||
version: '1.1.0',
|
version: '1.2.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Auswahl und Bearbeitung (V2: Selektion, Schraffur).',
|
description: 'Auswahl, Schraffur, Verschieben, Kopieren (V2).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [selectTool, hatchTool],
|
tools: [selectTool, hatchTool, moveTool, copyTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -348,6 +348,91 @@ describe('A4.5 pilot: hatch tool', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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('A3 pilot: select tool', () => {
|
describe('A3 pilot: select tool', () => {
|
||||||
function makeBridge(elements: CADElement[]) {
|
function makeBridge(elements: CADElement[]) {
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user