task(D-ext3): join/explode/align tools for v2

This commit is contained in:
Agent Zero
2026-08-27 20:03:14 +02:00
parent 9aed0e2163
commit b470ac3346
@@ -439,6 +439,139 @@ 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');
// ─────────────────────────────────────────────────────────────
// Join / Explode / Align (Task D-Extensions):
// - join: 2+ ausgewählte Linien → eine Polyline (Transaktion)
// - explode: Polyline → einzelne Liniensegmente (Transaktion)
// - align: bewegt ein Element so, dass sein Anker (Zentrum) auf das
// Ziel-Element-Zentrum ausgerichtet wird (3 Klicks: Quelle, Ziel, Commit)
// ─────────────────────────────────────────────────────────────
let alignSourceId: string | null = null;
function makeJoinExplodeAlignTool(mode: 'join' | 'explode' | 'align'): ToolExtensionV2 {
const labels = { join: 'Join', explode: 'Explode', align: 'Align' } as const;
return {
manifest: {
id: mode,
label: labels[mode],
icon: mode === 'join' ? '\u2795' : mode === 'explode' ? '\u2737' : '\u21c4',
ribbonTab: 'canvas',
tags: ['pro'],
description: `${labels[mode]}: siehe Statuszeile`,
},
optionsSchema: [],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc || !c.selection) return;
const ids = c.selection.getIds();
if (mode === 'join') {
// Alle selektierten Linien → eine Polyline:
const lines = ids
.map((id) => c.doc!.getElement(id))
.filter((el): el is CADElement => !!el && el.type === 'line');
if (lines.length < 2) {
ctx.setStatus('Join: mindestens 2 Linien auswählen');
return;
}
// Punkte der Linien in Reihenfolge sammeln:
const allPts: Pt[] = [];
for (const ln of lines) {
const props = ln.properties as Record<string, unknown>;
if (props.x1 === undefined) continue;
allPts.push({ x: props.x1 as number, y: props.y1 as number });
allPts.push({ x: props.x2 as number, y: props.y2 as number });
}
if (allPts.length < 4) return;
// Erste und letzte Koordinate als Start/Ende nehmen:
const first = allPts[0];
const last = allPts[allPts.length - 1];
const joined: CADElement = {
...lines[0],
id: uid('pl'),
type: 'polyline',
x: (first.x + last.x) / 2,
y: (first.y + last.y) / 2,
width: Math.abs(last.x - first.x),
height: Math.abs(last.y - first.y),
properties: { points: allPts, stroke: '#e0e0e0', strokeWidth: 1 },
};
c.doc.transact(() => {
for (const ln of lines) c.doc!.deleteElements([ln.id]);
c.doc!.addElement(joined);
});
ctx.setStatus(`Join: ${lines.length} Linien → 1 Polyline`);
return;
}
if (mode === 'explode') {
// Polyline → einzelne Liniensegmente:
const polys = ids
.map((id) => c.doc!.getElement(id))
.filter((el): el is CADElement => !!el && el.type === 'polyline');
if (polys.length === 0) {
ctx.setStatus('Explode: mindestens eine Polyline auswählen');
return;
}
c.doc.transact(() => {
for (const poly of polys) {
const props = poly.properties as Record<string, unknown>;
const pts = Array.isArray(props.points)
? (props.points as unknown[]).map((p) => p as Pt)
: [];
for (let i = 0; i < pts.length - 1; i++) {
c.doc!.addElement({
...poly,
id: uid('ln'),
type: 'line',
x: (pts[i].x + pts[i + 1].x) / 2,
y: (pts[i].y + pts[i + 1].y) / 2,
width: Math.abs(pts[i + 1].x - pts[i].x),
height: Math.abs(pts[i + 1].y - pts[i].y),
properties: { x1: pts[i].x, y1: pts[i].y, x2: pts[i + 1].x, y2: pts[i + 1].y, stroke: '#e0e0e0', strokeWidth: 1 },
});
}
c.doc!.deleteElements([poly.id]);
}
});
ctx.setStatus(`Explode: ${polys.length} Polyline(n) in Segmente zerlegt`);
return;
}
// align: Klick 1 = Quelle (Element), Klick 2 = Ziel-Position
if (mode === 'align') {
if (ids.length === 0) {
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
if (!hit) return;
c.selection.setIds([hit.id]);
alignSourceId = hit.id;
ctx.setStatus('Align: Ziel-Position klicken');
return;
}
const el = c.doc.getElement(ids[0]);
if (!el) return;
const cx = el.x;
const cy = el.y;
c.doc.transact(() => {
c.doc!.updateElement(el.id, { x: e.world.x, y: e.world.y });
});
ctx.setStatus(`Align: ${el.id} nach (${e.world.x.toFixed(0)}, ${e.world.y.toFixed(0)}) verschoben`);
alignSourceId = null;
return;
}
},
cancel(ctx) {
alignSourceId = null;
ctx.setStatus(`${labels[mode]} abgebrochen`);
},
},
};
}
export const joinTool: ToolExtensionV2 = makeJoinExplodeAlignTool('join');
export const explodeTool: ToolExtensionV2 = makeJoinExplodeAlignTool('explode');
export const alignTool: ToolExtensionV2 = makeJoinExplodeAlignTool('align');
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Array rect / polar (Task D-Extensions): // Array rect / polar (Task D-Extensions):
// Vervielfältigt die aktuelle Selektion in einem Raster bzw. im Kreis. // Vervielfältigt die aktuelle Selektion in einem Raster bzw. im Kreis.
@@ -950,11 +1083,11 @@ export const coreModifyPlugin: PluginV2 = {
manifest: { manifest: {
id: 'core-modify', id: 'core-modify',
name: 'Bearbeiten (Kern)', name: 'Bearbeiten (Kern)',
version: '1.7.0', version: '1.8.0',
author: 'web-cad team', author: 'web-cad team',
description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays/Chamfer/Lengthen (V2).', description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays/Chamfer/Lengthen/Join/Explode/Align (V2).',
category: 'tools', category: 'tools',
enabledByDefault: true, enabledByDefault: true,
}, },
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool], tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool],
}; };