fix(ui): left-click-drag on empty canvas pans the view
Canvas zoom shipped pan as middle-drag / Space+drag, which left m unable to reach a freshly-created frame outside the default viewport — the only escape was middle-button or holding Space, neither of which is discoverable. Empty-canvas left-pointerdown now starts an ambiguous gesture: if the cursor moves past a 3px screen-space threshold it promotes to a pan (Excalidraw / Figma standard); below the threshold it falls back to the historic "click empties the selection" UX so plain clicks still deselect. Pointerdown on a device, frame, IO marker, port, or cable keeps routing to its own handler. Middle-drag and Space+drag pan unchanged.
This commit is contained in:
@@ -271,6 +271,52 @@ function startPan(e) {
|
||||
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() {
|
||||
state.view.zoom = 1;
|
||||
state.view.x = 0;
|
||||
@@ -2036,12 +2082,18 @@ function onCanvasPointerDown(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No tool armed: clicks that started on a device/frame/io go to their
|
||||
// own handlers (drag / select). Leave them alone.
|
||||
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id]")) return;
|
||||
// No tool armed: clicks that started on a device/frame/io/clamp/port/cable
|
||||
// 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], [data-clamp-id], [data-port-id], [data-cable-id]")) return;
|
||||
|
||||
// Plain canvas click = clear selection.
|
||||
if (state.selection) { state.selection = null; render(); }
|
||||
// Empty-canvas left-click without an active cable draw: start a
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user