feat(stammbaum): mobile read path — pan, zoom, fit-to-view (#692) #694

Merged
marcel merged 39 commits from feat/issue-692-stammbaum-mobile-panzoom into main 2026-05-30 07:43:44 +02:00
Showing only changes of commit b170085311 - Show all commits

View File

@@ -104,11 +104,11 @@ export const panZoomGestures: Action<SVGSVGElement, PanZoomGesturesParams> = (no
const onPointerDown = (e: PointerEvent) => {
cancelInertia();
try {
node.setPointerCapture(e.pointerId);
} catch {
/* pointer not capturable (e.g. synthetic event) — drag still works */
}
// NB: do NOT capture the pointer here — capturing on pointerdown makes the
// browser dispatch the trailing `click` at this element instead of the
// node under the pointer, which silently breaks node selection (a tap must
// still reach the node's onclick). Capture is deferred to the first move
// that crosses the drag threshold (see onPointerMove).
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
p.onGestureStart?.();
@@ -146,7 +146,17 @@ export const panZoomGestures: Action<SVGSVGElement, PanZoomGesturesParams> = (no
if (!dragging) return;
const dxPx = e.clientX - lastX;
const dyPx = e.clientY - lastY;
if (!moved && Math.hypot(dxPx, dyPx) >= DRAG_THRESHOLD_PX) moved = true;
if (!moved && Math.hypot(dxPx, dyPx) >= DRAG_THRESHOLD_PX) {
// A real drag has started — now capture so we keep receiving move/up
// even if the pointer leaves the canvas. (Deferred from pointerdown so
// taps still select nodes.)
moved = true;
try {
node.setPointerCapture(e.pointerId);
} catch {
/* pointer not capturable (e.g. synthetic event) — drag still works */
}
}
const { dx, dy } = screenDeltaToSvg(
dxPx,