@cr8rcho/alkahest 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -181,6 +181,11 @@
181
181
  <path d="M1,1 L8,5 L1,9" fill="none" stroke="var(--edge)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
182
182
  </marker>
183
183
  </defs>
184
+ <!-- Screen-space hit target so empty areas fire pointer events (SVG's default
185
+ visiblePainted ignores unpainted regions, which on touch killed pan/pinch
186
+ anywhere but on a node). transparent fill is hittable; "none" is not. Sits
187
+ before #viewport so nodes paint on top and keep their own events. -->
188
+ <rect id="bg" x="0" y="0" width="100%" height="100%" fill="transparent" />
184
189
  <g id="viewport">
185
190
  <g id="edges"></g>
186
191
  <g id="nodes"></g>
@@ -606,18 +611,54 @@
606
611
  const DRAG_THRESHOLD = 4;
607
612
  let down = null; // {node|null, x, y, moved, pointerId}
608
613
 
614
+ // Active pointers for multi-touch. Two pointers down → pinch-to-zoom.
615
+ const pointers = new Map(); // pointerId -> {x, y}
616
+ let pinch = null; // { dist, midX, midY } for the running gesture
617
+
618
+ function startPinch() {
619
+ const pts = [...pointers.values()];
620
+ const dx = pts[0].x - pts[1].x, dy = pts[0].y - pts[1].y;
621
+ const rect = svg.getBoundingClientRect();
622
+ pinch = {
623
+ dist: Math.hypot(dx, dy) || 1,
624
+ midX: (pts[0].x + pts[1].x) / 2 - rect.left,
625
+ midY: (pts[0].y + pts[1].y) / 2 - rect.top,
626
+ };
627
+ // a pinch is purely a zoom — abandon any in-progress tap / pan / node drag
628
+ dragging = null; panStart = null; down = null;
629
+ }
630
+
609
631
  function onNodeDown(ev, n) {
610
632
  ev.stopPropagation();
611
- down = { node: n, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
633
+ pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
612
634
  svg.setPointerCapture(ev.pointerId);
635
+ if (pointers.size === 2) { startPinch(); return; }
636
+ down = { node: n, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
613
637
  }
614
638
  svg.addEventListener("pointerdown", (ev) => {
639
+ pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
640
+ svg.setPointerCapture(ev.pointerId);
641
+ if (pointers.size === 2) { startPinch(); return; }
615
642
  if (down) return; // already started on a node
616
643
  down = { node: null, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
617
644
  panStart = { x: ev.clientX - tx, y: ev.clientY - ty };
618
- svg.setPointerCapture(ev.pointerId);
619
645
  });
620
646
  svg.addEventListener("pointermove", (ev) => {
647
+ if (pointers.has(ev.pointerId)) pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
648
+ if (pinch && pointers.size >= 2) {
649
+ const pts = [...pointers.values()];
650
+ const dx = pts[0].x - pts[1].x, dy = pts[0].y - pts[1].y;
651
+ const dist = Math.hypot(dx, dy) || 1;
652
+ const rect = svg.getBoundingClientRect();
653
+ const mx = (pts[0].x + pts[1].x) / 2 - rect.left, my = (pts[0].y + pts[1].y) / 2 - rect.top;
654
+ // pan by how far the two-finger midpoint moved, then zoom around that midpoint
655
+ tx += mx - pinch.midX; ty += my - pinch.midY;
656
+ const nk = Math.min(4, Math.max(0.2, k * (dist / pinch.dist)));
657
+ tx = mx - (mx - tx) * (nk / k); ty = my - (my - ty) * (nk / k); k = nk;
658
+ pinch.dist = dist; pinch.midX = mx; pinch.midY = my;
659
+ applyTransform();
660
+ return;
661
+ }
621
662
  if (!down) return;
622
663
  if (!down.moved && Math.hypot(ev.clientX - down.x, ev.clientY - down.y) > DRAG_THRESHOLD) {
623
664
  down.moved = true;
@@ -627,8 +668,21 @@
627
668
  if (dragging) { const w = toWorld(ev.clientX, ev.clientY); dragging.x = w.x; dragging.y = w.y; dragging.vx = 0; dragging.vy = 0; } // free 2D since it's force-based
628
669
  else if (panStart) { tx = ev.clientX - panStart.x; ty = ev.clientY - panStart.y; applyTransform(); }
629
670
  });
630
- svg.addEventListener("pointerup", () => {
631
- if (down && !down.moved) { // didn't move = tap
671
+ function onPointerUp(ev) {
672
+ pointers.delete(ev.pointerId);
673
+ if (pinch) {
674
+ if (pointers.size < 2) {
675
+ pinch = null;
676
+ if (pointers.size === 1) {
677
+ // one finger left after a pinch → resume panning from there (no jump)
678
+ const [id, p] = [...pointers.entries()][0];
679
+ down = { node: null, x: p.x, y: p.y, moved: true, pointerId: id };
680
+ panStart = { x: p.x - tx, y: p.y - ty };
681
+ }
682
+ }
683
+ return; // a pinch gesture never counts as a tap
684
+ }
685
+ if (ev.type !== "pointercancel" && down && !down.moved) { // didn't move = tap
632
686
  if (down.node) select(down.node);
633
687
  else clearSelection();
634
688
  } else if (dragging) {
@@ -636,7 +690,9 @@
636
690
  alpha = 0.06;
637
691
  }
638
692
  dragging = null; panStart = null; down = null;
639
- });
693
+ }
694
+ svg.addEventListener("pointerup", onPointerUp);
695
+ svg.addEventListener("pointercancel", onPointerUp);
640
696
  svg.addEventListener("wheel", (ev) => {
641
697
  ev.preventDefault();
642
698
  const rect = svg.getBoundingClientRect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr8rcho/alkahest",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },