2026-08-26 21:30:54 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* core-modify – Built-in V2-Plugin (Task A3 Pilot).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Werkzeuge: select (Pilotportierung aus Legacy handleSelectDown).
|
|
|
|
|
|
* Pilot-Scope: Klick-Auswahl inkl. Group-Bewusstheit via SelectionBridge;
|
|
|
|
|
|
* Marquee/Box-Auswahl Migration folgt in A4 (Legacy bleibt parallel aktiv).
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type { PluginV2, ToolExtensionV2 } from '../../types';
|
2026-08-27 00:53:16 +02:00
|
|
|
|
import type { CADElement } from '../../../types/cad.types';
|
2026-08-26 21:30:54 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* select-Werkzeug: Klick wählt Element direkt (Shift=additiv, Ctrl=subtraktiv).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const selectTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'select',
|
|
|
|
|
|
label: 'Auswahl',
|
|
|
|
|
|
icon: '\u2196',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Elemente anklicken zum Auswählen',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
const bridge = (ctx as import('../../types').FullToolContext).selection;
|
|
|
|
|
|
if (!bridge) return;
|
|
|
|
|
|
|
|
|
|
|
|
const additive = e.shift;
|
|
|
|
|
|
const subtractive = e.ctrl;
|
|
|
|
|
|
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
|
|
|
|
|
|
|
|
|
|
|
if (!hit) {
|
|
|
|
|
|
// Leerer Klick: Selektion leeren (außer additiv)
|
|
|
|
|
|
if (!additive && !subtractive) bridge.setIds([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let ids = new Set(bridge.getIds());
|
|
|
|
|
|
if (subtractive) {
|
|
|
|
|
|
ids.delete(hit.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ids.add(hit.id);
|
|
|
|
|
|
if (!additive) ids = new Set([hit.id]); // ohne Shift: nur das getroffene
|
|
|
|
|
|
}
|
|
|
|
|
|
bridge.setIds(Array.from(ids));
|
|
|
|
|
|
ctx.setStatus(`Selektion: ${ids.size} Element(e)`);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-27 00:48:28 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* hatch-Werkzeug (A4.5): Klick auf geschlossenes Element (rect/circle/polygon)
|
|
|
|
|
|
* aktiviert Schraffur — Portierung aus Legacy handleHatchDown, aber jetzt via
|
|
|
|
|
|
* doc.updateElement (eigene Undo-Transaktion statt Callback-Direktaufruf).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const hatchTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'hatch', label: 'Schraffur', icon: '\u2593', ribbonTab: 'format', tags: ['pro'],
|
|
|
|
|
|
description: 'Geschlossenes Element anklicken zum Schraffieren',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [
|
|
|
|
|
|
{ key: 'hatchPattern', label: 'Muster', type: 'select', options: [
|
|
|
|
|
|
{ value: 'lines', label: 'Linien' },
|
|
|
|
|
|
{ value: 'cross', label: 'Kreuz' },
|
|
|
|
|
|
] },
|
|
|
|
|
|
{ key: 'hatchSpacing', label: 'Abstand', type: 'number', min: 2, max: 40 },
|
|
|
|
|
|
],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
const c = ctx as import('../../types').FullToolContext;
|
|
|
|
|
|
const bridge = c.selection;
|
|
|
|
|
|
if (!bridge || !c.doc) return;
|
|
|
|
|
|
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
|
|
|
|
|
if (!hit || !['rect', 'circle', 'polygon'].includes(hit.type)) {
|
|
|
|
|
|
ctx.setStatus('Schraffur: nur rect/circle/polygon');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const pattern = (c.options.hatchPattern as string | undefined) ?? 'lines';
|
|
|
|
|
|
const spacing = (c.options.hatchSpacing as number | undefined) ?? 8;
|
|
|
|
|
|
c.doc.updateElement(hit.id, {
|
|
|
|
|
|
properties: { hatch: true, hatchPattern: pattern, hatchSpacing: spacing },
|
|
|
|
|
|
});
|
|
|
|
|
|
ctx.setStatus(`Schraffur auf ${hit.id} (${pattern})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-27 00:53:16 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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). */
|
2026-08-26 21:30:54 +02:00
|
|
|
|
export const coreModifyPlugin: PluginV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'core-modify',
|
|
|
|
|
|
name: 'Bearbeiten (Kern)',
|
2026-08-27 00:53:16 +02:00
|
|
|
|
version: '1.2.0',
|
2026-08-26 21:30:54 +02:00
|
|
|
|
author: 'web-cad team',
|
2026-08-27 00:53:16 +02:00
|
|
|
|
description: 'Auswahl, Schraffur, Verschieben, Kopieren (V2).',
|
2026-08-26 21:30:54 +02:00
|
|
|
|
category: 'tools',
|
|
|
|
|
|
enabledByDefault: true,
|
|
|
|
|
|
},
|
2026-08-27 00:53:16 +02:00
|
|
|
|
tools: [selectTool, hatchTool, moveTool, copyTool],
|
2026-08-26 21:30:54 +02:00
|
|
|
|
};
|