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) <noreply@anthropic.com>
This commit is contained in:
Ilya Ashikhmin 2026-07-02 23:47:34 +02:00
parent 701ed61a82
commit 94026754b7
2 changed files with 24 additions and 1 deletions

View File

@ -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();
}
}

View File

@ -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<string, PropertyValue>;
if (selection.kind === "node") {