From e43ace18533f5f321695443bedcf1983708c1075 Mon Sep 17 00:00:00 2001 From: Ilya Ashikhmin Date: Thu, 2 Jul 2026 23:39:55 +0200 Subject: [PATCH] feat(engine): canvas panning, wheel zoom and link editing tools Drag empty space to pan, mouse-wheel to zoom, and selecting a link now shows vertex/segment/arrowhead/remove tools so connectors can be rerouted and deleted interactively. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/diagram/controller.ts | 45 ++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/renderer/diagram/controller.ts b/src/renderer/diagram/controller.ts index 33e0857..64058a2 100644 --- a/src/renderer/diagram/controller.ts +++ b/src/renderer/diagram/controller.ts @@ -3,7 +3,7 @@ * framework-agnostic API for the React shell: add nodes, edit properties, * select/delete/copy, undo/redo, configure, save/load and run the mock flow. */ -import { dia, shapes, highlighters, g } from '@joint/core' +import { dia, shapes, highlighters, linkTools, g } from '@joint/core' import { PipelineNode, createNode, @@ -113,10 +113,47 @@ export class DiagramController { } }) + // Pan the canvas by dragging empty space. + let pan: { tx: number; ty: number; x: number; y: number } | null = null + paper.on('blank:pointerdown', (evt: dia.Event) => { + const t = paper.translate() + pan = { tx: t.tx, ty: t.ty, x: evt.clientX ?? 0, y: evt.clientY ?? 0 } + }) + paper.on('blank:pointermove', (evt: dia.Event) => { + if (!pan) return + paper.translate(pan.tx + ((evt.clientX ?? 0) - pan.x), pan.ty + ((evt.clientY ?? 0) - pan.y)) + }) + paper.on('blank:pointerup', () => (pan = null)) + + // Wheel to zoom toward the cursor. + paper.on('blank:mousewheel', (evt: dia.Event, _x, _y, delta: number) => { + evt.preventDefault?.() + this.zoom(delta > 0 ? 1.1 : 1 / 1.1) + }) + paper.on('cell:mousewheel', (_v, evt: dia.Event, _x, _y, delta: number) => { + evt.preventDefault?.() + this.zoom(delta > 0 ? 1.1 : 1 / 1.1) + }) + this.graph.on('remove', () => !this.restoring && this.fire('change')) this.graph.on('add', () => !this.restoring && this.fire('change')) } + private addLinkTools(link: dia.Link): void { + const view = this.paper?.findViewByModel(link) + if (!view) return + view.addTools( + new dia.ToolsView({ + tools: [ + new linkTools.Vertices(), + new linkTools.Segments(), + new linkTools.TargetArrowhead(), + new linkTools.Remove({ distance: 20 }) + ] + }) + ) + } + private validateConnection = ( sourceView: dia.CellView, sourceMagnet: SVGElement | null, @@ -197,6 +234,7 @@ export class DiagramController { padding: 4, attrs: { stroke: '#f0c000', 'stroke-width': 2 } }) + if (cell.isLink()) this.addLinkTools(cell as dia.Link) } } @@ -206,7 +244,10 @@ export class DiagramController { if (!cell) continue if (!cell.isLink()) this.setPortsVisible(cell as dia.Element, false) const view = this.paper?.findViewByModel(cell) - if (view) highlighters.mask.remove(view, 'sel') + if (view) { + highlighters.mask.remove(view, 'sel') + if (cell.isLink()) view.removeTools() + } } }