diff --git a/frontend/src/plugins/builtin/core-modify/index.ts b/frontend/src/plugins/builtin/core-modify/index.ts index 19d5710..7e98762 100644 --- a/frontend/src/plugins/builtin/core-modify/index.ts +++ b/frontend/src/plugins/builtin/core-modify/index.ts @@ -572,6 +572,245 @@ export const joinTool: ToolExtensionV2 = makeJoinExplodeAlignTool('join'); export const explodeTool: ToolExtensionV2 = makeJoinExplodeAlignTool('explode'); export const alignTool: ToolExtensionV2 = makeJoinExplodeAlignTool('align'); +// ───────────────────────────────────────────────────────────── +// Polyline-Editor (Task D-Finish): Vertices einer Polyline verschieben. +// Klick 1: Polyline wählen. Klick 2+: Vertex wählen (Toleranz) und ziehen. +// ESC beendet. +// ───────────────────────────────────────────────────────────── +let editPoly: CADElement | null = null; +let editVertexIdx = -1; + +export const polylineEditTool: ToolExtensionV2 = { + manifest: { + id: 'polyline-edit', + label: 'Polyline-Editor', + icon: '\u2b1a', + ribbonTab: 'canvas', + tags: ['pro'], + description: 'Polyline wählen, dann Vertex ziehen', + }, + optionsSchema: [], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc || !c.selection) return; + if (!editPoly) { + const hit = c.selection.hitTest(e.world.x, e.world.y, 5); + if (!hit || hit.type !== 'polyline') { + ctx.setStatus('Polyline-Editor: erst eine Polyline anklicken'); + return; + } + editPoly = hit; + ctx.setStatus('Polyline-Editor: Vertex ziehen, ESC beendet'); + return; + } + // Vertex finden in der Nähe: + const props = editPoly.properties as Record; + const pts = Array.isArray(props.points) + ? (props.points as unknown[]).map((p) => p as Pt) + : []; + for (let i = 0; i < pts.length; i++) { + if (Math.hypot(pts[i].x - e.world.x, pts[i].y - e.world.y) < 8) { + editVertexIdx = i; + ctx.setStatus(`Vertex ${i + 1} — ziehen`); + return; + } + } + ctx.setStatus('Kein Vertex in der Nähe'); + }, + move(e, ctx) { + if (!editPoly || editVertexIdx < 0) return; + const c = ctx as FullToolContext; + const props = editPoly.properties as Record; + const pts = Array.isArray(props.points) + ? (props.points as unknown[]).map((p) => p as Pt) + : []; + pts[editVertexIdx] = e.world; + const preview: CADElement = { + ...editPoly, + properties: { ...props, points: pts }, + }; + c.setPreview?.(preview); + }, + up(e, ctx) { + if (!editPoly || editVertexIdx < 0) return; + const c = ctx as FullToolContext; + if (!c.doc) { editVertexIdx = -1; return; } + const current = c.doc.getElement(editPoly.id); + if (!current) { editVertexIdx = -1; return; } + const props = current.properties as Record; + const pts = Array.isArray(props.points) + ? (props.points as unknown[]).map((p) => ({ ...(p as Pt) })) + : []; + if (editVertexIdx < pts.length) { + pts[editVertexIdx] = e.world; + const xs = pts.map((p) => p.x); + const ys = pts.map((p) => p.y); + c.doc.transact(() => { + c.doc!.updateElement(editPoly!.id, { + x: (Math.min(...xs) + Math.max(...xs)) / 2, + y: (Math.min(...ys) + Math.max(...ys)) / 2, + width: Math.max(...xs) - Math.min(...xs), + height: Math.max(...ys) - Math.min(...ys), + properties: { ...props, points: pts }, + }); + }); + ctx.setStatus(`Vertex ${editVertexIdx + 1} verschoben`); + } + editVertexIdx = -1; + }, + cancel(ctx) { + editPoly = null; + editVertexIdx = -1; + ctx.setStatus('Polyline-Editor beendet'); + }, + }, +}; + +// ───────────────────────────────────────────────────────────── +// Stretch (Task D-Finish): Endpunkte von Linien in einem Auswahlbereich +// verschieben. Klick 1: Basis, Klick 2: Ziel (Delta auf alle selektierten +// Endpunkte, die in der Nähe des Basispunkts liegen). +// ───────────────────────────────────────────────────────────── +let stretchBase: Pt | null = null; + +export const stretchTool: ToolExtensionV2 = { + manifest: { + id: 'stretch', + label: 'Stretch', + icon: '\u2197', + ribbonTab: 'canvas', + tags: ['pro'], + description: 'Linien auswählen, dann Basis→Ziel (Endpunkte in Basisnähe werden gestreckt)', + }, + optionsSchema: [], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc || !c.selection) return; + if (!stretchBase) { + if (c.selection.getIds().length === 0) { + const hit = c.selection.hitTest(e.world.x, e.world.y, 5); + if (!hit) { + ctx.setStatus('Stretch: zuerst Linien auswählen'); + return; + } + c.selection.setIds([hit.id]); + } + stretchBase = e.world; + ctx.setStatus(`Stretch: Zielklick (${c.selection.getIds().length} Linien)`); + return; + } + // Commit: alle Endpunkte, die dem Basispunkt am nächsten sind, verschieben: + const dx = e.world.x - stretchBase.x; + const dy = e.world.y - stretchBase.y; + const ids = c.selection.getIds(); + c.doc.transact(() => { + for (const id of ids) { + const el = c.doc!.getElement(id); + if (!el || el.type !== 'line') continue; + const props = el.properties as { x1?: number; y1?: number; x2?: number; y2?: number }; + if (props.x1 === undefined || props.y1 === undefined || props.x2 === undefined || props.y2 === undefined) continue; + // Endpunkte die dem Basispunkt am nächsten sind verschieben: + const d1 = Math.hypot(props.x1! - stretchBase!.x, props.y1! - stretchBase!.y); + const d2 = Math.hypot(props.x2! - stretchBase!.x, props.y2! - stretchBase!.y); + if (d1 < d2) { + props.x1 = props.x1! + dx; + props.y1 = props.y1! + dy; + } else { + props.x2 = props.x2! + dx; + props.y2 = props.y2! + dy; + } + const nx1 = props.x1!; + const ny1 = props.y1!; + const nx2 = props.x2!; + const ny2 = props.y2!; + c.doc!.updateElement(id, { + x: (nx1 + nx2) / 2, + y: (ny1 + ny2) / 2, + width: Math.abs(nx2 - nx1), + height: Math.abs(ny2 - ny1), + properties: props, + }); + } + }); + ctx.setStatus(`Stretch: ${ids.length} Linie(n) gestreckt \u0394(${dx.toFixed(0)}, ${dy.toFixed(0)})`); + stretchBase = null; + }, + cancel(ctx) { + stretchBase = null; + ctx.setStatus('Stretch abgebrochen'); + }, + }, +}; + +// ───────────────────────────────────────────────────────────── +// Break (Task D-Finish): Linie an einem Punkt in zwei Teile splitten. +// Klick auf Linie → zwei neue Linien, Original wird entfernt. +// ───────────────────────────────────────────────────────────── +export const breakTool: ToolExtensionV2 = { + manifest: { + id: 'break', + label: 'Break', + icon: '\u2702', + ribbonTab: 'canvas', + tags: ['pro'], + description: 'Linie anklicken — wird am Klickpunkt geteilt', + }, + optionsSchema: [], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc || !c.selection) return; + const hit = c.selection.hitTest(e.world.x, e.world.y, 5); + if (!hit || hit.type !== 'line') { + ctx.setStatus('Break: Linie anklicken'); + return; + } + const props = hit.properties as { x1?: number; y1?: number; x2?: number; y2?: number }; + if (props.x1 === undefined || props.y1 === undefined || props.x2 === undefined || props.y2 === undefined) return; + + // Klick auf die Linie projizieren (nächster Punkt auf der Linie): + const x1 = props.x1 as number; + const y1 = props.y1 as number; + const x2 = props.x2 as number; + const y2 = props.y2 as number; + const dx = x2 - x1; + const dy = y2 - y1; + const lenSq = dx * dx + dy * dy; + if (lenSq === 0) return; + const t = Math.max(0, Math.min(1, ((e.world.x - x1) * dx + (e.world.y - y1) * dy) / lenSq)); + const breakX = x1 + t * dx; + const breakY = y1 + t * dy; + + c.doc.transact(() => { + // Original entfernen: + c.doc!.deleteElements([hit.id]); + // Zwei neue Linien hinzufügen: + c.doc!.addElement({ + ...hit, + id: uid('brk'), + x: (x1 + breakX) / 2, + y: (y1 + breakY) / 2, + width: Math.abs(breakX - x1), + height: Math.abs(breakY - y1), + properties: { ...props, x2: breakX, y2: breakY }, + }); + c.doc!.addElement({ + ...hit, + id: uid('brk'), + x: (breakX + x2) / 2, + y: (breakY + y2) / 2, + width: Math.abs(x2 - breakX), + height: Math.abs(y2 - breakY), + properties: { ...props, x1: breakX, y1: breakY }, + }); + }); + ctx.setStatus(`Break: Linie bei (${breakX.toFixed(0)}, ${breakY.toFixed(0)}) geteilt`); + }, + }, +}; + // ───────────────────────────────────────────────────────────── // Array rect / polar (Task D-Extensions): // Vervielfältigt die aktuelle Selektion in einem Raster bzw. im Kreis. @@ -1083,11 +1322,11 @@ export const coreModifyPlugin: PluginV2 = { manifest: { id: 'core-modify', name: 'Bearbeiten (Kern)', - version: '1.8.0', + version: '1.9.0', author: 'web-cad team', - description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays/Chamfer/Lengthen/Join/Explode/Align (V2).', + description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays/Chamfer/Lengthen/Join/Explode/Align/Polyline-Edit/Stretch/Break (V2).', category: 'tools', enabledByDefault: true, }, - tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool], + tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool, polylineEditTool, stretchTool, breakTool], };