@kolosal-ai/rivet 0.3.0 → 0.4.0

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId, strayPlacementPct, anchorHandleId, anchorAutoHandleId, parseAnchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS } from './chunk-AXT35ZJV.js';
2
- import { createPresenceRegistry, createLockRegistry, DEFAULT_PRESENCE_OPTIONS, peerColor } from './chunk-A52HZWLH.js';
3
- export { LOCK_DEFAULT_REFUSED, useNodeLock, useNodeLockAllows } from './chunk-A52HZWLH.js';
1
+ import { findSelectableAnchor, parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId, strayPlacementPct, anchorHandleId, anchorAutoHandleId, parseAnchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS, anchorRect } from './chunk-W5HBJ4VF.js';
2
+ import { snapToGrid, boundingRect, clampChildToParent, anchorTargetKey, createPresenceRegistry, createLockRegistry, nodeControlledEqual, buildChildIndex, collectDescendants, worldPosition, nodeDepth, descendantIds, hasAncestorIn, edgeTargetKey, DEFAULT_PRESENCE_OPTIONS, collectPeerOutlines, peerOutlinesEqual, usePeerIdentities, serializeGraph, presenceTargetKey, peerColor, isNodeTarget, presenceTargetEdgeId } from './chunk-TT2N7KWU.js';
3
+ export { LOCK_DEFAULT_REFUSED, boundingRect, clampChildToParent, serializeGraph, snapToGrid, useNodeLock, useNodeLockAllows } from './chunk-TT2N7KWU.js';
4
4
  import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
5
5
  export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
6
6
  import { clamp, clampNodeToLanes, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
@@ -282,6 +282,7 @@ var DEFAULT_NODE_SIZE = { width: 160, height: 44 };
282
282
  function handleKey(nodeId, handleId) {
283
283
  return `${nodeId} ${handleId}`;
284
284
  }
285
+ var DRAG_THRESHOLD = 4;
285
286
  var SELECTOR = {
286
287
  node: "[data-rivet-node]",
287
288
  handle: "[data-rivet-handle]",
@@ -387,111 +388,6 @@ function guardNodeChanges(changes, isHeld, getLocal) {
387
388
  return guarded;
388
389
  }
389
390
 
390
- // src/graph.ts
391
- function clampChildToParent(position, childSize, parentSize) {
392
- const maxX = Math.max(0, parentSize.width - childSize.width);
393
- const maxY = Math.max(0, parentSize.height - childSize.height);
394
- return {
395
- x: Math.min(maxX, Math.max(0, position.x)),
396
- y: Math.min(maxY, Math.max(0, position.y))
397
- };
398
- }
399
- function snapToGrid(position, grid) {
400
- const [gx, gy] = grid;
401
- return {
402
- x: gx > 0 ? Math.round(position.x / gx) * gx : position.x,
403
- y: gy > 0 ? Math.round(position.y / gy) * gy : position.y
404
- };
405
- }
406
- function boundingRect(rects) {
407
- if (rects.length === 0) return null;
408
- let minX = Number.POSITIVE_INFINITY;
409
- let minY = Number.POSITIVE_INFINITY;
410
- let maxX = Number.NEGATIVE_INFINITY;
411
- let maxY = Number.NEGATIVE_INFINITY;
412
- for (const r of rects) {
413
- minX = Math.min(minX, r.x);
414
- minY = Math.min(minY, r.y);
415
- maxX = Math.max(maxX, r.x + r.width);
416
- maxY = Math.max(maxY, r.y + r.height);
417
- }
418
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
419
- }
420
- var MAX_PARENT_DEPTH = 50;
421
- function worldPosition(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
422
- const node = nodes.get(id);
423
- if (!node) return { x: 0, y: 0 };
424
- let x = node.position.x;
425
- let y = node.position.y;
426
- let parentId = node.parentId;
427
- let guard = 0;
428
- while (parentId && guard++ < maxDepth) {
429
- const parent = nodes.get(parentId);
430
- if (!parent) break;
431
- x += parent.position.x;
432
- y += parent.position.y;
433
- parentId = parent.parentId;
434
- }
435
- return { x, y };
436
- }
437
- function nodeDepth(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
438
- let depth = 0;
439
- let parentId = nodes.get(id)?.parentId;
440
- while (parentId && depth < maxDepth) {
441
- depth++;
442
- parentId = nodes.get(parentId)?.parentId;
443
- }
444
- return depth;
445
- }
446
- function hasAncestorIn(nodes, id, set, maxDepth = MAX_PARENT_DEPTH) {
447
- let parentId = nodes.get(id)?.parentId;
448
- let guard = 0;
449
- while (parentId && guard++ < maxDepth) {
450
- if (set.has(parentId)) return true;
451
- parentId = nodes.get(parentId)?.parentId;
452
- }
453
- return false;
454
- }
455
- function buildChildIndex(nodes) {
456
- const childrenOf = /* @__PURE__ */ new Map();
457
- for (const node of nodes.values()) {
458
- if (!node.parentId) continue;
459
- const list = childrenOf.get(node.parentId);
460
- if (list) list.push(node.id);
461
- else childrenOf.set(node.parentId, [node.id]);
462
- }
463
- return childrenOf;
464
- }
465
- function collectDescendants(id, childIndex, out) {
466
- for (const child of childIndex.get(id) ?? []) {
467
- out.push(child);
468
- collectDescendants(child, childIndex, out);
469
- }
470
- }
471
- function descendantIds(nodes, id) {
472
- const out = [];
473
- collectDescendants(id, buildChildIndex(nodes), out);
474
- return out;
475
- }
476
- function serializeGraph(nodes, edges, viewport) {
477
- const cleanNodes = [];
478
- for (const node of nodes) {
479
- const copy = { ...node, position: { ...node.position } };
480
- if (node.size) copy.size = { ...node.size };
481
- copy.hovered = void 0;
482
- copy.dragging = void 0;
483
- cleanNodes.push(copy);
484
- }
485
- return {
486
- nodes: cleanNodes,
487
- edges: [...edges].map((edge) => ({ ...edge })),
488
- viewport: { ...viewport }
489
- };
490
- }
491
- function nodeControlledEqual(a, b) {
492
- return a.position.x === b.position.x && a.position.y === b.position.y && Boolean(a.selected) === Boolean(b.selected) && a.data === b.data && a.type === b.type && a.laneId === b.laneId && a.ariaLabel === b.ariaLabel && a.parentId === b.parentId && a.extent === b.extent && a.width === b.width && a.height === b.height && Boolean(a.dragging) === Boolean(b.dragging) && a.size?.width === b.size?.width && a.size?.height === b.size?.height;
493
- }
494
-
495
391
  // src/history.ts
496
392
  function hasRecordableChange(changes) {
497
393
  return changes.some((change) => {
@@ -997,6 +893,30 @@ function resolveEnd(edge, role, node, peer, frame) {
997
893
  extras?.displayedSides?.set(key, side);
998
894
  return endAt(side);
999
895
  }
896
+ function polylineMidpoint(points) {
897
+ const first = points[0];
898
+ if (!first) return { x: 0, y: 0 };
899
+ let total = 0;
900
+ for (let i = 0; i < points.length - 1; i++) {
901
+ const a = points[i];
902
+ const b = points[i + 1];
903
+ if (a && b) total += Math.hypot(b.x - a.x, b.y - a.y);
904
+ }
905
+ let travelled = 0;
906
+ const half = total / 2;
907
+ for (let i = 0; i < points.length - 1; i++) {
908
+ const a = points[i];
909
+ const b = points[i + 1];
910
+ if (!a || !b) continue;
911
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
912
+ if (travelled + length >= half) {
913
+ const t = length === 0 ? 0 : (half - travelled) / length;
914
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
915
+ }
916
+ travelled += length;
917
+ }
918
+ return points[points.length - 1] ?? first;
919
+ }
1000
920
  function distanceToPolyline(point, points) {
1001
921
  let best = Number.POSITIVE_INFINITY;
1002
922
  for (let i = 0; i < points.length - 1; i++) {
@@ -1029,7 +949,10 @@ var RivetGraphStore = class {
1029
949
  requestRender: () => this.requestRender(),
1030
950
  requestCursorRender: () => this.requestCursorRender()
1031
951
  });
1032
- locks = createLockRegistry(() => this.requestRender());
952
+ locks = createLockRegistry(
953
+ () => this.requestRender(),
954
+ (id) => this.nodes.get(id)?.parentId
955
+ );
1033
956
  viewport;
1034
957
  viewportClamp = null;
1035
958
  pending = null;
@@ -1042,6 +965,8 @@ var RivetGraphStore = class {
1042
965
  nodeElements = /* @__PURE__ */ new Map();
1043
966
  viewportListeners = /* @__PURE__ */ new Set();
1044
967
  frameListeners = /* @__PURE__ */ new Set();
968
+ cursorFrameListeners = /* @__PURE__ */ new Set();
969
+ connectionListeners = /* @__PURE__ */ new Set();
1045
970
  selectionListeners = /* @__PURE__ */ new Set();
1046
971
  selectedNodeIds = /* @__PURE__ */ new Set();
1047
972
  selectionBoxActive = false;
@@ -1051,6 +976,11 @@ var RivetGraphStore = class {
1051
976
  focusedNodeId = null;
1052
977
  focusListeners = /* @__PURE__ */ new Set();
1053
978
  selectedEdgeId = null;
979
+ /** Selected anchors, keyed like the registry so lookups are one hash. */
980
+ selectedAnchors = /* @__PURE__ */ new Map();
981
+ selectedAnchorSnapshot = [];
982
+ selectedAnchorsStale = false;
983
+ anchorSelectionListeners = /* @__PURE__ */ new Set();
1054
984
  nodeDragging = false;
1055
985
  nodeResizing = false;
1056
986
  edgeAlignment = "manual";
@@ -1062,6 +992,8 @@ var RivetGraphStore = class {
1062
992
  lockConflictHandler = null;
1063
993
  displayedSides = /* @__PURE__ */ new Map();
1064
994
  edgeLabelAnchors = /* @__PURE__ */ new Map();
995
+ /** Screen-space points on claimed edges, published only while DOM outlines are on. */
996
+ peerEdgeAnchors = /* @__PURE__ */ new Map();
1065
997
  alignmentGuides = [];
1066
998
  nodeChangeHandler = null;
1067
999
  edgeChangeHandler = null;
@@ -1255,6 +1187,7 @@ var RivetGraphStore = class {
1255
1187
  for (const key of this.anchors.keys()) {
1256
1188
  if (key.startsWith(prefix)) this.anchors.delete(key);
1257
1189
  }
1190
+ this.dropSelectedAnchors((ref) => ref.nodeId === id);
1258
1191
  }
1259
1192
  removeNodeInternal(id, removedEdges) {
1260
1193
  if (!this.nodes.has(id)) return false;
@@ -1382,6 +1315,15 @@ var RivetGraphStore = class {
1382
1315
  notifyFrame = () => {
1383
1316
  for (const listener of this.frameListeners) listener();
1384
1317
  };
1318
+ subscribeCursorFrame = (listener) => {
1319
+ this.cursorFrameListeners.add(listener);
1320
+ return () => {
1321
+ this.cursorFrameListeners.delete(listener);
1322
+ };
1323
+ };
1324
+ notifyCursorFrame = () => {
1325
+ for (const listener of this.cursorFrameListeners) listener();
1326
+ };
1385
1327
  // --- node geometry -------------------------------------------------------
1386
1328
  getWorldPositions = () => {
1387
1329
  if (!this.worldCache) {
@@ -1633,8 +1575,10 @@ var RivetGraphStore = class {
1633
1575
  const opened = this.locks.setLocks(locks);
1634
1576
  if (opened.length === 0 || this.heldNodeIds.size === 0) return;
1635
1577
  const held = [...this.heldNodeIds];
1636
- for (const nodeId of opened) {
1637
- if (!this.heldNodeIds.has(nodeId)) continue;
1578
+ const openedSet = new Set(opened);
1579
+ for (const nodeId of held) {
1580
+ const source = this.locks.getLockSource(nodeId);
1581
+ if (source === null || !openedSet.has(source)) continue;
1638
1582
  const holderId = this.locks.getHolder(nodeId);
1639
1583
  if (holderId) this.lockConflictHandler?.({ nodeId, holderId, ids: held });
1640
1584
  }
@@ -1662,6 +1606,7 @@ var RivetGraphStore = class {
1662
1606
  };
1663
1607
  selectNode = (id, additive = false) => {
1664
1608
  this.setSelectionBoxActive(false);
1609
+ this.dropSelectedAnchors(() => true);
1665
1610
  if (id === null) {
1666
1611
  this.applyNodeSelection(/* @__PURE__ */ new Set());
1667
1612
  this.notifySelection();
@@ -1681,6 +1626,7 @@ var RivetGraphStore = class {
1681
1626
  };
1682
1627
  selectNodes = (ids, additive = false) => {
1683
1628
  this.setSelectionBoxActive(false);
1629
+ this.dropSelectedAnchors(() => true);
1684
1630
  const target = additive ? new Set(this.selectedNodeIds) : /* @__PURE__ */ new Set();
1685
1631
  for (const id of ids) target.add(id);
1686
1632
  this.applyNodeSelection(target);
@@ -1689,6 +1635,7 @@ var RivetGraphStore = class {
1689
1635
  };
1690
1636
  getSelectedNodes = () => [...this.selectedNodeIds];
1691
1637
  isNodeSelected = (id) => this.selectedNodeIds.has(id);
1638
+ getSelectedEdgeId = () => this.selectedEdgeId;
1692
1639
  getSelection = () => this.buildSelection();
1693
1640
  subscribeSelection = (listener) => {
1694
1641
  this.selectionListeners.add(listener);
@@ -1728,6 +1675,7 @@ var RivetGraphStore = class {
1728
1675
  // --- edges ---------------------------------------------------------------
1729
1676
  selectEdge = (id) => {
1730
1677
  this.setSelectionBoxActive(false);
1678
+ this.dropSelectedAnchors(() => true);
1731
1679
  this.setEdgeSelection(id);
1732
1680
  this.applyNodeSelection(/* @__PURE__ */ new Set());
1733
1681
  this.notifySelection();
@@ -1773,7 +1721,7 @@ var RivetGraphStore = class {
1773
1721
  const removedNodes = [];
1774
1722
  const removedEdges = [];
1775
1723
  const selectedEdge = this.selectedEdgeId;
1776
- if (selectedEdge && this.removeEdgeInternal(selectedEdge)) {
1724
+ if (selectedEdge && this.locks.allows(edgeTargetKey(selectedEdge), "delete") && this.removeEdgeInternal(selectedEdge)) {
1777
1725
  removedEdges.push(selectedEdge);
1778
1726
  }
1779
1727
  if (this.selectedNodeIds.size > 0) {
@@ -1898,6 +1846,10 @@ var RivetGraphStore = class {
1898
1846
  setEdgeLabelAnchors = (anchors) => {
1899
1847
  this.edgeLabelAnchors = anchors;
1900
1848
  };
1849
+ getPeerEdgeAnchors = () => this.peerEdgeAnchors;
1850
+ setPeerEdgeAnchors = (anchors) => {
1851
+ this.peerEdgeAnchors = anchors;
1852
+ };
1901
1853
  getAlignmentGuides = () => this.alignmentGuides;
1902
1854
  setAlignmentGuides = (guides) => {
1903
1855
  this.alignmentGuides = guides;
@@ -1944,6 +1896,8 @@ var RivetGraphStore = class {
1944
1896
  anchorId,
1945
1897
  color: options?.color ?? prev?.color,
1946
1898
  strays: options?.strays ?? prev?.strays,
1899
+ selectable: options?.selectable ?? prev?.selectable,
1900
+ data: options?.data ?? prev?.data,
1947
1901
  element,
1948
1902
  // Keep the last-known geometry until the next successful measurement, so
1949
1903
  // a re-bind (display remount, editor DOM swap) never blanks the chrome.
@@ -1962,6 +1916,7 @@ var RivetGraphStore = class {
1962
1916
  };
1963
1917
  unregisterAnchor = (nodeId, anchorId) => {
1964
1918
  if (!this.anchors.delete(handleKey(nodeId, anchorId))) return;
1919
+ this.dropSelectedAnchors((ref) => ref.nodeId === nodeId && ref.anchorId === anchorId);
1965
1920
  this.bumpAnchors(nodeId);
1966
1921
  this.requestRender();
1967
1922
  };
@@ -1983,6 +1938,51 @@ var RivetGraphStore = class {
1983
1938
  return records;
1984
1939
  };
1985
1940
  getNodeAnchorsVersion = (nodeId) => this.anchorVersions.get(nodeId) ?? 0;
1941
+ getSelectedAnchors = () => {
1942
+ if (this.selectedAnchorsStale) {
1943
+ this.selectedAnchorsStale = false;
1944
+ this.selectedAnchorSnapshot = [...this.selectedAnchors.values()];
1945
+ }
1946
+ return this.selectedAnchorSnapshot;
1947
+ };
1948
+ setSelectedAnchors = (refs) => {
1949
+ const next = /* @__PURE__ */ new Map();
1950
+ for (const ref of refs) next.set(handleKey(ref.nodeId, ref.anchorId), ref);
1951
+ if (next.size === this.selectedAnchors.size && [...next.keys()].every((key) => this.selectedAnchors.has(key))) {
1952
+ return;
1953
+ }
1954
+ const clearsGraph = next.size > 0 && (this.selectedNodeIds.size > 0 || this.selectedEdgeId !== null);
1955
+ this.selectedAnchors = /* @__PURE__ */ new Map();
1956
+ if (clearsGraph) {
1957
+ this.selectNode(null);
1958
+ this.selectEdge(null);
1959
+ }
1960
+ this.selectedAnchors = next;
1961
+ this.selectedAnchorsStale = true;
1962
+ for (const listener of this.anchorSelectionListeners) listener();
1963
+ this.requestRender();
1964
+ };
1965
+ isAnchorSelected = (nodeId, anchorId) => this.selectedAnchors.has(handleKey(nodeId, anchorId));
1966
+ subscribeAnchorSelection = (listener) => {
1967
+ this.anchorSelectionListeners.add(listener);
1968
+ return () => {
1969
+ this.anchorSelectionListeners.delete(listener);
1970
+ };
1971
+ };
1972
+ /** Drop every selected anchor the predicate claims. No-op when none match. */
1973
+ dropSelectedAnchors(matches) {
1974
+ if (this.selectedAnchors.size === 0) return;
1975
+ let changed = false;
1976
+ for (const [key, ref] of this.selectedAnchors) {
1977
+ if (!matches(ref)) continue;
1978
+ this.selectedAnchors.delete(key);
1979
+ changed = true;
1980
+ }
1981
+ if (!changed) return;
1982
+ this.selectedAnchorsStale = true;
1983
+ for (const listener of this.anchorSelectionListeners) listener();
1984
+ this.requestRender();
1985
+ }
1986
1986
  subscribeNodeAnchors = (nodeId, listener) => {
1987
1987
  let set = this.anchorListeners.get(nodeId);
1988
1988
  if (!set) {
@@ -2007,17 +2007,29 @@ var RivetGraphStore = class {
2007
2007
  beginConnection = (next) => {
2008
2008
  this.pending = next;
2009
2009
  this.requestRender();
2010
+ this.notifyConnection();
2010
2011
  };
2011
2012
  updateConnection = (to, toPosition) => {
2012
2013
  if (!this.pending) return;
2013
2014
  this.pending = { ...this.pending, to, toPosition };
2014
2015
  this.requestRender();
2016
+ this.notifyConnection();
2015
2017
  };
2016
2018
  endConnection = () => {
2017
2019
  if (!this.pending) return;
2018
2020
  this.pending = null;
2019
2021
  this.requestRender();
2022
+ this.notifyConnection();
2020
2023
  };
2024
+ subscribeConnection = (listener) => {
2025
+ this.connectionListeners.add(listener);
2026
+ return () => {
2027
+ this.connectionListeners.delete(listener);
2028
+ };
2029
+ };
2030
+ notifyConnection() {
2031
+ for (const listener of this.connectionListeners) listener();
2032
+ }
2021
2033
  // --- render loop hook ----------------------------------------------------
2022
2034
  requestRender = () => {
2023
2035
  this.renderRequester();
@@ -2696,7 +2708,12 @@ function NodeResizer({
2696
2708
 
2697
2709
  // src/presence/local.ts
2698
2710
  var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
2699
- var samePresence = (a, b) => a.cursor?.x === b.cursor?.x && a.cursor?.y === b.cursor?.y && sameIds(a.selection, b.selection) && sameIds(a.holding, b.holding);
2711
+ var sameConnection = (a, b) => {
2712
+ if (a === b) return true;
2713
+ if (!a || !b) return false;
2714
+ return a.source === b.source && a.sourceHandle === b.sourceHandle && a.toPosition === b.toPosition && a.to.x === b.to.x && a.to.y === b.to.y;
2715
+ };
2716
+ var samePresence = (a, b) => a.cursor?.x === b.cursor?.x && a.cursor?.y === b.cursor?.y && sameIds(a.selection, b.selection) && sameIds(a.holding, b.holding) && sameConnection(a.pending, b.pending);
2700
2717
  function trackLocalPresence({
2701
2718
  store,
2702
2719
  pane,
@@ -2708,6 +2725,21 @@ function trackLocalPresence({
2708
2725
  let last = null;
2709
2726
  let timer = null;
2710
2727
  let lastEmit = Number.NEGATIVE_INFINITY;
2728
+ const selectionTargets = () => {
2729
+ const targets = store.getSelectedNodes();
2730
+ const edgeId = store.getSelectedEdgeId();
2731
+ if (edgeId) targets.push(presenceTargetKey({ kind: "edge", id: edgeId }));
2732
+ for (const ref of store.getSelectedAnchors()) {
2733
+ targets.push(presenceTargetKey({ kind: "anchor", ...ref }));
2734
+ }
2735
+ return targets;
2736
+ };
2737
+ const localConnection = () => {
2738
+ const pending = store.getPending();
2739
+ if (!pending) return null;
2740
+ const { source, sourceHandle, sourceType, from, to, toPosition } = pending;
2741
+ return toPosition === void 0 ? { source, sourceHandle, sourceType, from, to } : { source, sourceHandle, sourceType, from, to, toPosition };
2742
+ };
2711
2743
  const publish = () => {
2712
2744
  if (timer !== null) {
2713
2745
  clearTimeout(timer);
@@ -2716,8 +2748,9 @@ function trackLocalPresence({
2716
2748
  lastEmit = performance.now();
2717
2749
  const next = {
2718
2750
  cursor,
2719
- selection: store.getSelectedNodes(),
2720
- holding: [...store.getHeldNodeIds()]
2751
+ selection: selectionTargets(),
2752
+ holding: [...store.getHeldNodeIds()],
2753
+ pending: localConnection()
2721
2754
  };
2722
2755
  if (last && samePresence(last, next)) return;
2723
2756
  last = next;
@@ -2759,7 +2792,18 @@ function trackLocalPresence({
2759
2792
  schedule();
2760
2793
  });
2761
2794
  const unsubscribeSelection = store.subscribeSelection(() => publish());
2795
+ const unsubscribeAnchors = store.subscribeAnchorSelection(() => publish());
2762
2796
  const unsubscribeHeld = store.subscribeHeldNodes(() => publish());
2797
+ let hadPending = store.getPending() !== null;
2798
+ const unsubscribeConnection = store.subscribeConnection(() => {
2799
+ const has = store.getPending() !== null;
2800
+ if (has === hadPending) {
2801
+ schedule();
2802
+ return;
2803
+ }
2804
+ hadPending = has;
2805
+ publish();
2806
+ });
2763
2807
  pane.addEventListener("pointermove", onPointerMove);
2764
2808
  pane.addEventListener("pointerleave", onPointerLeave);
2765
2809
  return () => {
@@ -2768,7 +2812,9 @@ function trackLocalPresence({
2768
2812
  pane.removeEventListener("pointerleave", onPointerLeave);
2769
2813
  unsubscribeViewport();
2770
2814
  unsubscribeSelection();
2815
+ unsubscribeAnchors();
2771
2816
  unsubscribeHeld();
2817
+ unsubscribeConnection();
2772
2818
  };
2773
2819
  }
2774
2820
 
@@ -3170,6 +3216,9 @@ var LABEL_PADDING_X = 6;
3170
3216
  var LABEL_HEIGHT = 17;
3171
3217
  var LABEL_RADIUS = 4;
3172
3218
  var CULL_MARGIN_PX = 64;
3219
+ var PENDING_DASH = [6, 4];
3220
+ var PENDING_LINE_WIDTH = 2;
3221
+ var PENDING_TIP_RADIUS = 3.5;
3173
3222
  function traceRect(ctx, rect, radius) {
3174
3223
  ctx.beginPath();
3175
3224
  if (typeof ctx.roundRect === "function") {
@@ -3228,9 +3277,12 @@ function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
3228
3277
  ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
3229
3278
  const painted = /* @__PURE__ */ new Set();
3230
3279
  for (const peer of peers) {
3231
- const held = /* @__PURE__ */ new Set([...peer.holding, ...peer.transforms.keys()]);
3280
+ const held = /* @__PURE__ */ new Set([
3281
+ ...peer.holding.filter((key) => isNodeTarget(key)),
3282
+ ...peer.transforms.keys()
3283
+ ]);
3232
3284
  for (const id of peer.selection) {
3233
- if (held.has(id)) continue;
3285
+ if (!isNodeTarget(id) || held.has(id)) continue;
3234
3286
  const rect = getNodeRect(id);
3235
3287
  if (!rect) continue;
3236
3288
  const screen = projectRect(rect, viewport);
@@ -3247,7 +3299,7 @@ function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
3247
3299
  if (locks.size > 0) {
3248
3300
  const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
3249
3301
  for (const [id, holderId] of locks) {
3250
- if (painted.has(id)) continue;
3302
+ if (!isNodeTarget(id) || painted.has(id)) continue;
3251
3303
  const rect = getNodeRect(id);
3252
3304
  if (!rect) continue;
3253
3305
  const screen = projectRect(rect, viewport);
@@ -3257,6 +3309,69 @@ function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
3257
3309
  }
3258
3310
  ctx.restore();
3259
3311
  }
3312
+ function collectPeerEdges(peers, lockedEdges) {
3313
+ if (peers.length === 0 && lockedEdges.size === 0) return void 0;
3314
+ let claimed = null;
3315
+ const claim = (key, color, override) => {
3316
+ const edgeId = presenceTargetEdgeId(key);
3317
+ if (edgeId === null) return;
3318
+ claimed ??= /* @__PURE__ */ new Map();
3319
+ if (override || !claimed.has(edgeId)) claimed.set(edgeId, color);
3320
+ };
3321
+ for (const peer of peers) {
3322
+ for (const key of peer.selection) claim(key, peer.color, false);
3323
+ }
3324
+ for (const peer of peers) {
3325
+ for (const key of peer.holding) claim(key, peer.color, true);
3326
+ }
3327
+ if (lockedEdges.size > 0) {
3328
+ const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
3329
+ for (const [edgeId, holderId] of lockedEdges) {
3330
+ claimed ??= /* @__PURE__ */ new Map();
3331
+ if (!claimed.has(edgeId)) {
3332
+ claimed.set(edgeId, declared.get(holderId) ?? peerColor(holderId));
3333
+ }
3334
+ }
3335
+ }
3336
+ return claimed ?? void 0;
3337
+ }
3338
+ function drawPeerPending(ctx, peers, { viewport, size, handles }) {
3339
+ let saved = false;
3340
+ for (const peer of peers) {
3341
+ if (!peer.pending) continue;
3342
+ const from = worldToScreen(peer.pending.from, viewport);
3343
+ const to = worldToScreen(peer.pending.to, viewport);
3344
+ const bounds = {
3345
+ x: Math.min(from.x, to.x),
3346
+ y: Math.min(from.y, to.y),
3347
+ width: Math.abs(to.x - from.x),
3348
+ height: Math.abs(to.y - from.y)
3349
+ };
3350
+ if (!onScreen(bounds, size)) continue;
3351
+ if (!saved) {
3352
+ saved = true;
3353
+ ctx.save();
3354
+ ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
3355
+ ctx.lineWidth = PENDING_LINE_WIDTH;
3356
+ ctx.setLineDash(PENDING_DASH);
3357
+ }
3358
+ const points = buildPendingPath(peer.pending, handles, viewport);
3359
+ const [first, ...rest] = points;
3360
+ if (!first) continue;
3361
+ ctx.strokeStyle = peer.color;
3362
+ ctx.beginPath();
3363
+ ctx.moveTo(first.x, first.y);
3364
+ for (const point of rest) ctx.lineTo(point.x, point.y);
3365
+ ctx.stroke();
3366
+ ctx.setLineDash([]);
3367
+ ctx.fillStyle = peer.color;
3368
+ ctx.beginPath();
3369
+ ctx.arc(to.x, to.y, PENDING_TIP_RADIUS, 0, Math.PI * 2);
3370
+ ctx.fill();
3371
+ ctx.setLineDash(PENDING_DASH);
3372
+ }
3373
+ if (saved) ctx.restore();
3374
+ }
3260
3375
  function drawPeerCursors(ctx, peers, { viewport, size }) {
3261
3376
  ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
3262
3377
  ctx.clearRect(0, 0, size.width, size.height);
@@ -3327,6 +3442,7 @@ function drawAlignmentGuides(ctx, guides, viewport, size) {
3327
3442
  }
3328
3443
  var ENDPOINT_BUBBLE_RADIUS = 6;
3329
3444
  var HOVER_KEEP_TOLERANCE = 16;
3445
+ var EMPTY_EDGE_ANCHORS = /* @__PURE__ */ new Map();
3330
3446
  function drawEndpointBubbles(ctx, ends, size) {
3331
3447
  ctx.save();
3332
3448
  ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
@@ -3351,6 +3467,8 @@ function useRivetRuntime(params) {
3351
3467
  const [visibleIds, setVisibleIds] = useState([]);
3352
3468
  const reconnectRef = useRef(params);
3353
3469
  reconnectRef.current = params;
3470
+ const outlineModeRef = useRef(params.outlineMode);
3471
+ outlineModeRef.current = params.outlineMode;
3354
3472
  const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
3355
3473
  const visibleKeyRef = useRef("");
3356
3474
  const swimlaneLanesRef = useRef(swimlaneLanes);
@@ -3457,6 +3575,7 @@ function useRivetRuntime(params) {
3457
3575
  scrollToPan,
3458
3576
  schedule: () => schedule()
3459
3577
  });
3578
+ const canReconnect = (edgeId) => store.locks.size() === 0 || store.locks.allows(edgeTargetKey(edgeId), "reconnect");
3460
3579
  const render = () => {
3461
3580
  zoom.step();
3462
3581
  stepPresence();
@@ -3471,6 +3590,8 @@ function useRivetRuntime(params) {
3471
3590
  const edgeList = [...store.edges.values()];
3472
3591
  const pending = store.getPending();
3473
3592
  const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
3593
+ const outlineMode = outlineModeRef.current;
3594
+ const peerEdges = outlineMode === "none" ? void 0 : collectPeerEdges(store.presence.getPeers(), store.locks.getLockedEdges());
3474
3595
  edgeRenderer.draw(edgeList, store.nodes, viewport, {
3475
3596
  handles: store.handles,
3476
3597
  pending: null,
@@ -3479,24 +3600,35 @@ function useRivetRuntime(params) {
3479
3600
  worldPositions,
3480
3601
  displayedSides: store.getDisplayedSides(),
3481
3602
  liveAlignNodes,
3482
- anchorPlacement
3603
+ anchorPlacement,
3604
+ peerEdges
3483
3605
  });
3484
3606
  store.setEdgeLabelAnchors(edgeRenderer.getLabels());
3607
+ if (outlineMode === "dom") {
3608
+ store.setPeerEdgeAnchors(edgeRenderer.getPeerEdgeAnchors?.() ?? EMPTY_EDGE_ANCHORS);
3609
+ }
3485
3610
  foregroundRenderer.draw([], store.nodes, viewport, {
3486
3611
  handles: store.handles,
3487
3612
  pending
3488
3613
  });
3614
+ if (fgCtx && !store.presence.isEmpty()) {
3615
+ drawPeerPending(fgCtx, store.presence.getPeers(), {
3616
+ viewport,
3617
+ size,
3618
+ handles: store.handles
3619
+ });
3620
+ }
3489
3621
  if (fgCtx) drawAlignmentGuides(fgCtx, store.getAlignmentGuides(), viewport, size);
3490
3622
  if (fgCtx) {
3491
3623
  const affordanceId = hoveredEdgeId ?? store.getSelectedEdge();
3492
3624
  const edge = affordanceId ? store.edges.get(affordanceId) : void 0;
3493
3625
  const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
3494
- if (edge && reconnectable && edge.id !== pending?.reconnecting) {
3626
+ if (edge && reconnectable && edge.id !== pending?.reconnecting && canReconnect(edge.id)) {
3495
3627
  const ends = edgeRenderer.getEndpoints?.().get(edge.id);
3496
3628
  if (ends) drawEndpointBubbles(fgCtx, ends, size);
3497
3629
  }
3498
3630
  }
3499
- if (fgCtx && (!store.presence.isEmpty() || store.locks.size() > 0)) {
3631
+ if (fgCtx && outlineMode === "canvas" && (!store.presence.isEmpty() || store.locks.size() > 0)) {
3500
3632
  drawPeerOutlines(fgCtx, store.presence.getPeers(), {
3501
3633
  viewport,
3502
3634
  size,
@@ -3533,6 +3665,7 @@ function useRivetRuntime(params) {
3533
3665
  setVisibleIds(ids);
3534
3666
  }
3535
3667
  store.notifyFrame();
3668
+ store.notifyCursorFrame();
3536
3669
  };
3537
3670
  const tick = () => {
3538
3671
  frame = 0;
@@ -3546,6 +3679,7 @@ function useRivetRuntime(params) {
3546
3679
  cursorsDirty = false;
3547
3680
  stepPresence();
3548
3681
  drawCursorLayer();
3682
+ store.notifyCursorFrame();
3549
3683
  };
3550
3684
  const schedule = () => {
3551
3685
  dirty = true;
@@ -3594,6 +3728,7 @@ function useRivetRuntime(params) {
3594
3728
  return { x, y: base.y + size2.height / 2 };
3595
3729
  };
3596
3730
  const beginReconnect = (edge, end) => {
3731
+ if (!canReconnect(edge.id)) return;
3597
3732
  zoom.cancel();
3598
3733
  const fixedIsSource = end === "target";
3599
3734
  const fixedNodeId = fixedIsSource ? edge.source : edge.target;
@@ -3650,7 +3785,7 @@ function useRivetRuntime(params) {
3650
3785
  if (hit.edgeId !== hoveredEdgeId && hit.edgeId !== store.getSelectedEdge()) return null;
3651
3786
  const edge = store.edges.get(hit.edgeId);
3652
3787
  const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
3653
- return reconnectable ? hit : null;
3788
+ return reconnectable && canReconnect(hit.edgeId) ? hit : null;
3654
3789
  },
3655
3790
  begin: (edgeId, end) => {
3656
3791
  const edge = store.edges.get(edgeId);
@@ -3689,7 +3824,7 @@ function useRivetRuntime(params) {
3689
3824
  }
3690
3825
  const hitEdge = edgeRenderer.pick(px, py);
3691
3826
  if (hitEdge && store.edges.get(hitEdge)?.selectable !== false) {
3692
- store.selectEdge(hitEdge);
3827
+ if (store.locks.allows(edgeTargetKey(hitEdge), "select")) store.selectEdge(hitEdge);
3693
3828
  return;
3694
3829
  }
3695
3830
  store.selectNode(null);
@@ -4033,6 +4168,8 @@ var PICK_TOLERANCE = 6;
4033
4168
  var ENDPOINT_RADIUS = 12;
4034
4169
  var ARROW_SIZE = 9;
4035
4170
  var DASH_SPEED = 40;
4171
+ var PEER_HALO_EXTRA = 6;
4172
+ var PEER_HALO_ALPHA = 0.45;
4036
4173
  var Canvas2DEdgeRenderer = class {
4037
4174
  constructor(canvas, options = {}) {
4038
4175
  this.canvas = canvas;
@@ -4055,6 +4192,8 @@ var Canvas2DEdgeRenderer = class {
4055
4192
  endpoints = /* @__PURE__ */ new Map();
4056
4193
  /** Screen-space label anchor per edge that has a label. */
4057
4194
  labels = /* @__PURE__ */ new Map();
4195
+ /** Screen-space midpoint per edge somebody has claimed, for DOM claim chrome. */
4196
+ peerAnchors = /* @__PURE__ */ new Map();
4058
4197
  resize(width, height, dpr) {
4059
4198
  this.width = width;
4060
4199
  this.height = height;
@@ -4068,6 +4207,10 @@ var Canvas2DEdgeRenderer = class {
4068
4207
  getLabels() {
4069
4208
  return this.labels;
4070
4209
  }
4210
+ /** Screen-space midpoints for the edges a peer had claimed last frame. */
4211
+ getPeerEdgeAnchors() {
4212
+ return this.peerAnchors;
4213
+ }
4071
4214
  /** Screen-space endpoints per edge from the last frame (reconnect affordance). */
4072
4215
  getEndpoints() {
4073
4216
  return this.endpoints;
@@ -4082,11 +4225,23 @@ var Canvas2DEdgeRenderer = class {
4082
4225
  this.geometry.clear();
4083
4226
  this.endpoints.clear();
4084
4227
  this.labels.clear();
4228
+ this.peerAnchors.clear();
4085
4229
  const resolved = buildEdges(edges, nodes, viewport, this.edgeTypes, this.defaults, extras);
4230
+ const peerEdges = extras?.peerEdges;
4086
4231
  for (const edge of resolved) {
4087
4232
  this.geometry.set(edge.id, edge.points);
4088
4233
  this.endpoints.set(edge.id, { source: edge.source, target: edge.target });
4089
4234
  if (edge.label) this.labels.set(edge.id, edge.label);
4235
+ const peerColor2 = peerEdges?.get(edge.id);
4236
+ if (peerColor2) {
4237
+ this.peerAnchors.set(edge.id, polylineMidpoint(edge.points));
4238
+ ctx.strokeStyle = peerColor2;
4239
+ ctx.lineWidth = edge.width + PEER_HALO_EXTRA * 2;
4240
+ ctx.globalAlpha = PEER_HALO_ALPHA;
4241
+ ctx.setLineDash([]);
4242
+ ctx.lineDashOffset = 0;
4243
+ strokePolyline(ctx, edge.points);
4244
+ }
4090
4245
  ctx.strokeStyle = edge.stroke;
4091
4246
  ctx.lineWidth = edge.width;
4092
4247
  ctx.globalAlpha = edge.opacity;
@@ -4147,6 +4302,7 @@ var Canvas2DEdgeRenderer = class {
4147
4302
  this.geometry.clear();
4148
4303
  this.endpoints.clear();
4149
4304
  this.labels.clear();
4305
+ this.peerAnchors.clear();
4150
4306
  }
4151
4307
  };
4152
4308
  var canvas2DEdgeRenderer = (canvas, options) => new Canvas2DEdgeRenderer(canvas, options);
@@ -4256,7 +4412,6 @@ function EdgeLabelLayer() {
4256
4412
  }
4257
4413
 
4258
4414
  // src/input/group-drag.ts
4259
- var DRAG_THRESHOLD = 4;
4260
4415
  var ALIGN_THRESHOLD = 6;
4261
4416
  function startGroupDrag(params) {
4262
4417
  const { store, el, snapGrid, alignmentGuides, onLaneChange, onFrame } = params;
@@ -4281,6 +4436,7 @@ function startGroupDrag(params) {
4281
4436
  onFrame?.();
4282
4437
  };
4283
4438
  const beginDrag = () => {
4439
+ params.selectOnDrag?.();
4284
4440
  const movers = store.getMovers(store.getSelectedNodes()).filter((moverId) => store.locks.allows(moverId, "drag"));
4285
4441
  if (movers.length === 0) return false;
4286
4442
  started = true;
@@ -4409,6 +4565,37 @@ function clampToParent(store, parentId, position, size) {
4409
4565
  };
4410
4566
  return clampChildToParent(position, size, parentSize);
4411
4567
  }
4568
+
4569
+ // src/anchor/select-gesture.ts
4570
+ function attachAnchorSelection(store, record) {
4571
+ const element = record.element;
4572
+ if (!record.selectable || !(element instanceof HTMLElement || element instanceof SVGElement)) {
4573
+ return null;
4574
+ }
4575
+ const { nodeId, anchorId } = record;
4576
+ const previous = element.style.pointerEvents;
4577
+ element.style.pointerEvents = "auto";
4578
+ const onPointerDown = (event) => {
4579
+ if (!(event instanceof MouseEvent)) return;
4580
+ if (event.button !== 0) return;
4581
+ const originX = event.clientX;
4582
+ const originY = event.clientY;
4583
+ const onUp = (upEvent) => {
4584
+ window.removeEventListener("pointerup", onUp);
4585
+ if (!(upEvent instanceof MouseEvent)) return;
4586
+ const moved = Math.hypot(upEvent.clientX - originX, upEvent.clientY - originY);
4587
+ if (moved >= DRAG_THRESHOLD) return;
4588
+ if (!store.locks.allows(anchorTargetKey(nodeId, anchorId), "select")) return;
4589
+ store.setSelectedAnchors([{ nodeId, anchorId }]);
4590
+ };
4591
+ window.addEventListener("pointerup", onUp);
4592
+ };
4593
+ element.addEventListener("pointerdown", onPointerDown);
4594
+ return () => {
4595
+ element.removeEventListener("pointerdown", onPointerDown);
4596
+ element.style.pointerEvents = previous;
4597
+ };
4598
+ }
4412
4599
  var DEFAULT_ANCHOR_COLOR = "#6366f1";
4413
4600
  function NodeAnchors({ nodeId }) {
4414
4601
  const { store, anchorOptions } = useRivetContext();
@@ -4417,8 +4604,27 @@ function NodeAnchors({ nodeId }) {
4417
4604
  [store, nodeId]
4418
4605
  );
4419
4606
  const getAnchorsVersion = useCallback(() => store.getNodeAnchorsVersion(nodeId), [store, nodeId]);
4420
- useSyncExternalStore(subscribeAnchors, getAnchorsVersion, getAnchorsVersion);
4607
+ const anchorsVersion = useSyncExternalStore(
4608
+ subscribeAnchors,
4609
+ getAnchorsVersion,
4610
+ getAnchorsVersion
4611
+ );
4421
4612
  useSyncExternalStore(store.subscribeEdges, store.getEdgesVersion, store.getEdgesVersion);
4613
+ useSyncExternalStore(
4614
+ store.subscribeAnchorSelection,
4615
+ store.getSelectedAnchors,
4616
+ store.getSelectedAnchors
4617
+ );
4618
+ useEffect(() => {
4619
+ const cleanups = [];
4620
+ for (const record of store.getNodeAnchors(nodeId)) {
4621
+ const cleanup = attachAnchorSelection(store, record);
4622
+ if (cleanup) cleanups.push(cleanup);
4623
+ }
4624
+ return () => {
4625
+ for (const cleanup of cleanups) cleanup();
4626
+ };
4627
+ }, [store, nodeId, anchorsVersion]);
4422
4628
  const records = store.getNodeAnchors(nodeId);
4423
4629
  if (records.length === 0) return null;
4424
4630
  const dotsVisible = Boolean(store.nodes.get(nodeId)?.hovered);
@@ -4436,6 +4642,7 @@ function NodeAnchors({ nodeId }) {
4436
4642
  }
4437
4643
  }
4438
4644
  return /* @__PURE__ */ jsx(Fragment, { children: records.map((record) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
4645
+ store.isAnchorSelected(nodeId, record.anchorId) && /* @__PURE__ */ jsx(AnchorSelectionOutline, { record }),
4439
4646
  /* @__PURE__ */ jsx(AnchorDots, { record, options: anchorOptions, visible: dotsVisible }),
4440
4647
  record.strays !== "none" && /* @__PURE__ */ jsx(
4441
4648
  AnchorStrayHandles,
@@ -4447,6 +4654,31 @@ function NodeAnchors({ nodeId }) {
4447
4654
  )
4448
4655
  ] }, record.anchorId)) });
4449
4656
  }
4657
+ function AnchorSelectionOutline({ record }) {
4658
+ const geometry = record.geometry;
4659
+ if (!geometry) return null;
4660
+ const { dots } = geometry;
4661
+ const left = dots.left.xPct;
4662
+ const top = dots.top.yPct;
4663
+ return /* @__PURE__ */ jsx(
4664
+ "div",
4665
+ {
4666
+ "data-rivet-anchor-selected": record.anchorId,
4667
+ style: {
4668
+ position: "absolute",
4669
+ left: `${left}%`,
4670
+ top: `${top}%`,
4671
+ width: `${dots.right.xPct - left}%`,
4672
+ height: `${dots.bottom.yPct - top}%`,
4673
+ border: `1px solid ${record.color ?? DEFAULT_ANCHOR_COLOR}`,
4674
+ borderRadius: 2,
4675
+ // The box is a readout, never a target: presses belong to the element
4676
+ // underneath it, which is what put the selection here.
4677
+ pointerEvents: "none"
4678
+ }
4679
+ }
4680
+ );
4681
+ }
4450
4682
  function AnchorDots(props) {
4451
4683
  const { record, options } = props;
4452
4684
  if (!record.geometry || !record.element?.isConnected) return null;
@@ -4665,7 +4897,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4665
4897
  if (!noSelect) store.selectNode(id, true);
4666
4898
  return;
4667
4899
  }
4668
- if (!noSelect && !store.isNodeSelected(id)) store.selectNode(id);
4900
+ const onAnchor = target !== null && findSelectableAnchor(store.getNodeAnchors(id), target) !== null;
4901
+ const selectNode = () => {
4902
+ if (!noSelect && !store.isNodeSelected(id)) store.selectNode(id);
4903
+ };
4904
+ if (!onAnchor) selectNode();
4669
4905
  if (noDrag) return;
4670
4906
  startGroupDrag({
4671
4907
  store,
@@ -4678,7 +4914,8 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4678
4914
  clampToSwimlane,
4679
4915
  swimlaneMargin,
4680
4916
  swimlaneLabelWidth,
4681
- onLaneChange
4917
+ onLaneChange,
4918
+ selectOnDrag: onAnchor ? selectNode : void 0
4682
4919
  });
4683
4920
  };
4684
4921
  const node = store.nodes.get(id);
@@ -4835,6 +5072,176 @@ function NodeLayer({ containerRef, visibleIds }) {
4835
5072
  /* @__PURE__ */ jsx(NodesSelection, {})
4836
5073
  ] });
4837
5074
  }
5075
+ var overlayStyle2 = {
5076
+ position: "absolute",
5077
+ inset: 0,
5078
+ // Cursor chrome belongs to the canvas: clipped to the pane rather than
5079
+ // escaping into whatever app UI surrounds it.
5080
+ overflow: "hidden",
5081
+ pointerEvents: "none"
5082
+ };
5083
+ var cursorStyle = {
5084
+ position: "absolute",
5085
+ top: 0,
5086
+ left: 0,
5087
+ // The wrapper is a point, not a box: it sits at the pointer tip and whatever
5088
+ // the consumer renders lays itself out from there.
5089
+ width: 0,
5090
+ height: 0,
5091
+ pointerEvents: "none"
5092
+ };
5093
+ function PeerCursorLayer({
5094
+ component: Cursor
5095
+ }) {
5096
+ const { store, paneRef } = useRivetContext();
5097
+ const peers = usePeerIdentities();
5098
+ const refs = useRef(/* @__PURE__ */ new Map());
5099
+ const zoomRef = useRef(null);
5100
+ const zoomWritten = useRef(/* @__PURE__ */ new WeakSet());
5101
+ const position = useCallback(() => {
5102
+ const pane = paneRef.current;
5103
+ if (!pane) return;
5104
+ const viewport = store.getViewport();
5105
+ const zoomChanged = zoomRef.current !== viewport.zoom;
5106
+ zoomRef.current = viewport.zoom;
5107
+ const { width, height } = pane.getBoundingClientRect();
5108
+ const live = new Map(store.presence.getPeers().map((peer) => [peer.id, peer.cursor]));
5109
+ for (const [id, el] of refs.current) {
5110
+ if (!el) continue;
5111
+ const cursor = live.get(id);
5112
+ if (!cursor) {
5113
+ el.style.display = "none";
5114
+ continue;
5115
+ }
5116
+ el.style.display = "";
5117
+ const point = worldToScreen(cursor, viewport);
5118
+ const x = Math.min(Math.max(point.x, 0), width);
5119
+ const y = Math.min(Math.max(point.y, 0), height);
5120
+ const offscreen = x !== point.x || y !== point.y;
5121
+ el.style.transform = `translate(${x}px, ${y}px)`;
5122
+ el.style.visibility = offscreen ? "hidden" : "";
5123
+ if (offscreen) el.setAttribute("data-rivet-offscreen", "");
5124
+ else el.removeAttribute("data-rivet-offscreen");
5125
+ if (zoomChanged || !zoomWritten.current.has(el)) {
5126
+ el.style.setProperty("--rivet-zoom", String(viewport.zoom));
5127
+ zoomWritten.current.add(el);
5128
+ }
5129
+ }
5130
+ }, [store, paneRef]);
5131
+ useEffect(() => {
5132
+ return store.subscribeCursorFrame(position);
5133
+ }, [store, position]);
5134
+ useLayoutEffect(position);
5135
+ return /* @__PURE__ */ jsx("div", { style: overlayStyle2, children: peers.map((peer) => /* @__PURE__ */ jsx(
5136
+ "div",
5137
+ {
5138
+ ref: (el) => {
5139
+ if (el) refs.current.set(peer.id, el);
5140
+ else refs.current.delete(peer.id);
5141
+ },
5142
+ style: { ...cursorStyle, display: "none" },
5143
+ "data-rivet-peer-cursor": peer.id,
5144
+ children: /* @__PURE__ */ jsx(Cursor, { peer })
5145
+ },
5146
+ peer.id
5147
+ )) });
5148
+ }
5149
+ var overlayStyle3 = {
5150
+ position: "absolute",
5151
+ inset: 0,
5152
+ // Claim chrome belongs to the canvas, so a box that has been panned off the
5153
+ // pane is clipped rather than left hanging over the app around it.
5154
+ overflow: "hidden",
5155
+ pointerEvents: "none"
5156
+ };
5157
+ var wrapperStyle2 = {
5158
+ position: "absolute",
5159
+ top: 0,
5160
+ left: 0,
5161
+ pointerEvents: "none"
5162
+ };
5163
+ function projectRect2(rect, viewport) {
5164
+ const origin = worldToScreen({ x: rect.x, y: rect.y }, viewport);
5165
+ return {
5166
+ x: origin.x,
5167
+ y: origin.y,
5168
+ width: rect.width * viewport.zoom,
5169
+ height: rect.height * viewport.zoom
5170
+ };
5171
+ }
5172
+ function PeerOutlineLayer({
5173
+ component: Outline
5174
+ }) {
5175
+ const { store } = useRivetContext();
5176
+ const [entries, setEntries] = useState([]);
5177
+ const refs = useRef(/* @__PURE__ */ new Map());
5178
+ const zoomRef = useRef(null);
5179
+ const zoomWritten = useRef(/* @__PURE__ */ new WeakSet());
5180
+ const rectOf = useCallback(
5181
+ (target, viewport) => {
5182
+ if (target.kind === "edge") {
5183
+ const point = store.getPeerEdgeAnchors().get(target.id);
5184
+ return point ? { x: point.x, y: point.y, width: 0, height: 0 } : null;
5185
+ }
5186
+ const nodeId = target.kind === "node" ? target.id : target.nodeId;
5187
+ if (!store.nodes.has(nodeId)) return null;
5188
+ const node = store.getNodeRect(nodeId);
5189
+ if (target.kind === "node") return projectRect2(node, viewport);
5190
+ const record = store.anchors.get(handleKey(nodeId, target.anchorId));
5191
+ if (!record?.geometry) return null;
5192
+ return projectRect2(anchorRect(node, record.geometry), viewport);
5193
+ },
5194
+ [store]
5195
+ );
5196
+ const position = useCallback(() => {
5197
+ const viewport = store.getViewport();
5198
+ const zoomChanged = zoomRef.current !== viewport.zoom;
5199
+ zoomRef.current = viewport.zoom;
5200
+ for (const entry of entries) {
5201
+ const el = refs.current.get(entry.key);
5202
+ if (!el) continue;
5203
+ const rect = rectOf(entry.target, viewport);
5204
+ if (!rect) {
5205
+ el.style.display = "none";
5206
+ continue;
5207
+ }
5208
+ el.style.display = "";
5209
+ el.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
5210
+ el.style.width = `${rect.width}px`;
5211
+ el.style.height = `${rect.height}px`;
5212
+ if (zoomChanged || !zoomWritten.current.has(el)) {
5213
+ el.style.setProperty("--rivet-zoom", String(viewport.zoom));
5214
+ zoomWritten.current.add(el);
5215
+ }
5216
+ }
5217
+ }, [store, entries, rectOf]);
5218
+ useEffect(() => {
5219
+ return store.subscribeFrame(() => {
5220
+ const next = collectPeerOutlines(
5221
+ store.presence.getPeers(),
5222
+ store.presence.getPeerIdentities(),
5223
+ store.locks.getLocks()
5224
+ );
5225
+ setEntries((prev) => peerOutlinesEqual(prev, next) ? prev : next);
5226
+ position();
5227
+ });
5228
+ }, [store, position]);
5229
+ useLayoutEffect(position);
5230
+ return /* @__PURE__ */ jsx("div", { style: overlayStyle3, children: entries.map((entry) => /* @__PURE__ */ jsx(
5231
+ "div",
5232
+ {
5233
+ ref: (el) => {
5234
+ if (el) refs.current.set(entry.key, el);
5235
+ else refs.current.delete(entry.key);
5236
+ },
5237
+ style: { ...wrapperStyle2, display: "none" },
5238
+ "data-rivet-peer-outline": entry.key,
5239
+ "data-rivet-outline-kind": entry.target.kind,
5240
+ children: /* @__PURE__ */ jsx(Outline, { target: entry.target, claims: entry.claims, lock: entry.lock })
5241
+ },
5242
+ entry.key
5243
+ )) });
5244
+ }
4838
5245
  var containerStyle2 = {
4839
5246
  position: "absolute",
4840
5247
  inset: 0,
@@ -5282,6 +5689,7 @@ function Rivet({
5282
5689
  store.setLockedNodes(lockedNodes);
5283
5690
  }, [store, lockedNodes]);
5284
5691
  useLocalPresence({ store, paneRef, onLocalPresence, throttleMs: cfg.presenceOptions.throttleMs });
5692
+ const outlineMode = !cfg.presenceOptions.renderOutlines ? "none" : cfg.presenceOptions.outlineComponent ? "dom" : "canvas";
5285
5693
  const { visibleIds, controls, getViewportElements } = useRivetRuntime({
5286
5694
  store,
5287
5695
  paneRef,
@@ -5290,6 +5698,7 @@ function Rivet({
5290
5698
  edgeCanvasRef,
5291
5699
  foregroundCanvasRef,
5292
5700
  cursorCanvasRef,
5701
+ outlineMode,
5293
5702
  swimlaneLanes: swimlanes.lanes,
5294
5703
  edgeTypes: cfg.edgeTypes,
5295
5704
  defaultEdgeOptions: cfg.defaultEdgeOptions,
@@ -5382,7 +5791,8 @@ function Rivet({
5382
5791
  /* @__PURE__ */ jsx("canvas", { ref: edgeCanvasRef, style: canvasStyle }),
5383
5792
  /* @__PURE__ */ jsx(NodeLayer, { containerRef: nodeContainerRef, visibleIds }),
5384
5793
  /* @__PURE__ */ jsx("canvas", { ref: foregroundCanvasRef, style: canvasStyle }),
5385
- cfg.presenceOptions.renderCursors && /* @__PURE__ */ jsx("canvas", { ref: cursorCanvasRef, style: canvasStyle }),
5794
+ outlineMode === "dom" && cfg.presenceOptions.outlineComponent && /* @__PURE__ */ jsx(PeerOutlineLayer, { component: cfg.presenceOptions.outlineComponent }),
5795
+ cfg.presenceOptions.renderCursors && (cfg.presenceOptions.cursorComponent ? /* @__PURE__ */ jsx(PeerCursorLayer, { component: cfg.presenceOptions.cursorComponent }) : /* @__PURE__ */ jsx("canvas", { ref: cursorCanvasRef, style: canvasStyle })),
5386
5796
  /* @__PURE__ */ jsx(EdgeLabelLayer, {}),
5387
5797
  /* @__PURE__ */ jsx(SwimlaneOverlay, {}),
5388
5798
  children,
@@ -5480,8 +5890,12 @@ function buildInstance(store, controls, getViewportElements) {
5480
5890
  },
5481
5891
  unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
5482
5892
  remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
5893
+ getAnchor: (nodeId, anchorId) => store.getNodeAnchors(nodeId).find((record) => record.anchorId === anchorId) ?? null,
5894
+ setSelectedAnchors: (refs) => store.setSelectedAnchors(refs),
5895
+ getSelectedAnchors: () => store.getSelectedAnchors(),
5483
5896
  setPeerCursor: (peerId, point) => store.presence.setPeerCursor(peerId, point),
5484
5897
  setPeerNodeTransform: (peerId, nodeId, rect) => store.presence.setPeerNodeTransform(peerId, nodeId, rect),
5898
+ setPeerConnection: (peerId, connection) => store.presence.setPeerConnection(peerId, connection),
5485
5899
  removePeer: (peerId) => store.presence.removePeer(peerId),
5486
5900
  releaseNodeGesture: (id, ids) => store.endNodeGesture(id, ids),
5487
5901
  copy: (ids) => {
@@ -5523,6 +5937,6 @@ function useRivet() {
5523
5937
  // src/index.ts
5524
5938
  var VERSION = "0.0.0";
5525
5939
 
5526
- export { BUILTIN_EDGE_TYPES, BUILTIN_NODE_TYPES, Canvas2DEdgeRenderer, Controls, DEFAULT_ALIGNMENT_HYSTERESIS, DefaultNode, GroupNode, Handle, MiniMap, NodeResizer, Rivet, VERSION, alignRect, applyEdgeChanges, applyNodeChanges, boundingRect, canvas2DEdgeRenderer, clampChildToParent, cloneElements, createRivetStore, edgeEndpointHandle, facingSide, getBezierPath, getSmoothStepPath, getStepPath, getStraightPath, handleKey, parseEdgeEndpoint, parseSideHandleId, rectsIntersect, resolveFacingSide, resolveFacingSideAmong, screenToWorld, serializeGraph, snapToGrid, useRivet, viewportToCss, visibleWorldRect, worldToScreen, zoomAt };
5940
+ export { BUILTIN_EDGE_TYPES, BUILTIN_NODE_TYPES, Canvas2DEdgeRenderer, Controls, DEFAULT_ALIGNMENT_HYSTERESIS, DefaultNode, GroupNode, Handle, MiniMap, NodeResizer, Rivet, VERSION, alignRect, applyEdgeChanges, applyNodeChanges, canvas2DEdgeRenderer, cloneElements, createRivetStore, edgeEndpointHandle, facingSide, getBezierPath, getSmoothStepPath, getStepPath, getStraightPath, handleKey, parseEdgeEndpoint, parseSideHandleId, rectsIntersect, resolveFacingSide, resolveFacingSideAmong, screenToWorld, useRivet, viewportToCss, visibleWorldRect, worldToScreen, zoomAt };
5527
5941
  //# sourceMappingURL=index.js.map
5528
5942
  //# sourceMappingURL=index.js.map