merge: left-click-drag on empty canvas pans the view
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user