From 94026754b7afd352ceefeec850dead8e20a4713c Mon Sep 17 00:00:00 2001 From: Ilya Ashikhmin Date: Thu, 2 Jul 2026 23:47:34 +0200 Subject: [PATCH] feat(canvas): blank-drag panning and Ctrl/Cmd+wheel zoom-to-cursor - Pan the viewport by dragging empty canvas - Ctrl/Cmd + mouse wheel zooms toward the pointer; handler cleaned up on destroy - Make PropertiesPanel's selectionRev dependency explicit for the linter Co-Authored-By: Claude Opus 4.8 (1M context) --- src/canvas/CanvasController.ts | 24 +++++++++++++++++++++++- src/ui/PropertiesPanel.tsx | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/canvas/CanvasController.ts b/src/canvas/CanvasController.ts index 0e31801..37e989b 100644 --- a/src/canvas/CanvasController.ts +++ b/src/canvas/CanvasController.ts @@ -200,15 +200,36 @@ export class CanvasController { this.selectCell(link, false); this.commit(); }); - p.on("blank:pointerdown", () => { + // Blank drag pans the canvas. + p.on("blank:pointerdown", (evt: dia.Event) => { this.clearSelection(); + const t = this.paper.translate(); + this.panStart = { x: (evt.clientX ?? 0) - t.tx, y: (evt.clientY ?? 0) - t.ty }; + }); + p.on("blank:pointermove", (evt: dia.Event) => { + if (!this.panStart) return; + this.paper.translate((evt.clientX ?? 0) - this.panStart.x, (evt.clientY ?? 0) - this.panStart.y); + }); + p.on("blank:pointerup", () => { + this.panStart = null; }); p.on("cell:pointerdblclick", (view) => { // double-click focuses without toggling multi-select this.selectCell(view.model, false); }); + + // Ctrl/⌘ + wheel zooms toward the cursor. + this.wheelHandler = (e: WheelEvent) => { + if (!(e.ctrlKey || e.metaKey)) return; + e.preventDefault(); + this.zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1, { x: e.clientX, y: e.clientY }); + }; + this.paper.el.addEventListener("wheel", this.wheelHandler, { passive: false }); } + private panStart: { x: number; y: number } | null = null; + private wheelHandler: ((e: WheelEvent) => void) | null = null; + private snapElement(el: dia.Element): void { const g = this.config.gridSize; const { x, y } = el.position(); @@ -720,6 +741,7 @@ export class CanvasController { destroy(): void { this.stopFlow(); + if (this.wheelHandler) this.paper.el.removeEventListener("wheel", this.wheelHandler); this.paper.remove(); } } diff --git a/src/ui/PropertiesPanel.tsx b/src/ui/PropertiesPanel.tsx index 26d9e0b..30fa30e 100644 --- a/src/ui/PropertiesPanel.tsx +++ b/src/ui/PropertiesPanel.tsx @@ -29,6 +29,7 @@ export function PropertiesPanel() { const cell = selection && controller ? controller.getPrimary() : null; const model = useMemo(() => { + void rev; // recompute when the selected cell's props change externally if (!cell || !selection) return null; const props = (cell.prop("props") ?? {}) as Record; if (selection.kind === "node") {