merge: left-click-drag on empty canvas pans the view

This commit is contained in:
mAi
2026-05-16 14:05:56 +02:00

View File

@@ -271,6 +271,52 @@ function startPan(e) {
svg.addEventListener("pointercancel", onUp); svg.addEventListener("pointercancel", onUp);
} }
// Left-click on empty canvas: ambiguous between "deselect" and "pan".
// We resolve by movement — under the drag threshold m gets the historic
// "click empties the selection" behaviour; past the threshold the gesture
// promotes to a pan (Excalidraw / Figma standard). 3px screen-space dead
// zone is enough that a steady click doesn't accidentally nudge the view.
const EMPTY_CANVAS_PAN_THRESHOLD_PX = 3;
function startEmptyCanvasGesture(e) {
const svg = /** @type {SVGSVGElement} */ ($("#canvas"));
const ctm = svg.getScreenCTM();
if (!ctm) return;
const scaleX = ctm.a, scaleY = ctm.d;
const startClientX = e.clientX, startClientY = e.clientY;
const startViewX = state.view.x, startViewY = state.view.y;
let panning = false;
try { svg.setPointerCapture(e.pointerId); } catch {}
const onMove = (ev) => {
const dx = ev.clientX - startClientX;
const dy = ev.clientY - startClientY;
if (!panning) {
if (Math.hypot(dx, dy) <= EMPTY_CANVAS_PAN_THRESHOLD_PX) return;
panning = true;
$(".canvas-wrap").classList.add("panning");
}
state.view.x = startViewX - dx / scaleX;
state.view.y = startViewY - dy / scaleY;
applyViewBox();
};
const onUp = (ev) => {
svg.removeEventListener("pointermove", onMove);
svg.removeEventListener("pointerup", onUp);
svg.removeEventListener("pointercancel", onUp);
try { svg.releasePointerCapture(ev.pointerId); } catch {}
if (panning) {
$(".canvas-wrap").classList.remove("panning");
setViewInURL();
} else if (state.selection) {
state.selection = null;
render();
}
};
svg.addEventListener("pointermove", onMove);
svg.addEventListener("pointerup", onUp);
svg.addEventListener("pointercancel", onUp);
}
function resetView() { function resetView() {
state.view.zoom = 1; state.view.zoom = 1;
state.view.x = 0; state.view.x = 0;
@@ -2036,12 +2082,18 @@ function onCanvasPointerDown(e) {
return; return;
} }
// No tool armed: clicks that started on a device/frame/io go to their // No tool armed: clicks that started on a device/frame/io/clamp/port/cable
// own handlers (drag / select). Leave them alone. // go to their own handlers (drag / select / replug). Leave them alone.
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id]")) return; if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id], [data-clamp-id], [data-port-id], [data-cable-id]")) return;
// Plain canvas click = clear selection. // Empty-canvas left-click without an active cable draw: start a
if (state.selection) { state.selection = null; render(); } // maybe-pan gesture. It promotes to a pan once the cursor crosses the
// drag threshold; if m clicks without dragging it falls back to the
// historic "clear selection" UX. Other buttons fall through (middle is
// already handled above, right-click is the browser context menu).
if (e.button === 0 && state.cableDrawFromPortID == null) {
startEmptyCanvasGesture(e);
}
} }
function startFrameRubberBand(e, p0) { function startFrameRubberBand(e, p0) {