| 664 | }; |
| 665 | |
| 666 | const onPointerMove = (e: PointerEvent) => { |
| 667 | const { px, py } = toLocal(e); |
| 668 | // 任意拖动(拖节点 或 空白平移)超过阈值即记为「移动过」→ 松手不算点击, |
| 669 | // 否则平移空白后会误判成「点空白 → 取消选中」。 |
| 670 | if (pointerDownRef.current) { |
| 671 | const ddx = px - pointerDownRef.current.x, ddy = py - pointerDownRef.current.y; |
| 672 | if (ddx * ddx + ddy * ddy > 16) pointerDownRef.current.moved = true; |
| 673 | } |
| 674 | const drag = draggingRef.current; |
| 675 | if (drag) { |
| 676 | const tf = transformRef.current; |
| 677 | drag.fx = (px - tf.x) / tf.k; |
| 678 | drag.fy = (py - tf.y) / tf.k; |
| 679 | sim.alphaTarget(0.3).restart(); |
| 680 | return; |
| 681 | } |
| 682 | const hit = pickNode(px, py); |
| 683 | const newId = hit ? hit.id : null; |
| 684 | if (newId !== hoverIdRef.current) { |
| 685 | hoverIdRef.current = newId; |
| 686 | canvas.style.cursor = newId ? "pointer" : "grab"; |
| 687 | // 卡片跟随 hover;无 hover 时退回展示「选中」节点的卡 |
| 688 | cardFor(newId ?? selectedIdRef.current); |
| 689 | draw(); |
| 690 | } |
| 691 | }; |
| 692 | |
| 693 | const onPointerDown = (e: PointerEvent) => { |
| 694 | const { px, py } = toLocal(e); |