dsh-recency-guard 0.11.3 → 0.11.4

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.
package/README.md CHANGED
@@ -65,6 +65,14 @@
65
65
 
66
66
  当某个插件把 DSH 拖垮时,本插件会在崩溃的那一刻记录下"谁最可疑",并在**下一次启动时、在崩溃插件的代码被加载之前**把它挡在门外。反复崩溃时,关闭的插件会逐个增加(第 1 次关最近更新的 1 个,仍崩溃则关最近更新的 2 个)。稳定之后,除首要嫌疑外,其余被关闭的配置会自动恢复。
67
67
 
68
+ ## v0.11.4 触屏双指缩放(图谱交互补齐)
69
+
70
+ - **双指捏合缩放**:两指同时按下即进入捏合 —— 中点下的世界点在缩放中保持
71
+ 不动(与单指缩放同一不变式),同时中点位移会带动画布平移;捏合期间取消
72
+ 画布平移拖拽,抬指后清空取样。退化输入(两指重合/非有限)返回原视口,
73
+ 绝不产生 Infinity。
74
+ - 回归:viewport 新增 4 条捏合断言(翻倍缩放/锚点不动/跟随平移/退化与上界)。
75
+
68
76
  ## v0.11.3 图谱交互补齐(边可交互 + 布局过渡)
69
77
 
70
78
  - **边可交互(审计 P0 收尾)**:此前边完全不可点击、`edge.label` 是死数据。
package/dist/client.js CHANGED
@@ -674,6 +674,22 @@ function zoomAt(viewport, anchor, nextScale) {
674
674
  function zoomBy(viewport, anchor, factor) {
675
675
  return zoomAt(viewport, anchor, clampScale(viewport.scale) * finite(factor, 1));
676
676
  }
677
+ function pinchViewport(viewport, prev, next) {
678
+ const prevDx = finite(prev.b.x, 0) - finite(prev.a.x, 0);
679
+ const prevDy = finite(prev.b.y, 0) - finite(prev.a.y, 0);
680
+ const nextDx = finite(next.b.x, 0) - finite(next.a.x, 0);
681
+ const nextDy = finite(next.b.y, 0) - finite(next.a.y, 0);
682
+ const prevDist = Math.hypot(prevDx, prevDy);
683
+ const nextDist = Math.hypot(nextDx, nextDy);
684
+ if (!(prevDist > 0.01) || !Number.isFinite(nextDist)) return viewport;
685
+ const s0 = clampScale(viewport.scale);
686
+ const s1 = clampScale(s0 * nextDist / prevDist);
687
+ const prevMid = { x: (finite(prev.a.x, 0) + finite(prev.b.x, 0)) / 2, y: (finite(prev.a.y, 0) + finite(prev.b.y, 0)) / 2 };
688
+ const nextMid = { x: (finite(next.a.x, 0) + finite(next.b.x, 0)) / 2, y: (finite(next.a.y, 0) + finite(next.b.y, 0)) / 2 };
689
+ const worldX = (prevMid.x - viewport.tx) / s0;
690
+ const worldY = (prevMid.y - viewport.ty) / s0;
691
+ return { scale: s1, tx: nextMid.x - worldX * s1, ty: nextMid.y - worldY * s1 };
692
+ }
677
693
  function centerOf(size) {
678
694
  return { x: finite(size.width, 0) / 2, y: finite(size.height, 0) / 2 };
679
695
  }
@@ -1336,6 +1352,8 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1336
1352
  const [colorMode, setColorMode] = (0, import_react.useState)("tone");
1337
1353
  const [onlyConnected, setOnlyConnected] = (0, import_react.useState)(false);
1338
1354
  const [layoutEpoch, setLayoutEpoch] = (0, import_react.useState)(0);
1355
+ const pointersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
1356
+ const pinchPrevRef = (0, import_react.useRef)(null);
1339
1357
  const [offsets, setOffsets] = (0, import_react.useState)({});
1340
1358
  const wrapRef = (0, import_react.useRef)(null);
1341
1359
  const dragRef = (0, import_react.useRef)({
@@ -1433,11 +1451,29 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1433
1451
  return () => host.removeEventListener("wheel", listener);
1434
1452
  }, []);
1435
1453
  const onPointerDown = (0, import_react.useCallback)((event) => {
1454
+ const id = event.pointerId ?? 0;
1455
+ pointersRef.current.set(id, { x: event.clientX, y: event.clientY });
1456
+ if (pointersRef.current.size >= 2) {
1457
+ const [a, b] = [...pointersRef.current.values()];
1458
+ pinchPrevRef.current = { a: a ?? { x: 0, y: 0 }, b: b ?? { x: 0, y: 0 } };
1459
+ dragRef.current = { active: false, moved: false, x: 0, y: 0 };
1460
+ return;
1461
+ }
1436
1462
  suppressClickRef.current = false;
1437
1463
  dragRef.current = { active: true, moved: false, x: event.clientX, y: event.clientY };
1438
1464
  }, []);
1439
1465
  const onPointerMove = (0, import_react.useCallback)(
1440
1466
  (event) => {
1467
+ const id = event.pointerId ?? 0;
1468
+ if (pointersRef.current.has(id)) pointersRef.current.set(id, { x: event.clientX, y: event.clientY });
1469
+ if (pointersRef.current.size >= 2 && pinchPrevRef.current !== null) {
1470
+ const [a, b] = [...pointersRef.current.values()];
1471
+ const next = { a: a ?? { x: 0, y: 0 }, b: b ?? { x: 0, y: 0 } };
1472
+ const prev = pinchPrevRef.current;
1473
+ setViewport((current) => clampPan(pinchViewport(current, prev, next), size, bounds));
1474
+ pinchPrevRef.current = next;
1475
+ return;
1476
+ }
1441
1477
  const nodeDrag = nodeDragRef.current;
1442
1478
  if (nodeDrag !== null) {
1443
1479
  const dx2 = event.clientX - nodeDrag.x;
@@ -1465,7 +1501,9 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1465
1501
  },
1466
1502
  [viewport.scale, size, bounds]
1467
1503
  );
1468
- const onPointerUp = (0, import_react.useCallback)(() => {
1504
+ const onPointerUp = (0, import_react.useCallback)((event) => {
1505
+ pointersRef.current.delete(event?.pointerId ?? 0);
1506
+ if (pointersRef.current.size < 2) pinchPrevRef.current = null;
1469
1507
  if (nodeDragRef.current !== null && nodeDragRef.current.moved) suppressClickRef.current = true;
1470
1508
  nodeDragRef.current = null;
1471
1509
  dragRef.current = { active: false, moved: false, x: 0, y: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recency-guard",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Crash-recovery guard for DeepSeek Harness: records third-party/experimental plugin install & update recency, backs up their config, and after a crash auto-disables the most-recently-updated plugin(s) so the profile boots again, then restores stable config.",
5
5
  "license": "MIT",
6
6
  "type": "module",