task(A4.8): migrate delete/offset tools to v2
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
mirrorElement,
|
mirrorElement,
|
||||||
angleBetween,
|
angleBetween,
|
||||||
distance,
|
distance,
|
||||||
|
offsetElement,
|
||||||
} from '../../../tools/modification/geometry';
|
} from '../../../tools/modification/geometry';
|
||||||
import type { Pt } from '../../../tools/modification/geometry';
|
import type { Pt } from '../../../tools/modification/geometry';
|
||||||
|
|
||||||
@@ -273,16 +274,106 @@ export const rotateTool: ToolExtensionV2 = makeTransformTool('rotate');
|
|||||||
export const scaleTool: ToolExtensionV2 = makeTransformTool('scale');
|
export const scaleTool: ToolExtensionV2 = makeTransformTool('scale');
|
||||||
export const mirrorTool: ToolExtensionV2 = makeTransformTool('mirror');
|
export const mirrorTool: ToolExtensionV2 = makeTransformTool('mirror');
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Delete (Task A4.8) — Portierung aus Legacy handleDeleteDown:
|
||||||
|
// Einzelnklick auf Element löscht es sofort (kein up/Bestätigung).
|
||||||
|
// Jetzt via doc.deleteElements → undo-fähig.
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
export const deleteTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'delete', label: 'Löschen', icon: '\u2715', ribbonTab: 'canvas', tags: ['basic'],
|
||||||
|
description: 'Element anklicken zum Löschen',
|
||||||
|
},
|
||||||
|
optionsSchema: [],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc || !c.selection) return;
|
||||||
|
const ids = c.selection.getIds();
|
||||||
|
if (ids.length > 0) {
|
||||||
|
// Selektion vorhanden → alles selektierte löschen
|
||||||
|
c.doc.deleteElements(ids);
|
||||||
|
c.selection.setIds([]);
|
||||||
|
ctx.setStatus(`${ids.length} Element(e) gelöscht`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sonst: direkter Klick-Treffer
|
||||||
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
if (!hit) return;
|
||||||
|
c.doc.deleteElements([hit.id]);
|
||||||
|
ctx.setStatus(`Gelöscht: ${hit.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Offset (Task A4.8) — nutzt die BUG-3-reparierte offsetElement-
|
||||||
|
// Signatur mit Seitenparameter; Legacy-Ablauf:
|
||||||
|
// Klick1 = Element wählen, Klick2 = Richtung+Distanz → NEUES Element.
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
let offsetSource: CADElement | null = null;
|
||||||
|
let offsetBase: Pt | null = null;
|
||||||
|
|
||||||
|
export const offsetTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'offset', label: 'Versatz', icon: '\u2937', ribbonTab: 'canvas', tags: ['pro'],
|
||||||
|
description: 'Element anklicken, dann seitlichen Versatz klicken',
|
||||||
|
},
|
||||||
|
optionsSchema: [],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc || !c.selection) return;
|
||||||
|
if (!offsetSource) {
|
||||||
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
if (!hit) return;
|
||||||
|
offsetSource = hit;
|
||||||
|
offsetBase = e.world;
|
||||||
|
ctx.setStatus(`Offset: Element ${hit.id} gewählt – Versatz klicken`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Zweiter Klick: Distanz + Seite (Kreuzprodukt für Linien, Radius sonst)
|
||||||
|
const src = c.doc.getElement(offsetSource.id);
|
||||||
|
if (!src) { offsetSource = null; offsetBase = null; return; }
|
||||||
|
const dist = Math.hypot(e.world.x - offsetBase!.x, e.world.y - offsetBase!.y);
|
||||||
|
let side: 1 | -1 = 1;
|
||||||
|
if (src.type === 'line' && src.properties.x1 !== undefined && src.properties.y1 !== undefined) {
|
||||||
|
const p1x = src.properties.x1 as number;
|
||||||
|
const p1y = src.properties.y1 as number;
|
||||||
|
const p2x = src.properties.x2 as number;
|
||||||
|
const p2y = src.properties.y2 as number;
|
||||||
|
side = (p2x - p1x) * (e.world.y - p1y) - (p2y - p1y) * (e.world.x - p1x) >= 0 ? 1 : -1;
|
||||||
|
} else if ((src.type === 'circle' || src.type === 'arc') && src.properties.radius !== undefined) {
|
||||||
|
side = Math.hypot(e.world.x - src.x, e.world.y - src.y) >= (src.properties.radius as number) ? 1 : -1;
|
||||||
|
}
|
||||||
|
const next = offsetElement(src, dist, side);
|
||||||
|
let newEl: CADElement | null = null;
|
||||||
|
c.doc.transact(() => {
|
||||||
|
newEl = { ...next, id: uid('off') };
|
||||||
|
c.doc!.addElement(newEl!);
|
||||||
|
});
|
||||||
|
ctx.setStatus(`Offset erstellt (${(newEl as unknown as CADElement).id})`);
|
||||||
|
offsetSource = null;
|
||||||
|
offsetBase = null;
|
||||||
|
},
|
||||||
|
cancel(ctx) {
|
||||||
|
offsetSource = null;
|
||||||
|
offsetBase = null;
|
||||||
|
ctx.setStatus('Offset abgebrochen');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/** V2-Plugin: Kern-Bearbeitungswerkzeuge. */
|
/** V2-Plugin: Kern-Bearbeitungswerkzeuge. */
|
||||||
export const coreModifyPlugin: PluginV2 = {
|
export const coreModifyPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-modify',
|
id: 'core-modify',
|
||||||
name: 'Bearbeiten (Kern)',
|
name: 'Bearbeiten (Kern)',
|
||||||
version: '1.3.0',
|
version: '1.4.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Auswahl, Schraffur, Verschieben, Kopieren, Rotieren, Skalieren, Spiegeln (V2).',
|
description: 'Auswahl, Schraffur, Verschieben, Kopieren, Rotieren, Skalieren, Spiegeln, Löschen, Versatz (V2).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool],
|
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -513,6 +513,86 @@ describe('A4.7 pilot: transform tools', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
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