@kolosal-ai/rivet 0.1.1 → 0.2.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,7 +1,9 @@
1
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-RG5ALD3X.js';
3
+ export { LOCK_DEFAULT_REFUSED, useNodeLock, useNodeLockAllows } from './chunk-RG5ALD3X.js';
4
+ import { clamp, clampNodeToLanes, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
2
5
  import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
3
6
  export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
4
- import { clampNodeToLanes, clamp, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
5
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
8
  import { memo, useRef, useState, useCallback, useSyncExternalStore, useMemo, useEffect, Fragment as Fragment$1, useId, useLayoutEffect } from 'react';
7
9
 
@@ -356,6 +358,35 @@ function parseEdgeEndpoint(nodeId, handleId, handles) {
356
358
  return side ? { nodeId, side } : { nodeId };
357
359
  }
358
360
 
361
+ // src/gesture-guard.ts
362
+ function keepLocalGeometry(incoming, local) {
363
+ return {
364
+ ...incoming,
365
+ position: local.position,
366
+ width: local.width,
367
+ height: local.height,
368
+ size: local.size
369
+ };
370
+ }
371
+ function guardNodeChanges(changes, isHeld, getLocal) {
372
+ if (!changes.some((change) => change.type !== "add" && isHeld(change.id))) return changes;
373
+ const guarded = [];
374
+ for (const change of changes) {
375
+ if (change.type === "add" || !isHeld(change.id)) {
376
+ guarded.push(change);
377
+ continue;
378
+ }
379
+ if (change.type === "position" || change.type === "dimensions") continue;
380
+ if (change.type === "replace") {
381
+ const local = getLocal(change.id);
382
+ guarded.push(local ? { ...change, item: keepLocalGeometry(change.item, local) } : change);
383
+ continue;
384
+ }
385
+ guarded.push(change);
386
+ }
387
+ return guarded;
388
+ }
389
+
359
390
  // src/graph.ts
360
391
  function clampChildToParent(position, childSize, parentSize) {
361
392
  const maxX = Math.max(0, parentSize.width - childSize.width);
@@ -522,6 +553,26 @@ var HistoryManager = class {
522
553
  this.last = this.snapshot();
523
554
  });
524
555
  }
556
+ /**
557
+ * Fold an external change into every stored snapshot.
558
+ *
559
+ * Snapshots are structural, so a restore rewrites the whole graph — which in a
560
+ * shared document means undoing past a peer's edit would silently erase it.
561
+ * {@link rebase} can't help: it only refreshes the rolling baseline, while the
562
+ * stale states are the ones already on the stacks. Replaying the incoming
563
+ * change over each of them keeps every stored step consistent with work this
564
+ * client doesn't own.
565
+ *
566
+ * The trade-off is that an undo touching a node a peer has since changed no
567
+ * longer moves it — the peer's newer value survives instead. That's the
568
+ * intended precedence: a local undo may drop its own effect, never someone
569
+ * else's edit.
570
+ */
571
+ rewrite(transform) {
572
+ this.undoStack = this.undoStack.map(transform);
573
+ this.redoStack = this.redoStack.map(transform);
574
+ this.last = transform(this.last);
575
+ }
525
576
  flush() {
526
577
  this.scheduled = false;
527
578
  this.undoStack.push(this.last);
@@ -575,12 +626,407 @@ var HistoryManager = class {
575
626
  }
576
627
  };
577
628
 
629
+ // src/viewport.ts
630
+ function worldToScreen(point, viewport) {
631
+ return {
632
+ x: point.x * viewport.zoom + viewport.x,
633
+ y: point.y * viewport.zoom + viewport.y
634
+ };
635
+ }
636
+ function screenToWorld(point, viewport) {
637
+ return {
638
+ x: (point.x - viewport.x) / viewport.zoom,
639
+ y: (point.y - viewport.y) / viewport.zoom
640
+ };
641
+ }
642
+ function zoomAt(viewport, anchor, nextZoom, minZoom = 0.1, maxZoom = 4) {
643
+ const zoom = clamp(nextZoom, minZoom, maxZoom);
644
+ const world = screenToWorld(anchor, viewport);
645
+ return {
646
+ zoom,
647
+ x: anchor.x - world.x * zoom,
648
+ y: anchor.y - world.y * zoom
649
+ };
650
+ }
651
+ function viewportToCss(viewport) {
652
+ return `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
653
+ }
654
+ function visibleWorldRect(viewport, width, height) {
655
+ const topLeft = screenToWorld({ x: 0, y: 0 }, viewport);
656
+ const bottomRight = screenToWorld({ x: width, y: height }, viewport);
657
+ return {
658
+ x: topLeft.x,
659
+ y: topLeft.y,
660
+ width: bottomRight.x - topLeft.x,
661
+ height: bottomRight.y - topLeft.y
662
+ };
663
+ }
664
+ function rectsIntersect(a, b) {
665
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
666
+ }
667
+
668
+ // src/renderer/edge-paths.ts
669
+ var SIDE_DIR = {
670
+ left: { x: -1, y: 0 },
671
+ right: { x: 1, y: 0 },
672
+ top: { x: 0, y: -1 },
673
+ bottom: { x: 0, y: 1 }
674
+ };
675
+ var BEZIER_SEGMENT_PX = 8;
676
+ var BEZIER_MIN_SAMPLES = 16;
677
+ var BEZIER_MAX_SAMPLES = 160;
678
+ var CORNER_RADIUS = 8;
679
+ var ARC_SAMPLES = 6;
680
+ function cubicAt(a, b, c, d, t) {
681
+ const mt = 1 - t;
682
+ return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
683
+ }
684
+ function quadAt(a, b, c, t) {
685
+ const mt = 1 - t;
686
+ return mt * mt * a + 2 * mt * t * b + t * t * c;
687
+ }
688
+ var getStraightPath = ({ sourceX, sourceY, targetX, targetY }) => ({
689
+ points: [
690
+ { x: sourceX, y: sourceY },
691
+ { x: targetX, y: targetY }
692
+ ],
693
+ labelX: (sourceX + targetX) / 2,
694
+ labelY: (sourceY + targetY) / 2
695
+ });
696
+ var getBezierPath = (params) => {
697
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
698
+ const sDir = SIDE_DIR[sourcePosition];
699
+ const tDir = SIDE_DIR[targetPosition];
700
+ const dist = Math.hypot(targetX - sourceX, targetY - sourceY);
701
+ const reach = Math.max(40, dist * 0.4);
702
+ const p0 = { x: sourceX, y: sourceY };
703
+ const p1 = { x: sourceX + sDir.x * reach, y: sourceY + sDir.y * reach };
704
+ const p2 = { x: targetX + tDir.x * reach, y: targetY + tDir.y * reach };
705
+ const p3 = { x: targetX, y: targetY };
706
+ const ctrlLen = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y) + Math.hypot(p3.x - p2.x, p3.y - p2.y);
707
+ const samples = Math.max(
708
+ BEZIER_MIN_SAMPLES,
709
+ Math.min(BEZIER_MAX_SAMPLES, Math.ceil(ctrlLen / BEZIER_SEGMENT_PX))
710
+ );
711
+ const points = [];
712
+ for (let i = 0; i <= samples; i++) {
713
+ const t = i / samples;
714
+ points.push({
715
+ x: cubicAt(p0.x, p1.x, p2.x, p3.x, t),
716
+ y: cubicAt(p0.y, p1.y, p2.y, p3.y, t)
717
+ });
718
+ }
719
+ return {
720
+ points,
721
+ labelX: cubicAt(p0.x, p1.x, p2.x, p3.x, 0.5),
722
+ labelY: cubicAt(p0.y, p1.y, p2.y, p3.y, 0.5)
723
+ };
724
+ };
725
+ var STEP_OFFSET = 20;
726
+ function simplifyCorners(corners) {
727
+ const out = [];
728
+ for (const p of corners) {
729
+ const a = out[out.length - 2];
730
+ const b = out[out.length - 1];
731
+ if (b && b.x === p.x && b.y === p.y) continue;
732
+ if (a && b && (a.x === b.x && b.x === p.x || a.y === b.y && b.y === p.y)) out.pop();
733
+ out.push(p);
734
+ }
735
+ return out;
736
+ }
737
+ function stepCorners(params) {
738
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
739
+ const s = { x: sourceX, y: sourceY };
740
+ const t = { x: targetX, y: targetY };
741
+ const sDir = SIDE_DIR[sourcePosition];
742
+ const tDir = SIDE_DIR[targetPosition];
743
+ const sg = { x: sourceX + sDir.x * STEP_OFFSET, y: sourceY + sDir.y * STEP_OFFSET };
744
+ const tg = { x: targetX + tDir.x * STEP_OFFSET, y: targetY + tDir.y * STEP_OFFSET };
745
+ return simplifyCorners(routeCorners(s, t, sg, tg, sDir, tDir));
746
+ }
747
+ function routeCorners(s, t, sg, tg, sDir, tDir) {
748
+ const sHoriz = sDir.x !== 0;
749
+ const tHoriz = tDir.x !== 0;
750
+ if (sHoriz && tHoriz) {
751
+ if (sDir.x * tDir.x < 0) {
752
+ if ((t.x - s.x) * sDir.x >= 2 * STEP_OFFSET) {
753
+ const midX = (s.x + t.x) / 2;
754
+ return [s, { x: midX, y: s.y }, { x: midX, y: t.y }, t];
755
+ }
756
+ const midY = (s.y + t.y) / 2;
757
+ return [s, sg, { x: sg.x, y: midY }, { x: tg.x, y: midY }, tg, t];
758
+ }
759
+ const railX = sDir.x > 0 ? Math.max(sg.x, tg.x) : Math.min(sg.x, tg.x);
760
+ return [s, { x: railX, y: s.y }, { x: railX, y: t.y }, t];
761
+ }
762
+ if (!sHoriz && !tHoriz) {
763
+ if (sDir.y * tDir.y < 0) {
764
+ if ((t.y - s.y) * sDir.y >= 2 * STEP_OFFSET) {
765
+ const midY = (s.y + t.y) / 2;
766
+ return [s, { x: s.x, y: midY }, { x: t.x, y: midY }, t];
767
+ }
768
+ const midX = (s.x + t.x) / 2;
769
+ return [s, sg, { x: midX, y: sg.y }, { x: midX, y: tg.y }, tg, t];
770
+ }
771
+ const railY = sDir.y > 0 ? Math.max(sg.y, tg.y) : Math.min(sg.y, tg.y);
772
+ return [s, { x: s.x, y: railY }, { x: t.x, y: railY }, t];
773
+ }
774
+ const corner = sHoriz ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
775
+ const exitsSource = sHoriz ? (corner.x - s.x) * sDir.x >= STEP_OFFSET : (corner.y - s.y) * sDir.y >= STEP_OFFSET;
776
+ const exitsTarget = tHoriz ? (corner.x - t.x) * tDir.x >= STEP_OFFSET : (corner.y - t.y) * tDir.y >= STEP_OFFSET;
777
+ if (exitsSource && exitsTarget) return [s, corner, t];
778
+ const bend = sHoriz ? { x: sg.x, y: tg.y } : { x: tg.x, y: sg.y };
779
+ return [s, sg, bend, tg, t];
780
+ }
781
+ function roundCorners(corners, radius) {
782
+ if (corners.length <= 2) return corners;
783
+ const points = [];
784
+ const first = corners[0];
785
+ if (first) points.push(first);
786
+ for (let i = 1; i < corners.length - 1; i++) {
787
+ const prev = corners[i - 1];
788
+ const curr = corners[i];
789
+ const next = corners[i + 1];
790
+ if (!prev || !curr || !next) continue;
791
+ const inLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
792
+ const outLen = Math.hypot(next.x - curr.x, next.y - curr.y);
793
+ const r = Math.min(radius, inLen / 2, outLen / 2);
794
+ const inPt = {
795
+ x: curr.x + (prev.x - curr.x) / (inLen || 1) * r,
796
+ y: curr.y + (prev.y - curr.y) / (inLen || 1) * r
797
+ };
798
+ const outPt = {
799
+ x: curr.x + (next.x - curr.x) / (outLen || 1) * r,
800
+ y: curr.y + (next.y - curr.y) / (outLen || 1) * r
801
+ };
802
+ points.push(inPt);
803
+ for (let j = 1; j < ARC_SAMPLES; j++) {
804
+ const tt = j / ARC_SAMPLES;
805
+ points.push({
806
+ x: quadAt(inPt.x, curr.x, outPt.x, tt),
807
+ y: quadAt(inPt.y, curr.y, outPt.y, tt)
808
+ });
809
+ }
810
+ points.push(outPt);
811
+ }
812
+ const last = corners[corners.length - 1];
813
+ if (last) points.push(last);
814
+ return points;
815
+ }
816
+ function stepLabel(corners, params) {
817
+ const i = Math.floor((corners.length - 1) / 2);
818
+ const a = corners[i];
819
+ const b = corners[i + 1];
820
+ if (!a || !b) return { x: params.sourceX, y: params.sourceY };
821
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
822
+ }
823
+ var getSmoothStepPath = (params) => {
824
+ const corners = stepCorners(params);
825
+ const label = stepLabel(corners, params);
826
+ return { points: roundCorners(corners, CORNER_RADIUS), labelX: label.x, labelY: label.y };
827
+ };
828
+ var getStepPath = (params) => {
829
+ const corners = stepCorners(params);
830
+ const label = stepLabel(corners, params);
831
+ return { points: corners, labelX: label.x, labelY: label.y };
832
+ };
833
+ var BUILTIN_EDGE_TYPES = {
834
+ bezier: getBezierPath,
835
+ smoothstep: getSmoothStepPath,
836
+ step: getStepPath,
837
+ straight: getStraightPath
838
+ };
839
+
840
+ // src/renderer/edge-geometry.ts
841
+ var EDGE_COLOR = "rgba(100, 116, 139, 0.75)";
842
+ var EDGE_COLOR_SELECTED = "#6366f1";
843
+ var EDGE_COLOR_HOVERED = "rgba(99, 102, 241, 0.9)";
844
+ var EDGE_WIDTH = 1.5;
845
+ var EDGE_WIDTH_HOVERED = 2.5;
846
+ var PENDING_COLOR = "#6366f1";
847
+ var OPPOSITE = {
848
+ left: "right",
849
+ right: "left",
850
+ top: "bottom",
851
+ bottom: "top"
852
+ };
853
+ var falseToNull = (m) => m || null;
854
+ function buildEdges(edges, nodes, viewport, edgeTypes, defaults, extras) {
855
+ const handles = extras?.handles;
856
+ const hovered = extras?.hovered;
857
+ const reconnecting = extras?.reconnecting;
858
+ const worldPositions = extras?.worldPositions;
859
+ const resolved = [];
860
+ for (const edge of edges) {
861
+ if (edge.id === reconnecting) continue;
862
+ const source = nodes.get(edge.source);
863
+ const target = nodes.get(edge.target);
864
+ if (!source || !target) continue;
865
+ const frame = { handles, viewport, worldPositions, extras };
866
+ const start = resolveEnd(edge, "source", source, target, frame);
867
+ const end = resolveEnd(edge, "target", target, source, frame);
868
+ const params = {
869
+ sourceX: start.point.x,
870
+ sourceY: start.point.y,
871
+ targetX: end.point.x,
872
+ targetY: end.point.y,
873
+ sourcePosition: start.position,
874
+ targetPosition: end.position
875
+ };
876
+ const pathFn = edgeTypes[edge.type ?? defaults.type ?? "bezier"] ?? getBezierPath;
877
+ const path = pathFn(params);
878
+ if (path.points.length < 2) continue;
879
+ const style = { ...defaults.style, ...edge.style };
880
+ const isHovered = edge.id === hovered;
881
+ resolved.push({
882
+ id: edge.id,
883
+ points: path.points,
884
+ source: start.point,
885
+ target: end.point,
886
+ label: edge.label !== void 0 ? { x: path.labelX, y: path.labelY } : void 0,
887
+ stroke: edge.selected ? EDGE_COLOR_SELECTED : isHovered ? EDGE_COLOR_HOVERED : style.stroke ?? EDGE_COLOR,
888
+ width: style.strokeWidth ?? (isHovered ? EDGE_WIDTH_HOVERED : EDGE_WIDTH),
889
+ opacity: style.opacity ?? 1,
890
+ animated: edge.animated ?? defaults.animated ?? false,
891
+ dash: style.strokeDasharray,
892
+ markerStart: falseToNull(edge.markerStart ?? defaults.markerStart),
893
+ markerEnd: falseToNull(edge.markerEnd ?? defaults.markerEnd)
894
+ });
895
+ }
896
+ return resolved;
897
+ }
898
+ function buildPendingPath(pending, handles, viewport) {
899
+ const record = handles?.get(handleKey(pending.source, pending.sourceHandle));
900
+ const sourcePosition = record?.position ?? (pending.sourceType === "target" ? "left" : "right");
901
+ const from = worldToScreen(pending.from, viewport);
902
+ const to = worldToScreen(pending.to, viewport);
903
+ const targetPosition = pending.toPosition ?? OPPOSITE[sourcePosition];
904
+ return getBezierPath({
905
+ sourceX: from.x,
906
+ sourceY: from.y,
907
+ targetX: to.x,
908
+ targetY: to.y,
909
+ sourcePosition,
910
+ targetPosition
911
+ }).points;
912
+ }
913
+ function pointOnSide(rect, side, alongPct = 50) {
914
+ const t = alongPct / 100;
915
+ switch (side) {
916
+ case "left":
917
+ return { x: rect.x, y: rect.y + rect.height * t };
918
+ case "right":
919
+ return { x: rect.x + rect.width, y: rect.y + rect.height * t };
920
+ case "top":
921
+ return { x: rect.x + rect.width * t, y: rect.y };
922
+ case "bottom":
923
+ return { x: rect.x + rect.width * t, y: rect.y + rect.height };
924
+ }
925
+ }
926
+ function nodeRect(node, worldPositions) {
927
+ const origin = worldPositions?.get(node.id) ?? node.position;
928
+ const size = node.size ?? DEFAULT_NODE_SIZE;
929
+ return { x: origin.x, y: origin.y, width: size.width, height: size.height };
930
+ }
931
+ var WANTED_TYPES = {
932
+ source: ["source", "either"],
933
+ target: ["target", "either"]
934
+ };
935
+ function handleOnSide(nodeId, side, role, handles) {
936
+ if (!handles) return null;
937
+ for (const record of handles.values()) {
938
+ if (record.nodeId === nodeId && record.position === side && WANTED_TYPES[role].includes(record.type)) {
939
+ return record;
940
+ }
941
+ }
942
+ return null;
943
+ }
944
+ function reachableSides(nodeId, role, handles) {
945
+ if (!handles) return [];
946
+ const sides = [];
947
+ for (const record of handles.values()) {
948
+ if (record.nodeId !== nodeId) continue;
949
+ if (!WANTED_TYPES[role].includes(record.type)) continue;
950
+ if (!sides.includes(record.position)) sides.push(record.position);
951
+ }
952
+ return sides;
953
+ }
954
+ function resolveEnd(edge, role, node, peer, frame) {
955
+ const { handles, viewport, worldPositions, extras } = frame;
956
+ const handleId = role === "source" ? edge.sourceHandle : edge.targetHandle;
957
+ const rect = nodeRect(node, worldPositions);
958
+ const anchorId = parseAnchorAutoHandleId(handleId) ?? parseAnchorHandleId(handleId)?.anchorId ?? null;
959
+ const endAt = (side2) => {
960
+ const alongPct = (anchorId ? extras?.anchorPlacement?.(node.id, anchorId, side2) : null) ?? void 0;
961
+ if (alongPct === void 0 && !anchorId) {
962
+ const candidate = handleOnSide(node.id, side2, role, handles);
963
+ if (candidate) {
964
+ return {
965
+ point: worldToScreen(
966
+ { x: rect.x + candidate.offset.x, y: rect.y + candidate.offset.y },
967
+ viewport
968
+ ),
969
+ position: side2
970
+ };
971
+ }
972
+ }
973
+ return { point: worldToScreen(pointOnSide(rect, side2, alongPct), viewport), position: side2 };
974
+ };
975
+ const registered = handleId && handles ? handles.get(handleKey(node.id, handleId)) : void 0;
976
+ const pinnedSide = parseAnchorHandleId(handleId)?.side ?? registered?.position ?? parseSideHandleId(handleId);
977
+ const live = extras?.liveAlignNodes !== void 0 && (extras.liveAlignNodes.has(edge.source) || extras.liveAlignNodes.has(edge.target)) && edge.source !== edge.target;
978
+ if (pinnedSide && !live) {
979
+ if (registered) {
980
+ return {
981
+ point: worldToScreen(
982
+ { x: rect.x + registered.offset.x, y: rect.y + registered.offset.y },
983
+ viewport
984
+ ),
985
+ position: registered.position
986
+ };
987
+ }
988
+ return endAt(pinnedSide);
989
+ }
990
+ const key = displayedSideKey(edge.id, role);
991
+ const side = resolveFacingSideAmong(
992
+ rect,
993
+ nodeRect(peer, worldPositions),
994
+ anchorId ? [] : reachableSides(node.id, role, handles),
995
+ extras?.displayedSides?.get(key)
996
+ );
997
+ extras?.displayedSides?.set(key, side);
998
+ return endAt(side);
999
+ }
1000
+ function distanceToPolyline(point, points) {
1001
+ let best = Number.POSITIVE_INFINITY;
1002
+ for (let i = 0; i < points.length - 1; i++) {
1003
+ const a = points[i];
1004
+ const b = points[i + 1];
1005
+ if (!a || !b) continue;
1006
+ best = Math.min(best, distanceToSegment(point, a, b));
1007
+ }
1008
+ return best;
1009
+ }
1010
+ function distanceToSegment(p, a, b) {
1011
+ const dx = b.x - a.x;
1012
+ const dy = b.y - a.y;
1013
+ const lenSq = dx * dx + dy * dy;
1014
+ if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
1015
+ let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
1016
+ t = Math.max(0, Math.min(1, t));
1017
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
1018
+ }
1019
+
578
1020
  // src/store.ts
579
1021
  var RivetGraphStore = class {
580
1022
  nodes = /* @__PURE__ */ new Map();
581
1023
  edges = /* @__PURE__ */ new Map();
582
1024
  handles = /* @__PURE__ */ new Map();
583
1025
  anchors = /* @__PURE__ */ new Map();
1026
+ // Arrow-wrapped so the (later-initialized) requestRender field is looked up
1027
+ // at call time, not while this field is being constructed.
1028
+ presence = createPresenceRegistry(() => this.requestRender());
1029
+ locks = createLockRegistry(() => this.requestRender());
584
1030
  viewport;
585
1031
  viewportClamp = null;
586
1032
  pending = null;
@@ -606,6 +1052,11 @@ var RivetGraphStore = class {
606
1052
  nodeResizing = false;
607
1053
  edgeAlignment = "manual";
608
1054
  draggingNodeIds = /* @__PURE__ */ new Set();
1055
+ heldNodeIds = /* @__PURE__ */ new Set();
1056
+ heldListeners = /* @__PURE__ */ new Set();
1057
+ gestureStartHandler = null;
1058
+ gestureEndHandler = null;
1059
+ lockConflictHandler = null;
609
1060
  displayedSides = /* @__PURE__ */ new Map();
610
1061
  edgeLabelAnchors = /* @__PURE__ */ new Map();
611
1062
  alignmentGuides = [];
@@ -615,6 +1066,10 @@ var RivetGraphStore = class {
615
1066
  history = new HistoryManager(() => this.historySnapshot());
616
1067
  lastSelectionKey;
617
1068
  worldCache = null;
1069
+ /** {@link worldCache} with peers' live gesture boxes folded in. */
1070
+ peerWorldCache = null;
1071
+ /** Presence transform version {@link peerWorldCache} was built from. */
1072
+ peerWorldVersion = -1;
618
1073
  renderRequester = () => {
619
1074
  };
620
1075
  constructor(init) {
@@ -629,6 +1084,7 @@ var RivetGraphStore = class {
629
1084
  // --- internal bookkeeping ------------------------------------------------
630
1085
  invalidateWorld() {
631
1086
  this.worldCache = null;
1087
+ this.peerWorldCache = null;
632
1088
  }
633
1089
  /**
634
1090
  * The single node-map write path. Invalidates the world-position cache exactly
@@ -666,17 +1122,22 @@ var RivetGraphStore = class {
666
1122
  const selection = this.buildSelection();
667
1123
  for (const listener of this.selectionListeners) listener(selection);
668
1124
  }
1125
+ notifyHeld() {
1126
+ this.peerWorldCache = null;
1127
+ this.requestRender();
1128
+ for (const listener of this.heldListeners) listener();
1129
+ }
669
1130
  /**
670
1131
  * The one emission policy. Unless a `reconcile` is applying incoming props
671
1132
  * (which must never echo them back out), record the batch to history — when
672
1133
  * `record` is set — then hand it to the controlled consumer. Recording happens
673
1134
  * before the handler so history works in uncontrolled mode too.
674
1135
  */
675
- emit(changes, handler, record) {
1136
+ emit(changes, handler, record, origin = "local") {
676
1137
  if (this.reconciling || changes.length === 0) return;
677
1138
  if (record) this.history.record(hasRecordableChange(changes));
678
1139
  else if (hasRecordableChange(changes)) this.history.rebase();
679
- handler?.(changes);
1140
+ handler?.(changes, { origin });
680
1141
  }
681
1142
  emitNodeChanges(changes, record = true) {
682
1143
  this.emit(changes, this.nodeChangeHandler, record);
@@ -821,10 +1282,11 @@ var RivetGraphStore = class {
821
1282
  this.writeNode(node.id, node);
822
1283
  continue;
823
1284
  }
824
- if (nodeControlledEqual(existing, node)) continue;
825
- this.writeNode(node.id, existing.hovered ? { ...node, hovered: true } : node);
1285
+ const settled = this.heldNodeIds.has(node.id) ? keepLocalGeometry(node, existing) : node;
1286
+ if (nodeControlledEqual(existing, settled)) continue;
1287
+ this.writeNode(node.id, existing.hovered ? { ...settled, hovered: true } : settled);
826
1288
  this.bumpNode(node.id);
827
- if (existing.position.x !== node.position.x || existing.position.y !== node.position.y) {
1289
+ if (existing.position.x !== settled.position.x || existing.position.y !== settled.position.y) {
828
1290
  moved.push(node.id);
829
1291
  }
830
1292
  }
@@ -917,12 +1379,47 @@ var RivetGraphStore = class {
917
1379
  this.worldCache = /* @__PURE__ */ new Map();
918
1380
  for (const id of this.nodes.keys()) this.worldCache.set(id, worldPosition(this.nodes, id));
919
1381
  }
920
- return this.worldCache;
1382
+ const version = this.presence.getTransformVersion();
1383
+ if (this.presence.getNodeTransforms().size === 0) return this.worldCache;
1384
+ if (this.peerWorldCache && this.peerWorldVersion === version) return this.peerWorldCache;
1385
+ this.peerWorldVersion = version;
1386
+ this.peerWorldCache = this.withPeerGestures(this.worldCache);
1387
+ return this.peerWorldCache;
921
1388
  };
1389
+ /**
1390
+ * Fold peers' live gesture boxes into world positions.
1391
+ *
1392
+ * A node somebody else is dragging is drawn, hit-tested and wired at the box
1393
+ * they last sent — the node moves, rather than a ghost of it appearing
1394
+ * elsewhere. The graph underneath is untouched: nothing here is written back,
1395
+ * so their drag never enters the document, the change stream, or history, and
1396
+ * the node lands for real when their commit arrives.
1397
+ *
1398
+ * Descendants ride along by the same delta, since their own positions are
1399
+ * relative to a parent this map has just moved.
1400
+ */
1401
+ withPeerGestures(base) {
1402
+ const merged = new Map(base);
1403
+ for (const [id, rect] of this.presence.getNodeTransforms()) {
1404
+ if (this.heldNodeIds.has(id)) continue;
1405
+ const origin = base.get(id);
1406
+ if (!origin) continue;
1407
+ const dx = rect.x - origin.x;
1408
+ const dy = rect.y - origin.y;
1409
+ if (dx === 0 && dy === 0) continue;
1410
+ merged.set(id, { x: rect.x, y: rect.y });
1411
+ for (const childId of this.getDescendantIds(id)) {
1412
+ const child = merged.get(childId);
1413
+ if (child) merged.set(childId, { x: child.x + dx, y: child.y + dy });
1414
+ }
1415
+ }
1416
+ return merged;
1417
+ }
922
1418
  getNodeWorldPosition = (id) => this.getWorldPositions().get(id) ?? { x: 0, y: 0 };
923
1419
  getNodeRect = (id) => {
924
1420
  const origin = this.getNodeWorldPosition(id);
925
- const size = this.nodes.get(id)?.size ?? DEFAULT_NODE_SIZE;
1421
+ const live = this.heldNodeIds.has(id) ? void 0 : this.presence.getNodeTransforms().get(id);
1422
+ const size = live ?? this.nodes.get(id)?.size ?? DEFAULT_NODE_SIZE;
926
1423
  return { x: origin.x, y: origin.y, width: size.width, height: size.height };
927
1424
  };
928
1425
  getNodeDepth = (id) => nodeDepth(this.nodes, id);
@@ -1025,6 +1522,16 @@ var RivetGraphStore = class {
1025
1522
  * candidate (single-handle nodes, custom layouts, a culled peer whose
1026
1523
  * handles are unregistered) means the pin is kept.
1027
1524
  */
1525
+ /**
1526
+ * The side of `nodeId` that faces `peerRect` and can actually be reached by
1527
+ * this end. An anchor end is unconstrained — its stray places itself along
1528
+ * whichever side faces the peer — and so is a node with no compatible handle
1529
+ * at all, which attaches at side midpoints.
1530
+ */
1531
+ reachableFacingSide(nodeId, handleId, rect, peerRect, end) {
1532
+ const allowed = parseAnchorHandleId(handleId) || parseAnchorAutoHandleId(handleId) ? [] : reachableSides(nodeId, end, this.handles);
1533
+ return resolveFacingSideAmong(rect, peerRect, allowed);
1534
+ }
1028
1535
  realignEndHandle(nodeId, handleId, side, end) {
1029
1536
  if (!handleId || parseAnchorAutoHandleId(handleId)) return handleId;
1030
1537
  const anchor = parseAnchorHandleId(handleId);
@@ -1066,8 +1573,8 @@ var RivetGraphStore = class {
1066
1573
  const targetRect = this.getNodeRect(edge.target);
1067
1574
  const sourceKey = displayedSideKey(edgeId, "source");
1068
1575
  const targetKey = displayedSideKey(edgeId, "target");
1069
- const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? facingSide(sourceRect, targetRect);
1070
- const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ?? facingSide(targetRect, sourceRect);
1576
+ const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? this.reachableFacingSide(edge.source, edge.sourceHandle, sourceRect, targetRect, "source");
1577
+ const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ?? this.reachableFacingSide(edge.target, edge.targetHandle, targetRect, sourceRect, "target");
1071
1578
  const sourceHandle = this.realignEndHandle(
1072
1579
  edge.source,
1073
1580
  edge.sourceHandle,
@@ -1102,6 +1609,42 @@ var RivetGraphStore = class {
1102
1609
  if (!dragging) this.draggingNodeIds.clear();
1103
1610
  };
1104
1611
  isNodeResizing = () => this.nodeResizing;
1612
+ setGestureHandlers = (handlers) => {
1613
+ this.gestureStartHandler = handlers.start ?? null;
1614
+ this.gestureEndHandler = handlers.end ?? null;
1615
+ this.lockConflictHandler = handlers.conflict ?? null;
1616
+ };
1617
+ setLockedNodes = (locks) => {
1618
+ const opened = this.locks.setLocks(locks);
1619
+ if (opened.length === 0 || this.heldNodeIds.size === 0) return;
1620
+ const held = [...this.heldNodeIds];
1621
+ for (const nodeId of opened) {
1622
+ if (!this.heldNodeIds.has(nodeId)) continue;
1623
+ const holderId = this.locks.getHolder(nodeId);
1624
+ if (holderId) this.lockConflictHandler?.({ nodeId, holderId, ids: held });
1625
+ }
1626
+ };
1627
+ isNodeHeld = (id) => this.heldNodeIds.has(id);
1628
+ getHeldNodeIds = () => this.heldNodeIds;
1629
+ beginNodeGesture = (id, ids = [id]) => {
1630
+ const opened = ids.filter((nodeId) => !this.heldNodeIds.has(nodeId));
1631
+ if (opened.length === 0) return;
1632
+ for (const nodeId of opened) this.heldNodeIds.add(nodeId);
1633
+ this.gestureStartHandler?.({ id, ids: [...ids] });
1634
+ this.notifyHeld();
1635
+ };
1636
+ endNodeGesture = (id, ids = [id]) => {
1637
+ const closed = ids.filter((nodeId) => this.heldNodeIds.delete(nodeId));
1638
+ if (closed.length === 0) return;
1639
+ this.gestureEndHandler?.({ id, ids: [...ids] });
1640
+ this.notifyHeld();
1641
+ };
1642
+ subscribeHeldNodes = (listener) => {
1643
+ this.heldListeners.add(listener);
1644
+ return () => {
1645
+ this.heldListeners.delete(listener);
1646
+ };
1647
+ };
1105
1648
  selectNode = (id, additive = false) => {
1106
1649
  this.setSelectionBoxActive(false);
1107
1650
  if (id === null) {
@@ -1220,6 +1763,7 @@ var RivetGraphStore = class {
1220
1763
  }
1221
1764
  if (this.selectedNodeIds.size > 0) {
1222
1765
  for (const id of [...this.selectedNodeIds]) {
1766
+ if (!this.locks.allows(id, "delete")) continue;
1223
1767
  if (this.removeNodeInternal(id, removedEdges)) removedNodes.push(id);
1224
1768
  }
1225
1769
  }
@@ -1249,6 +1793,19 @@ var RivetGraphStore = class {
1249
1793
  this.history.rebase();
1250
1794
  this.requestRender();
1251
1795
  };
1796
+ applyRemote = ({ nodes = [], edges = [], origin = "remote" }) => {
1797
+ const guarded = guardNodeChanges(nodes, this.isNodeHeld, (id) => this.nodes.get(id));
1798
+ if (guarded.length === 0 && edges.length === 0) return;
1799
+ const currentNodes = [...this.nodes.values()];
1800
+ const currentEdges = [...this.edges.values()];
1801
+ this.reconcile(applyNodeChanges(guarded, currentNodes), applyEdgeChanges(edges, currentEdges));
1802
+ this.history.rewrite((snapshot) => ({
1803
+ nodes: guarded.length > 0 ? applyNodeChanges(guarded, snapshot.nodes) : snapshot.nodes,
1804
+ edges: edges.length > 0 ? applyEdgeChanges(edges, snapshot.edges) : snapshot.edges
1805
+ }));
1806
+ this.emit(guarded, this.nodeChangeHandler, false, origin);
1807
+ this.emit(edges, this.edgeChangeHandler, false, origin);
1808
+ };
1252
1809
  // --- undo / redo ---------------------------------------------------------
1253
1810
  /** Structural snapshot for history — no selection/hover/drag state. */
1254
1811
  historySnapshot() {
@@ -1465,45 +2022,6 @@ function createRivetStore(init) {
1465
2022
  return new RivetGraphStore(init);
1466
2023
  }
1467
2024
 
1468
- // src/viewport.ts
1469
- function worldToScreen(point, viewport) {
1470
- return {
1471
- x: point.x * viewport.zoom + viewport.x,
1472
- y: point.y * viewport.zoom + viewport.y
1473
- };
1474
- }
1475
- function screenToWorld(point, viewport) {
1476
- return {
1477
- x: (point.x - viewport.x) / viewport.zoom,
1478
- y: (point.y - viewport.y) / viewport.zoom
1479
- };
1480
- }
1481
- function zoomAt(viewport, anchor, nextZoom, minZoom = 0.1, maxZoom = 4) {
1482
- const zoom = clamp(nextZoom, minZoom, maxZoom);
1483
- const world = screenToWorld(anchor, viewport);
1484
- return {
1485
- zoom,
1486
- x: anchor.x - world.x * zoom,
1487
- y: anchor.y - world.y * zoom
1488
- };
1489
- }
1490
- function viewportToCss(viewport) {
1491
- return `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
1492
- }
1493
- function visibleWorldRect(viewport, width, height) {
1494
- const topLeft = screenToWorld({ x: 0, y: 0 }, viewport);
1495
- const bottomRight = screenToWorld({ x: width, y: height }, viewport);
1496
- return {
1497
- x: topLeft.x,
1498
- y: topLeft.y,
1499
- width: bottomRight.x - topLeft.x,
1500
- height: bottomRight.y - topLeft.y
1501
- };
1502
- }
1503
- function rectsIntersect(a, b) {
1504
- return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
1505
- }
1506
-
1507
2025
  // src/connection.ts
1508
2026
  function clientToWorld(store, paneRef, clientX, clientY) {
1509
2027
  const rect = paneRef.current?.getBoundingClientRect();
@@ -1595,8 +2113,17 @@ function startConnectionDrag(config) {
1595
2113
  validate,
1596
2114
  map,
1597
2115
  onEnd,
1598
- resolveHit = resolveConnectionHit
2116
+ resolveHit: resolveHitInput = resolveConnectionHit
1599
2117
  } = config;
2118
+ if (!store.locks.allows(from.nodeId, "connect")) {
2119
+ onEnd?.();
2120
+ return;
2121
+ }
2122
+ const resolveHit = (...args) => {
2123
+ const hit = resolveHitInput(...args);
2124
+ if (hit && !store.locks.allows(hit.nodeId, "connect")) return null;
2125
+ return hit;
2126
+ };
1600
2127
  store.beginConnection(begin);
1601
2128
  const onMove = (event) => {
1602
2129
  const hit = resolveHit(store, paneRef, from, event.clientX, event.clientY);
@@ -1699,6 +2226,10 @@ function Handle({
1699
2226
  if (event.key !== "Enter" && event.key !== " ") return;
1700
2227
  event.preventDefault();
1701
2228
  const self = { nodeId, handleId, type: resolvedType };
2229
+ if (!store.locks.allows(nodeId, "connect")) {
2230
+ announce(`${nodeName(nodeId)} is locked by someone else.`);
2231
+ return;
2232
+ }
1702
2233
  const pending = store.getPending();
1703
2234
  if (!pending) {
1704
2235
  const origin = handleWorldPosition(store, self);
@@ -1751,6 +2282,7 @@ function Handle({
1751
2282
  const onFocus = () => {
1752
2283
  const pending = store.getPending();
1753
2284
  if (!pending || pending.source === nodeId && pending.sourceHandle === handleId) return;
2285
+ if (!store.locks.allows(nodeId, "connect")) return;
1754
2286
  const world = handleWorldPosition(store, { nodeId, handleId});
1755
2287
  if (world) store.updateConnection(world, place);
1756
2288
  };
@@ -1835,7 +2367,12 @@ var baseStyle = {
1835
2367
  minWidth: 120,
1836
2368
  padding: "10px 14px",
1837
2369
  borderRadius: 8,
1838
- border: "1px solid rgba(100, 116, 139, 0.4)",
2370
+ // Longhand, not the `border` shorthand: the hover/selected styles below
2371
+ // override `borderColor` alone, and React warns when a shorthand and a
2372
+ // longhand for the same value are mixed across renders.
2373
+ borderWidth: 1,
2374
+ borderStyle: "solid",
2375
+ borderColor: "rgba(100, 116, 139, 0.4)",
1839
2376
  background: "#ffffff",
1840
2377
  color: "#0f172a",
1841
2378
  fontSize: 13,
@@ -1867,7 +2404,10 @@ var baseStyle2 = {
1867
2404
  minHeight: 100,
1868
2405
  boxSizing: "border-box",
1869
2406
  borderRadius: 10,
1870
- border: "1.5px dashed rgba(100, 116, 139, 0.45)",
2407
+ // Longhand see the note in `default-node.tsx`.
2408
+ borderWidth: 1.5,
2409
+ borderStyle: "dashed",
2410
+ borderColor: "rgba(100, 116, 139, 0.45)",
1871
2411
  background: "rgba(100, 116, 139, 0.06)"
1872
2412
  };
1873
2413
  var hoveredStyle2 = {
@@ -2051,6 +2591,11 @@ function NodeResizer({
2051
2591
  }) {
2052
2592
  const { store } = useRivetContext();
2053
2593
  const { nodeId } = useRivetNodeContext();
2594
+ const resizable = useSyncExternalStore(
2595
+ store.locks.subscribe,
2596
+ useCallback(() => store.locks.allows(nodeId, "resize"), [store, nodeId]),
2597
+ useCallback(() => store.locks.allows(nodeId, "resize"), [store, nodeId])
2598
+ );
2054
2599
  const startResize = (dir) => (event) => {
2055
2600
  if (event.button !== 0) return;
2056
2601
  event.stopPropagation();
@@ -2082,7 +2627,16 @@ function NodeResizer({
2082
2627
  };
2083
2628
  return { size: { width, height }, position };
2084
2629
  };
2630
+ let started = false;
2085
2631
  const onMove = (moveEvent) => {
2632
+ if (!started) {
2633
+ started = true;
2634
+ store.beginNodeGesture(nodeId);
2635
+ } else if (!store.isNodeHeld(nodeId)) {
2636
+ window.removeEventListener("pointermove", onMove);
2637
+ window.removeEventListener("pointerup", onUp);
2638
+ return;
2639
+ }
2086
2640
  const { size, position } = compute(moveEvent);
2087
2641
  const moved = dir.left || dir.top;
2088
2642
  store.resizeNode(nodeId, size, moved ? position : void 0, false);
@@ -2091,14 +2645,17 @@ function NodeResizer({
2091
2645
  const onUp = (upEvent) => {
2092
2646
  window.removeEventListener("pointermove", onMove);
2093
2647
  window.removeEventListener("pointerup", onUp);
2648
+ if (started && !store.isNodeHeld(nodeId)) return;
2094
2649
  const { size, position } = compute(upEvent);
2095
2650
  const moved = dir.left || dir.top;
2096
2651
  store.resizeNode(nodeId, size, moved ? position : void 0, true);
2652
+ if (started) store.endNodeGesture(nodeId);
2097
2653
  onResizeEnd?.(size, position);
2098
2654
  };
2099
2655
  window.addEventListener("pointermove", onMove);
2100
2656
  window.addEventListener("pointerup", onUp);
2101
2657
  };
2658
+ if (!resizable) return null;
2102
2659
  return /* @__PURE__ */ jsx(Fragment, { children: HANDLES.map((dir) => /* @__PURE__ */ jsx(
2103
2660
  "div",
2104
2661
  {
@@ -2114,6 +2671,106 @@ function NodeResizer({
2114
2671
  dir.key
2115
2672
  )) });
2116
2673
  }
2674
+
2675
+ // src/presence/local.ts
2676
+ var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
2677
+ 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);
2678
+ function trackLocalPresence({
2679
+ store,
2680
+ pane,
2681
+ emit,
2682
+ throttleMs
2683
+ }) {
2684
+ let cursor = null;
2685
+ let screenPoint = null;
2686
+ let last = null;
2687
+ let timer = null;
2688
+ let lastEmit = Number.NEGATIVE_INFINITY;
2689
+ const publish = () => {
2690
+ if (timer !== null) {
2691
+ clearTimeout(timer);
2692
+ timer = null;
2693
+ }
2694
+ lastEmit = performance.now();
2695
+ const next = {
2696
+ cursor,
2697
+ selection: store.getSelectedNodes(),
2698
+ holding: [...store.getHeldNodeIds()]
2699
+ };
2700
+ if (last && samePresence(last, next)) return;
2701
+ last = next;
2702
+ emit(next);
2703
+ };
2704
+ const schedule = () => {
2705
+ if (timer !== null) return;
2706
+ const wait = throttleMs - (performance.now() - lastEmit);
2707
+ if (wait <= 0) {
2708
+ publish();
2709
+ return;
2710
+ }
2711
+ timer = setTimeout(() => {
2712
+ timer = null;
2713
+ publish();
2714
+ }, wait);
2715
+ };
2716
+ const project = () => {
2717
+ if (!screenPoint) {
2718
+ cursor = null;
2719
+ return;
2720
+ }
2721
+ cursor = screenToWorld(screenPoint, store.getViewport());
2722
+ };
2723
+ const onPointerMove = (event) => {
2724
+ const rect = pane.getBoundingClientRect();
2725
+ screenPoint = { x: event.clientX - rect.left, y: event.clientY - rect.top };
2726
+ project();
2727
+ schedule();
2728
+ };
2729
+ const onPointerLeave = () => {
2730
+ screenPoint = null;
2731
+ cursor = null;
2732
+ publish();
2733
+ };
2734
+ const unsubscribeViewport = store.subscribeViewport(() => {
2735
+ if (!screenPoint) return;
2736
+ project();
2737
+ schedule();
2738
+ });
2739
+ const unsubscribeSelection = store.subscribeSelection(() => publish());
2740
+ const unsubscribeHeld = store.subscribeHeldNodes(() => publish());
2741
+ pane.addEventListener("pointermove", onPointerMove);
2742
+ pane.addEventListener("pointerleave", onPointerLeave);
2743
+ return () => {
2744
+ if (timer !== null) clearTimeout(timer);
2745
+ pane.removeEventListener("pointermove", onPointerMove);
2746
+ pane.removeEventListener("pointerleave", onPointerLeave);
2747
+ unsubscribeViewport();
2748
+ unsubscribeSelection();
2749
+ unsubscribeHeld();
2750
+ };
2751
+ }
2752
+
2753
+ // src/hooks/use-local-presence.ts
2754
+ function useLocalPresence({
2755
+ store,
2756
+ paneRef,
2757
+ onLocalPresence,
2758
+ throttleMs
2759
+ }) {
2760
+ const handlerRef = useRef(onLocalPresence);
2761
+ handlerRef.current = onLocalPresence;
2762
+ const enabled = Boolean(onLocalPresence);
2763
+ useEffect(() => {
2764
+ const pane = paneRef.current;
2765
+ if (!enabled || !pane) return;
2766
+ return trackLocalPresence({
2767
+ store,
2768
+ pane,
2769
+ emit: (presence) => handlerRef.current?.(presence),
2770
+ throttleMs
2771
+ });
2772
+ }, [store, paneRef, enabled, throttleMs]);
2773
+ }
2117
2774
  function shallowEqual(a, b) {
2118
2775
  if (Object.is(a, b)) return true;
2119
2776
  if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
@@ -2169,6 +2826,11 @@ function useRivetConfig(params) {
2169
2826
  () => ({ ...DEFAULT_ANCHOR_OPTIONS, ...anchorOptionsInput }),
2170
2827
  [anchorOptionsInput]
2171
2828
  );
2829
+ const presenceOptionsInput = useShallowStable(params.presenceOptions);
2830
+ const presenceOptions = useMemo(
2831
+ () => ({ ...DEFAULT_PRESENCE_OPTIONS, ...presenceOptionsInput }),
2832
+ [presenceOptionsInput]
2833
+ );
2172
2834
  const isValidConnection = useStableOptional(params.isValidConnection);
2173
2835
  const mapConnection = useStableOptional(params.mapConnection);
2174
2836
  const onConnect = useStableOptional(params.onConnect);
@@ -2182,6 +2844,7 @@ function useRivetConfig(params) {
2182
2844
  snapGrid,
2183
2845
  swimlaneMargin,
2184
2846
  anchorOptions,
2847
+ presenceOptions,
2185
2848
  panButtons,
2186
2849
  selectionKeys,
2187
2850
  multiSelectionKeys,
@@ -2221,7 +2884,7 @@ function createKeyboardHandler(store, getSnapGrid) {
2221
2884
  }
2222
2885
  const dir = ARROW_DIRS[event.key];
2223
2886
  if (!dir) return;
2224
- const movers = store.getMovers(store.getSelectedNodes());
2887
+ const movers = store.getMovers(store.getSelectedNodes()).filter((id) => store.locks.allows(id, "drag"));
2225
2888
  if (movers.length === 0) return;
2226
2889
  event.preventDefault();
2227
2890
  const snapGrid = getSnapGrid?.() ?? null;
@@ -2299,6 +2962,7 @@ function createMarqueeController(deps) {
2299
2962
  const ids = [];
2300
2963
  for (const [id, node] of store.nodes) {
2301
2964
  if (node.selectable === false) continue;
2965
+ if (!store.locks.allows(id, "select")) continue;
2302
2966
  if (rectsIntersect(worldRect, store.getNodeRect(id))) ids.push(id);
2303
2967
  }
2304
2968
  store.selectNodes(additive ? [.../* @__PURE__ */ new Set([...base, ...ids])] : ids);
@@ -2456,363 +3120,127 @@ function drawDotGrid(ctx, width, height, viewport, options) {
2456
3120
  ctx.beginPath();
2457
3121
  for (let x = offsetX; x < width; x += step) {
2458
3122
  for (let y = offsetY; y < height; y += step) {
2459
- ctx.moveTo(x + radius, y);
2460
- ctx.arc(x, y, radius, 0, Math.PI * 2);
2461
- }
2462
- }
2463
- ctx.fill();
2464
- }
2465
-
2466
- // src/renderer/edge-paths.ts
2467
- var SIDE_DIR = {
2468
- left: { x: -1, y: 0 },
2469
- right: { x: 1, y: 0 },
2470
- top: { x: 0, y: -1 },
2471
- bottom: { x: 0, y: 1 }
2472
- };
2473
- var BEZIER_SEGMENT_PX = 8;
2474
- var BEZIER_MIN_SAMPLES = 16;
2475
- var BEZIER_MAX_SAMPLES = 160;
2476
- var CORNER_RADIUS = 8;
2477
- var ARC_SAMPLES = 6;
2478
- function cubicAt(a, b, c, d, t) {
2479
- const mt = 1 - t;
2480
- return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
2481
- }
2482
- function quadAt(a, b, c, t) {
2483
- const mt = 1 - t;
2484
- return mt * mt * a + 2 * mt * t * b + t * t * c;
2485
- }
2486
- var getStraightPath = ({ sourceX, sourceY, targetX, targetY }) => ({
2487
- points: [
2488
- { x: sourceX, y: sourceY },
2489
- { x: targetX, y: targetY }
2490
- ],
2491
- labelX: (sourceX + targetX) / 2,
2492
- labelY: (sourceY + targetY) / 2
2493
- });
2494
- var getBezierPath = (params) => {
2495
- const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2496
- const sDir = SIDE_DIR[sourcePosition];
2497
- const tDir = SIDE_DIR[targetPosition];
2498
- const dist = Math.hypot(targetX - sourceX, targetY - sourceY);
2499
- const reach = Math.max(40, dist * 0.4);
2500
- const p0 = { x: sourceX, y: sourceY };
2501
- const p1 = { x: sourceX + sDir.x * reach, y: sourceY + sDir.y * reach };
2502
- const p2 = { x: targetX + tDir.x * reach, y: targetY + tDir.y * reach };
2503
- const p3 = { x: targetX, y: targetY };
2504
- const ctrlLen = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y) + Math.hypot(p3.x - p2.x, p3.y - p2.y);
2505
- const samples = Math.max(
2506
- BEZIER_MIN_SAMPLES,
2507
- Math.min(BEZIER_MAX_SAMPLES, Math.ceil(ctrlLen / BEZIER_SEGMENT_PX))
2508
- );
2509
- const points = [];
2510
- for (let i = 0; i <= samples; i++) {
2511
- const t = i / samples;
2512
- points.push({
2513
- x: cubicAt(p0.x, p1.x, p2.x, p3.x, t),
2514
- y: cubicAt(p0.y, p1.y, p2.y, p3.y, t)
2515
- });
2516
- }
2517
- return {
2518
- points,
2519
- labelX: cubicAt(p0.x, p1.x, p2.x, p3.x, 0.5),
2520
- labelY: cubicAt(p0.y, p1.y, p2.y, p3.y, 0.5)
2521
- };
2522
- };
2523
- var STEP_OFFSET = 20;
2524
- function simplifyCorners(corners) {
2525
- const out = [];
2526
- for (const p of corners) {
2527
- const a = out[out.length - 2];
2528
- const b = out[out.length - 1];
2529
- if (b && b.x === p.x && b.y === p.y) continue;
2530
- if (a && b && (a.x === b.x && b.x === p.x || a.y === b.y && b.y === p.y)) out.pop();
2531
- out.push(p);
2532
- }
2533
- return out;
2534
- }
2535
- function stepCorners(params) {
2536
- const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2537
- const s = { x: sourceX, y: sourceY };
2538
- const t = { x: targetX, y: targetY };
2539
- const sDir = SIDE_DIR[sourcePosition];
2540
- const tDir = SIDE_DIR[targetPosition];
2541
- const sg = { x: sourceX + sDir.x * STEP_OFFSET, y: sourceY + sDir.y * STEP_OFFSET };
2542
- const tg = { x: targetX + tDir.x * STEP_OFFSET, y: targetY + tDir.y * STEP_OFFSET };
2543
- return simplifyCorners(routeCorners(s, t, sg, tg, sDir, tDir));
2544
- }
2545
- function routeCorners(s, t, sg, tg, sDir, tDir) {
2546
- const sHoriz = sDir.x !== 0;
2547
- const tHoriz = tDir.x !== 0;
2548
- if (sHoriz && tHoriz) {
2549
- if (sDir.x * tDir.x < 0) {
2550
- if ((t.x - s.x) * sDir.x >= 2 * STEP_OFFSET) {
2551
- const midX = (s.x + t.x) / 2;
2552
- return [s, { x: midX, y: s.y }, { x: midX, y: t.y }, t];
2553
- }
2554
- const midY = (s.y + t.y) / 2;
2555
- return [s, sg, { x: sg.x, y: midY }, { x: tg.x, y: midY }, tg, t];
2556
- }
2557
- const railX = sDir.x > 0 ? Math.max(sg.x, tg.x) : Math.min(sg.x, tg.x);
2558
- return [s, { x: railX, y: s.y }, { x: railX, y: t.y }, t];
2559
- }
2560
- if (!sHoriz && !tHoriz) {
2561
- if (sDir.y * tDir.y < 0) {
2562
- if ((t.y - s.y) * sDir.y >= 2 * STEP_OFFSET) {
2563
- const midY = (s.y + t.y) / 2;
2564
- return [s, { x: s.x, y: midY }, { x: t.x, y: midY }, t];
2565
- }
2566
- const midX = (s.x + t.x) / 2;
2567
- return [s, sg, { x: midX, y: sg.y }, { x: midX, y: tg.y }, tg, t];
2568
- }
2569
- const railY = sDir.y > 0 ? Math.max(sg.y, tg.y) : Math.min(sg.y, tg.y);
2570
- return [s, { x: s.x, y: railY }, { x: t.x, y: railY }, t];
2571
- }
2572
- const corner = sHoriz ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
2573
- const exitsSource = sHoriz ? (corner.x - s.x) * sDir.x >= STEP_OFFSET : (corner.y - s.y) * sDir.y >= STEP_OFFSET;
2574
- const exitsTarget = tHoriz ? (corner.x - t.x) * tDir.x >= STEP_OFFSET : (corner.y - t.y) * tDir.y >= STEP_OFFSET;
2575
- if (exitsSource && exitsTarget) return [s, corner, t];
2576
- const bend = sHoriz ? { x: sg.x, y: tg.y } : { x: tg.x, y: sg.y };
2577
- return [s, sg, bend, tg, t];
2578
- }
2579
- function roundCorners(corners, radius) {
2580
- if (corners.length <= 2) return corners;
2581
- const points = [];
2582
- const first = corners[0];
2583
- if (first) points.push(first);
2584
- for (let i = 1; i < corners.length - 1; i++) {
2585
- const prev = corners[i - 1];
2586
- const curr = corners[i];
2587
- const next = corners[i + 1];
2588
- if (!prev || !curr || !next) continue;
2589
- const inLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
2590
- const outLen = Math.hypot(next.x - curr.x, next.y - curr.y);
2591
- const r = Math.min(radius, inLen / 2, outLen / 2);
2592
- const inPt = {
2593
- x: curr.x + (prev.x - curr.x) / (inLen || 1) * r,
2594
- y: curr.y + (prev.y - curr.y) / (inLen || 1) * r
2595
- };
2596
- const outPt = {
2597
- x: curr.x + (next.x - curr.x) / (outLen || 1) * r,
2598
- y: curr.y + (next.y - curr.y) / (outLen || 1) * r
2599
- };
2600
- points.push(inPt);
2601
- for (let j = 1; j < ARC_SAMPLES; j++) {
2602
- const tt = j / ARC_SAMPLES;
2603
- points.push({
2604
- x: quadAt(inPt.x, curr.x, outPt.x, tt),
2605
- y: quadAt(inPt.y, curr.y, outPt.y, tt)
2606
- });
3123
+ ctx.moveTo(x + radius, y);
3124
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
2607
3125
  }
2608
- points.push(outPt);
2609
3126
  }
2610
- const last = corners[corners.length - 1];
2611
- if (last) points.push(last);
2612
- return points;
2613
- }
2614
- function stepLabel(corners, params) {
2615
- const i = Math.floor((corners.length - 1) / 2);
2616
- const a = corners[i];
2617
- const b = corners[i + 1];
2618
- if (!a || !b) return { x: params.sourceX, y: params.sourceY };
2619
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
3127
+ ctx.fill();
2620
3128
  }
2621
- var getSmoothStepPath = (params) => {
2622
- const corners = stepCorners(params);
2623
- const label = stepLabel(corners, params);
2624
- return { points: roundCorners(corners, CORNER_RADIUS), labelX: label.x, labelY: label.y };
2625
- };
2626
- var getStepPath = (params) => {
2627
- const corners = stepCorners(params);
2628
- const label = stepLabel(corners, params);
2629
- return { points: corners, labelX: label.x, labelY: label.y };
2630
- };
2631
- var BUILTIN_EDGE_TYPES = {
2632
- bezier: getBezierPath,
2633
- smoothstep: getSmoothStepPath,
2634
- step: getStepPath,
2635
- straight: getStraightPath
2636
- };
2637
3129
 
2638
- // src/renderer/edge-geometry.ts
2639
- var EDGE_COLOR = "rgba(100, 116, 139, 0.75)";
2640
- var EDGE_COLOR_SELECTED = "#6366f1";
2641
- var EDGE_COLOR_HOVERED = "rgba(99, 102, 241, 0.9)";
2642
- var EDGE_WIDTH = 1.5;
2643
- var EDGE_WIDTH_HOVERED = 2.5;
2644
- var PENDING_COLOR = "#6366f1";
2645
- var OPPOSITE = {
2646
- left: "right",
2647
- right: "left",
2648
- top: "bottom",
2649
- bottom: "top"
2650
- };
2651
- var falseToNull = (m) => m || null;
2652
- function buildEdges(edges, nodes, viewport, edgeTypes, defaults, extras) {
2653
- const handles = extras?.handles;
2654
- const hovered = extras?.hovered;
2655
- const reconnecting = extras?.reconnecting;
2656
- const worldPositions = extras?.worldPositions;
2657
- const resolved = [];
2658
- for (const edge of edges) {
2659
- if (edge.id === reconnecting) continue;
2660
- const source = nodes.get(edge.source);
2661
- const target = nodes.get(edge.target);
2662
- if (!source || !target) continue;
2663
- const frame = { handles, viewport, worldPositions, extras };
2664
- const start = resolveEnd(edge, "source", source, target, frame);
2665
- const end = resolveEnd(edge, "target", target, source, frame);
2666
- const params = {
2667
- sourceX: start.point.x,
2668
- sourceY: start.point.y,
2669
- targetX: end.point.x,
2670
- targetY: end.point.y,
2671
- sourcePosition: start.position,
2672
- targetPosition: end.position
2673
- };
2674
- const pathFn = edgeTypes[edge.type ?? defaults.type ?? "bezier"] ?? getBezierPath;
2675
- const path = pathFn(params);
2676
- if (path.points.length < 2) continue;
2677
- const style = { ...defaults.style, ...edge.style };
2678
- const isHovered = edge.id === hovered;
2679
- resolved.push({
2680
- id: edge.id,
2681
- points: path.points,
2682
- source: start.point,
2683
- target: end.point,
2684
- label: edge.label !== void 0 ? { x: path.labelX, y: path.labelY } : void 0,
2685
- stroke: edge.selected ? EDGE_COLOR_SELECTED : isHovered ? EDGE_COLOR_HOVERED : style.stroke ?? EDGE_COLOR,
2686
- width: style.strokeWidth ?? (isHovered ? EDGE_WIDTH_HOVERED : EDGE_WIDTH),
2687
- opacity: style.opacity ?? 1,
2688
- animated: edge.animated ?? defaults.animated ?? false,
2689
- dash: style.strokeDasharray,
2690
- markerStart: falseToNull(edge.markerStart ?? defaults.markerStart),
2691
- markerEnd: falseToNull(edge.markerEnd ?? defaults.markerEnd)
2692
- });
3130
+ // src/renderer/presence.ts
3131
+ var HELD_LINE_WIDTH = 2;
3132
+ var SELECTED_LINE_WIDTH = 1.5;
3133
+ var SELECTED_DASH = [4, 4];
3134
+ var OUTLINE_RADIUS = 6;
3135
+ var CURSOR_POINTS = [
3136
+ { x: 0, y: 0 },
3137
+ { x: 0, y: 16 },
3138
+ { x: 4.2, y: 12.2 },
3139
+ { x: 6.8, y: 17.6 },
3140
+ { x: 9.2, y: 16.6 },
3141
+ { x: 6.6, y: 11.4 },
3142
+ { x: 11.4, y: 11.4 }
3143
+ ];
3144
+ var CURSOR_LABEL_OFFSET = { x: 12, y: 17 };
3145
+ var LABEL_FONT = "500 11px ui-sans-serif, system-ui, -apple-system, sans-serif";
3146
+ var LABEL_PADDING_X = 6;
3147
+ var LABEL_HEIGHT = 17;
3148
+ var LABEL_RADIUS = 4;
3149
+ var CULL_MARGIN_PX = 64;
3150
+ function traceRect(ctx, rect, radius) {
3151
+ ctx.beginPath();
3152
+ if (typeof ctx.roundRect === "function") {
3153
+ ctx.roundRect(rect.x, rect.y, rect.width, rect.height, radius);
3154
+ } else {
3155
+ ctx.rect(rect.x, rect.y, rect.width, rect.height);
2693
3156
  }
2694
- return resolved;
2695
- }
2696
- function buildPendingPath(pending, handles, viewport) {
2697
- const record = handles?.get(handleKey(pending.source, pending.sourceHandle));
2698
- const sourcePosition = record?.position ?? (pending.sourceType === "target" ? "left" : "right");
2699
- const from = worldToScreen(pending.from, viewport);
2700
- const to = worldToScreen(pending.to, viewport);
2701
- const targetPosition = pending.toPosition ?? OPPOSITE[sourcePosition];
2702
- return getBezierPath({
2703
- sourceX: from.x,
2704
- sourceY: from.y,
2705
- targetX: to.x,
2706
- targetY: to.y,
2707
- sourcePosition,
2708
- targetPosition
2709
- }).points;
2710
3157
  }
2711
- function pointOnSide(rect, side, alongPct = 50) {
2712
- const t = alongPct / 100;
2713
- switch (side) {
2714
- case "left":
2715
- return { x: rect.x, y: rect.y + rect.height * t };
2716
- case "right":
2717
- return { x: rect.x + rect.width, y: rect.y + rect.height * t };
2718
- case "top":
2719
- return { x: rect.x + rect.width * t, y: rect.y };
2720
- case "bottom":
2721
- return { x: rect.x + rect.width * t, y: rect.y + rect.height };
2722
- }
3158
+ function projectRect(rect, viewport) {
3159
+ const origin = worldToScreen({ x: rect.x, y: rect.y }, viewport);
3160
+ return {
3161
+ x: origin.x,
3162
+ y: origin.y,
3163
+ width: rect.width * viewport.zoom,
3164
+ height: rect.height * viewport.zoom
3165
+ };
2723
3166
  }
2724
- function nodeRect(node, worldPositions) {
2725
- const origin = worldPositions?.get(node.id) ?? node.position;
2726
- const size = node.size ?? DEFAULT_NODE_SIZE;
2727
- return { x: origin.x, y: origin.y, width: size.width, height: size.height };
3167
+ function onScreen(rect, size) {
3168
+ return rect.x + rect.width >= -CULL_MARGIN_PX && rect.y + rect.height >= -CULL_MARGIN_PX && rect.x <= size.width + CULL_MARGIN_PX && rect.y <= size.height + CULL_MARGIN_PX;
2728
3169
  }
2729
- var WANTED_TYPES = {
2730
- source: ["source", "either"],
2731
- target: ["target", "either"]
2732
- };
2733
- function handleOnSide(nodeId, side, role, handles) {
2734
- if (!handles) return null;
2735
- for (const record of handles.values()) {
2736
- if (record.nodeId === nodeId && record.position === side && WANTED_TYPES[role].includes(record.type)) {
2737
- return record;
2738
- }
2739
- }
2740
- return null;
3170
+ function drawNodeOutline(ctx, rect, color, mode) {
3171
+ ctx.strokeStyle = color;
3172
+ ctx.lineWidth = mode === "selected" ? SELECTED_LINE_WIDTH : HELD_LINE_WIDTH;
3173
+ ctx.setLineDash(mode === "selected" ? SELECTED_DASH : []);
3174
+ traceRect(ctx, rect, OUTLINE_RADIUS);
3175
+ ctx.stroke();
3176
+ ctx.setLineDash([]);
2741
3177
  }
2742
- function reachableSides(nodeId, role, handles) {
2743
- if (!handles) return [];
2744
- const sides = [];
2745
- for (const record of handles.values()) {
2746
- if (record.nodeId !== nodeId) continue;
2747
- if (!WANTED_TYPES[role].includes(record.type)) continue;
2748
- if (!sides.includes(record.position)) sides.push(record.position);
2749
- }
2750
- return sides;
3178
+ function drawCursor(ctx, point, color, name) {
3179
+ ctx.beginPath();
3180
+ const [first, ...rest] = CURSOR_POINTS;
3181
+ if (!first) return;
3182
+ ctx.moveTo(point.x + first.x, point.y + first.y);
3183
+ for (const offset of rest) ctx.lineTo(point.x + offset.x, point.y + offset.y);
3184
+ ctx.closePath();
3185
+ ctx.fillStyle = color;
3186
+ ctx.fill();
3187
+ ctx.strokeStyle = "#ffffff";
3188
+ ctx.lineWidth = 1;
3189
+ ctx.stroke();
3190
+ if (!name) return;
3191
+ ctx.font = LABEL_FONT;
3192
+ ctx.textBaseline = "middle";
3193
+ const width = ctx.measureText(name).width + LABEL_PADDING_X * 2;
3194
+ const x = point.x + CURSOR_LABEL_OFFSET.x;
3195
+ const y = point.y + CURSOR_LABEL_OFFSET.y;
3196
+ traceRect(ctx, { x, y, width, height: LABEL_HEIGHT }, LABEL_RADIUS);
3197
+ ctx.fillStyle = color;
3198
+ ctx.fill();
3199
+ ctx.fillStyle = "#ffffff";
3200
+ ctx.fillText(name, x + LABEL_PADDING_X, y + LABEL_HEIGHT / 2);
2751
3201
  }
2752
- function resolveEnd(edge, role, node, peer, frame) {
2753
- const { handles, viewport, worldPositions, extras } = frame;
2754
- const handleId = role === "source" ? edge.sourceHandle : edge.targetHandle;
2755
- const rect = nodeRect(node, worldPositions);
2756
- const anchorId = parseAnchorAutoHandleId(handleId) ?? parseAnchorHandleId(handleId)?.anchorId ?? null;
2757
- const endAt = (side2) => {
2758
- const alongPct = (anchorId ? extras?.anchorPlacement?.(node.id, anchorId, side2) : null) ?? void 0;
2759
- if (alongPct === void 0 && !anchorId) {
2760
- const candidate = handleOnSide(node.id, side2, role, handles);
2761
- if (candidate) {
2762
- return {
2763
- point: worldToScreen(
2764
- { x: rect.x + candidate.offset.x, y: rect.y + candidate.offset.y },
2765
- viewport
2766
- ),
2767
- position: side2
2768
- };
2769
- }
3202
+ function drawPresence(ctx, peers, { viewport, size, getNodeRect, renderCursors, locks }) {
3203
+ if (peers.length === 0 && locks.size === 0) return;
3204
+ ctx.save();
3205
+ ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
3206
+ const painted = /* @__PURE__ */ new Set();
3207
+ for (const peer of peers) {
3208
+ const held = /* @__PURE__ */ new Set([...peer.holding, ...peer.transforms.keys()]);
3209
+ for (const id of peer.selection) {
3210
+ if (held.has(id)) continue;
3211
+ const rect = getNodeRect(id);
3212
+ if (!rect) continue;
3213
+ const screen = projectRect(rect, viewport);
3214
+ if (onScreen(screen, size)) drawNodeOutline(ctx, screen, peer.color, "selected");
2770
3215
  }
2771
- return { point: worldToScreen(pointOnSide(rect, side2, alongPct), viewport), position: side2 };
2772
- };
2773
- const registered = handleId && handles ? handles.get(handleKey(node.id, handleId)) : void 0;
2774
- const pinnedSide = parseAnchorHandleId(handleId)?.side ?? registered?.position ?? parseSideHandleId(handleId);
2775
- const live = extras?.liveAlignNodes !== void 0 && (extras.liveAlignNodes.has(edge.source) || extras.liveAlignNodes.has(edge.target)) && edge.source !== edge.target;
2776
- if (pinnedSide && !live) {
2777
- if (registered) {
2778
- return {
2779
- point: worldToScreen(
2780
- { x: rect.x + registered.offset.x, y: rect.y + registered.offset.y },
2781
- viewport
2782
- ),
2783
- position: registered.position
2784
- };
3216
+ for (const id of held) {
3217
+ painted.add(id);
3218
+ const rect = getNodeRect(id);
3219
+ if (!rect) continue;
3220
+ const screen = projectRect(rect, viewport);
3221
+ if (onScreen(screen, size)) drawNodeOutline(ctx, screen, peer.color, "held");
2785
3222
  }
2786
- return endAt(pinnedSide);
2787
3223
  }
2788
- const key = displayedSideKey(edge.id, role);
2789
- const side = resolveFacingSideAmong(
2790
- rect,
2791
- nodeRect(peer, worldPositions),
2792
- anchorId ? [] : reachableSides(node.id, role, handles),
2793
- extras?.displayedSides?.get(key)
2794
- );
2795
- extras?.displayedSides?.set(key, side);
2796
- return endAt(side);
2797
- }
2798
- function distanceToPolyline(point, points) {
2799
- let best = Number.POSITIVE_INFINITY;
2800
- for (let i = 0; i < points.length - 1; i++) {
2801
- const a = points[i];
2802
- const b = points[i + 1];
2803
- if (!a || !b) continue;
2804
- best = Math.min(best, distanceToSegment(point, a, b));
3224
+ if (locks.size > 0) {
3225
+ const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
3226
+ for (const [id, holderId] of locks) {
3227
+ if (painted.has(id)) continue;
3228
+ const rect = getNodeRect(id);
3229
+ if (!rect) continue;
3230
+ const screen = projectRect(rect, viewport);
3231
+ if (!onScreen(screen, size)) continue;
3232
+ drawNodeOutline(ctx, screen, declared.get(holderId) ?? peerColor(holderId), "held");
3233
+ }
2805
3234
  }
2806
- return best;
2807
- }
2808
- function distanceToSegment(p, a, b) {
2809
- const dx = b.x - a.x;
2810
- const dy = b.y - a.y;
2811
- const lenSq = dx * dx + dy * dy;
2812
- if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
2813
- let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
2814
- t = Math.max(0, Math.min(1, t));
2815
- return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
3235
+ if (renderCursors) {
3236
+ for (const peer of peers) {
3237
+ if (!peer.cursor) continue;
3238
+ const point = worldToScreen(peer.cursor, viewport);
3239
+ if (!onScreen({ ...point, width: 0, height: 0 }, size)) continue;
3240
+ drawCursor(ctx, point, peer.color, peer.name);
3241
+ }
3242
+ }
3243
+ ctx.restore();
2816
3244
  }
2817
3245
 
2818
3246
  // src/renderer/swimlane.ts
@@ -2845,7 +3273,7 @@ function drawSwimlanes(ctx, width, height, viewport, lanes, style) {
2845
3273
  }
2846
3274
 
2847
3275
  // src/hooks/use-rivet-runtime.ts
2848
- var CULL_MARGIN_PX = 240;
3276
+ var CULL_MARGIN_PX2 = 240;
2849
3277
  var ZOOM_STEP = 1.2;
2850
3278
  var WILL_CHANGE_IDLE_MS = 180;
2851
3279
  var GUIDE_COLOR = "rgba(236, 72, 153, 0.9)";
@@ -2930,6 +3358,31 @@ function useRivetRuntime(params) {
2930
3358
  let frame = 0;
2931
3359
  let dirty = true;
2932
3360
  let hoveredEdgeId = null;
3361
+ let peerPositioned = /* @__PURE__ */ new Set();
3362
+ const positionPeerNodes = (worldPositions) => {
3363
+ const transforms = store.presence.getNodeTransforms();
3364
+ if (transforms.size === 0 && peerPositioned.size === 0) return;
3365
+ const current = /* @__PURE__ */ new Set();
3366
+ for (const id of transforms.keys()) {
3367
+ current.add(id);
3368
+ for (const childId of store.getDescendantIds(id)) current.add(childId);
3369
+ }
3370
+ for (const id of /* @__PURE__ */ new Set([...current, ...peerPositioned])) {
3371
+ const element = store.getNodeElement(id);
3372
+ const world = worldPositions.get(id);
3373
+ if (!element || !world) continue;
3374
+ element.style.transform = `translate(${world.x}px, ${world.y}px)`;
3375
+ const live = transforms.get(id);
3376
+ if (live && !store.isNodeHeld(id)) {
3377
+ element.style.width = `${live.width}px`;
3378
+ element.style.height = `${live.height}px`;
3379
+ } else if (peerPositioned.has(id)) {
3380
+ element.style.width = "";
3381
+ element.style.height = "";
3382
+ }
3383
+ }
3384
+ peerPositioned = current;
3385
+ };
2933
3386
  const anchorPlacement = (nodeId, anchorId, side) => {
2934
3387
  const record = store.anchors.get(handleKey(nodeId, anchorId));
2935
3388
  return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
@@ -2947,6 +3400,7 @@ function useRivetRuntime(params) {
2947
3400
  });
2948
3401
  const render = () => {
2949
3402
  zoom.step();
3403
+ if (store.presence.step(performance.now())) schedule();
2950
3404
  const viewport = store.getViewport();
2951
3405
  layerHint.apply(viewportToCss(viewport));
2952
3406
  bgCtx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
@@ -2954,6 +3408,7 @@ function useRivetRuntime(params) {
2954
3408
  drawDotGrid(bgCtx, size.width, size.height, viewport, { gap: gridGap });
2955
3409
  drawSwimlanes(bgCtx, size.width, size.height, viewport, swimlaneLanesRef.current);
2956
3410
  const worldPositions = store.getWorldPositions();
3411
+ positionPeerNodes(worldPositions);
2957
3412
  const edgeList = [...store.edges.values()];
2958
3413
  const pending = store.getPending();
2959
3414
  const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
@@ -2982,9 +3437,18 @@ function useRivetRuntime(params) {
2982
3437
  if (ends) drawEndpointBubbles(fgCtx, ends, size);
2983
3438
  }
2984
3439
  }
3440
+ if (fgCtx && (!store.presence.isEmpty() || store.locks.size() > 0)) {
3441
+ drawPresence(fgCtx, store.presence.getPeers(), {
3442
+ viewport,
3443
+ size,
3444
+ getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
3445
+ renderCursors: reconnectRef.current.presenceOptions.renderCursors,
3446
+ locks: store.locks.getLocks()
3447
+ });
3448
+ }
2985
3449
  if (edgeList.some((edge) => edge.animated ?? defaultEdgeOptions?.animated)) schedule();
2986
3450
  const rect = visibleWorldRect(viewport, size.width, size.height);
2987
- const margin = CULL_MARGIN_PX / viewport.zoom;
3451
+ const margin = CULL_MARGIN_PX2 / viewport.zoom;
2988
3452
  const view = {
2989
3453
  x: rect.x - margin,
2990
3454
  y: rect.y - margin,
@@ -3668,7 +4132,7 @@ var labelStyle2 = {
3668
4132
  whiteSpace: "nowrap",
3669
4133
  pointerEvents: "none"
3670
4134
  };
3671
- function sameIds(a, b) {
4135
+ function sameIds2(a, b) {
3672
4136
  if (a.length !== b.length) return false;
3673
4137
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
3674
4138
  return true;
@@ -3694,7 +4158,7 @@ function EdgeLabelLayer() {
3694
4158
  return store.subscribeFrame(() => {
3695
4159
  const anchors = store.getEdgeLabelAnchors();
3696
4160
  const next = [...anchors.keys()];
3697
- setIds((prev) => sameIds(prev, next) ? prev : next);
4161
+ setIds((prev) => sameIds2(prev, next) ? prev : next);
3698
4162
  position();
3699
4163
  });
3700
4164
  }, [store, position]);
@@ -3730,6 +4194,8 @@ function startGroupDrag(params) {
3730
4194
  let started = false;
3731
4195
  let starts = /* @__PURE__ */ new Map();
3732
4196
  let affected = /* @__PURE__ */ new Set();
4197
+ let initiator;
4198
+ let anchor;
3733
4199
  const repositionAffected = () => {
3734
4200
  for (const affectedId of affected) {
3735
4201
  const affectedEl = store.getNodeElement(affectedId);
@@ -3740,8 +4206,9 @@ function startGroupDrag(params) {
3740
4206
  onFrame?.();
3741
4207
  };
3742
4208
  const beginDrag = () => {
4209
+ const movers = store.getMovers(store.getSelectedNodes()).filter((moverId) => store.locks.allows(moverId, "drag"));
4210
+ if (movers.length === 0) return false;
3743
4211
  started = true;
3744
- const movers = store.getMovers(store.getSelectedNodes());
3745
4212
  starts = /* @__PURE__ */ new Map();
3746
4213
  for (const moverId of movers) {
3747
4214
  const moverNode = store.nodes.get(moverId);
@@ -3753,17 +4220,42 @@ function startGroupDrag(params) {
3753
4220
  for (const descendant of store.getDescendantIds(moverId)) affected.add(descendant);
3754
4221
  }
3755
4222
  store.setNodeDragging(true);
4223
+ initiator = params.nodeId ?? movers[0];
4224
+ anchor = movers[0];
4225
+ if (initiator) store.beginNodeGesture(initiator, movers);
3756
4226
  el.setPointerCapture(pointerId);
3757
4227
  el.style.cursor = "grabbing";
3758
4228
  window.getSelection()?.removeAllRanges();
3759
4229
  document.body.style.userSelect = "none";
3760
4230
  document.body.style.webkitUserSelect = "none";
4231
+ return true;
4232
+ };
4233
+ const teardown = () => {
4234
+ window.removeEventListener("pointermove", onMove);
4235
+ window.removeEventListener("pointerup", onUp);
4236
+ if (!started) return;
4237
+ started = false;
4238
+ el.style.cursor = "grab";
4239
+ document.body.style.userSelect = "";
4240
+ document.body.style.webkitUserSelect = "";
4241
+ store.setAlignmentGuides([]);
4242
+ store.setNodeDragging(false);
4243
+ if (el.hasPointerCapture(pointerId)) el.releasePointerCapture(pointerId);
3761
4244
  };
4245
+ const yielded = () => anchor !== void 0 && !store.isNodeHeld(anchor);
3762
4246
  const onMove = (moveEvent) => {
3763
4247
  if (!started) {
3764
4248
  const moved = Math.hypot(moveEvent.clientX - originX, moveEvent.clientY - originY);
3765
4249
  if (moved < DRAG_THRESHOLD) return;
3766
- beginDrag();
4250
+ if (!beginDrag()) {
4251
+ window.removeEventListener("pointermove", onMove);
4252
+ window.removeEventListener("pointerup", onUp);
4253
+ return;
4254
+ }
4255
+ }
4256
+ if (yielded()) {
4257
+ teardown();
4258
+ return;
3767
4259
  }
3768
4260
  const dx = (moveEvent.clientX - originX) / zoom;
3769
4261
  const dy = (moveEvent.clientY - originY) / zoom;
@@ -3798,6 +4290,10 @@ function startGroupDrag(params) {
3798
4290
  window.removeEventListener("pointermove", onMove);
3799
4291
  window.removeEventListener("pointerup", onUp);
3800
4292
  if (!started) return;
4293
+ if (yielded()) {
4294
+ teardown();
4295
+ return;
4296
+ }
3801
4297
  el.style.cursor = "grab";
3802
4298
  document.body.style.userSelect = "";
3803
4299
  document.body.style.webkitUserSelect = "";
@@ -3824,6 +4320,7 @@ function startGroupDrag(params) {
3824
4320
  }
3825
4321
  store.moveNode(moverId, position, true);
3826
4322
  }
4323
+ if (initiator) store.endNodeGesture(initiator, [...starts.keys()]);
3827
4324
  repositionAffected();
3828
4325
  };
3829
4326
  window.addEventListener("pointermove", onMove);
@@ -3951,7 +4448,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3951
4448
  return node2?.ariaLabel ?? `node ${id}`;
3952
4449
  };
3953
4450
  const grab = () => {
3954
- const movers = store.getMovers(store.isNodeSelected(id) ? store.getSelectedNodes() : [id]);
4451
+ const movers = store.getMovers(store.isNodeSelected(id) ? store.getSelectedNodes() : [id]).filter((moverId) => store.locks.allows(moverId, "drag"));
4452
+ if (movers.length === 0) {
4453
+ announce(`${nodeName()} is locked by someone else.`);
4454
+ return;
4455
+ }
3955
4456
  const origins = /* @__PURE__ */ new Map();
3956
4457
  for (const moverId of movers) {
3957
4458
  const mover = store.nodes.get(moverId);
@@ -3959,6 +4460,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3959
4460
  }
3960
4461
  grabOrigins.current = origins;
3961
4462
  setGrabbed(true);
4463
+ store.beginNodeGesture(id, movers);
3962
4464
  announce(`Grabbed ${nodeName()}. Use the arrow keys to move, Enter to drop, Escape to cancel.`);
3963
4465
  };
3964
4466
  const releaseGrab = (revert) => {
@@ -3968,9 +4470,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3968
4470
  if (!origins) return;
3969
4471
  if (revert) {
3970
4472
  for (const [moverId, origin] of origins) store.moveNode(moverId, origin, true);
4473
+ store.endNodeGesture(id, [...origins.keys()]);
3971
4474
  announce("Move cancelled.");
3972
4475
  return;
3973
4476
  }
4477
+ store.endNodeGesture(id, [...origins.keys()]);
3974
4478
  const node2 = store.nodes.get(id);
3975
4479
  if (node2) {
3976
4480
  const { x, y } = node2.position;
@@ -3983,6 +4487,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3983
4487
  );
3984
4488
  const getSnapshot = useCallback(() => store.getNodeVersion(id), [store, id]);
3985
4489
  useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
4490
+ const lockHolder = useSyncExternalStore(
4491
+ store.locks.subscribe,
4492
+ useCallback(() => store.locks.getHolder(id), [store, id]),
4493
+ useCallback(() => store.locks.getHolder(id), [store, id])
4494
+ );
3986
4495
  const nodeContext = useMemo(() => ({ nodeId: id, wrapperRef: ref }), [id]);
3987
4496
  useEffect(() => {
3988
4497
  const el = ref.current;
@@ -4015,6 +4524,13 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4015
4524
  if (event.target !== event.currentTarget) return;
4016
4525
  const node2 = store.nodes.get(id);
4017
4526
  if (!node2 || node2.selectable === false) return;
4527
+ if (grabOrigins.current && !store.isNodeHeld(id)) {
4528
+ grabOrigins.current = null;
4529
+ setGrabbed(false);
4530
+ announce(`${nodeName()} was taken by someone else.`);
4531
+ return;
4532
+ }
4533
+ if (!store.locks.allows(id, "select")) return;
4018
4534
  if (event.key === "Enter" || event.key === " ") {
4019
4535
  event.preventDefault();
4020
4536
  if (grabOrigins.current) {
@@ -4067,8 +4583,8 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4067
4583
  const node2 = store.nodes.get(id);
4068
4584
  if (!el || !node2) return;
4069
4585
  const target = event.target instanceof Element ? event.target : null;
4070
- const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect));
4071
- const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag));
4586
+ const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect)) || !store.locks.allows(id, "select");
4587
+ const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag)) || !store.locks.allows(id, "drag");
4072
4588
  const additive = keyHeld(event, multiSelectionKeys);
4073
4589
  if (additive) {
4074
4590
  if (!noSelect) store.selectNode(id, true);
@@ -4078,6 +4594,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4078
4594
  if (noDrag) return;
4079
4595
  startGroupDrag({
4080
4596
  store,
4597
+ nodeId: id,
4081
4598
  el,
4082
4599
  event,
4083
4600
  snapGrid,
@@ -4095,6 +4612,9 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4095
4612
  const Component = nodeTypes[key] ?? BUILTIN_NODE_TYPES[key] ?? DefaultNode;
4096
4613
  const world = store.getNodeWorldPosition(id);
4097
4614
  const depth = store.getNodeDepth(id);
4615
+ const locked = lockHolder !== null;
4616
+ const lockedForSelect = locked && !store.locks.allows(id, "select");
4617
+ const lockedForDrag = locked && !store.locks.allows(id, "drag");
4098
4618
  return (
4099
4619
  // biome-ignore lint/a11y/useSemanticElements: a node wraps arbitrary (often interactive) content, so it can't be a native <button>
4100
4620
  /* @__PURE__ */ jsx(
@@ -4103,12 +4623,14 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4103
4623
  ref,
4104
4624
  "data-rivet-node": id,
4105
4625
  "data-rivet-grabbed": grabbed ? "" : void 0,
4106
- tabIndex: node.selectable === false ? -1 : 0,
4626
+ "data-rivet-locked": locked ? lockHolder ?? "" : void 0,
4627
+ tabIndex: node.selectable === false || lockedForSelect ? -1 : 0,
4107
4628
  role: "button",
4108
4629
  "aria-roledescription": "graph node",
4109
4630
  "aria-pressed": Boolean(node.selected),
4110
4631
  "aria-label": node.ariaLabel,
4111
- "aria-describedby": node.selectable === false ? void 0 : nodeDescriptionId,
4632
+ "aria-disabled": lockedForSelect || void 0,
4633
+ "aria-describedby": node.selectable === false || lockedForSelect ? void 0 : nodeDescriptionId,
4112
4634
  onPointerDown,
4113
4635
  onPointerEnter,
4114
4636
  onPointerLeave,
@@ -4117,6 +4639,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4117
4639
  onBlur,
4118
4640
  style: {
4119
4641
  ...wrapperStyle,
4642
+ ...lockedForDrag ? { cursor: "not-allowed" } : {},
4120
4643
  transform: `translate(${world.x}px, ${world.y}px)`,
4121
4644
  zIndex: depth,
4122
4645
  // Explicit dimensions from NodeResizer override the intrinsic size.
@@ -4494,6 +5017,12 @@ function Rivet({
4494
5017
  snapGrid,
4495
5018
  alignmentGuides = false,
4496
5019
  anchorOptions,
5020
+ peers,
5021
+ onLocalPresence,
5022
+ presenceOptions,
5023
+ lockedNodes,
5024
+ canInteractWithLocked,
5025
+ onNodeLockConflict,
4497
5026
  panOnDrag = true,
4498
5027
  selectionOnDrag = false,
4499
5028
  selectionKeyCode = "Shift",
@@ -4521,6 +5050,8 @@ function Rivet({
4521
5050
  onReconnect,
4522
5051
  onReconnectStart,
4523
5052
  onReconnectEnd,
5053
+ onNodeDragStart,
5054
+ onNodeDragEnd,
4524
5055
  onSelectionChange,
4525
5056
  onFocusChange,
4526
5057
  className,
@@ -4543,6 +5074,7 @@ function Rivet({
4543
5074
  snapGrid,
4544
5075
  swimlaneMargin,
4545
5076
  anchorOptions,
5077
+ presenceOptions,
4546
5078
  panOnDrag,
4547
5079
  selectionKeyCode,
4548
5080
  multiSelectionKeyCode,
@@ -4583,14 +5115,28 @@ function Rivet({
4583
5115
  const hasEdgeChange = Boolean(onEdgesChange);
4584
5116
  useEffect(() => {
4585
5117
  store.setChangeHandlers({
4586
- nodes: hasNodeChange ? (changes) => onNodesChangeRef.current?.(changes) : void 0,
4587
- edges: hasEdgeChange ? (changes) => onEdgesChangeRef.current?.(changes) : void 0
5118
+ nodes: hasNodeChange ? (changes, meta) => onNodesChangeRef.current?.(changes, meta) : void 0,
5119
+ edges: hasEdgeChange ? (changes, meta) => onEdgesChangeRef.current?.(changes, meta) : void 0
4588
5120
  });
4589
5121
  return () => store.setChangeHandlers({});
4590
5122
  }, [store, hasNodeChange, hasEdgeChange]);
5123
+ const onNodeDragStartRef = useRef(onNodeDragStart);
5124
+ onNodeDragStartRef.current = onNodeDragStart;
5125
+ const onNodeDragEndRef = useRef(onNodeDragEnd);
5126
+ onNodeDragEndRef.current = onNodeDragEnd;
5127
+ const onNodeLockConflictRef = useRef(onNodeLockConflict);
5128
+ onNodeLockConflictRef.current = onNodeLockConflict;
5129
+ useEffect(() => {
5130
+ store.setGestureHandlers({
5131
+ start: (event) => onNodeDragStartRef.current?.(event),
5132
+ end: (event) => onNodeDragEndRef.current?.(event),
5133
+ conflict: (event) => onNodeLockConflictRef.current?.(event)
5134
+ });
5135
+ return () => store.setGestureHandlers({});
5136
+ }, [store]);
4591
5137
  const isControlled = controlledNodes !== void 0 || controlledEdges !== void 0;
4592
5138
  useEffect(() => {
4593
- if (!isControlled || store.isNodeDragging() || store.isNodeResizing()) return;
5139
+ if (!isControlled) return;
4594
5140
  store.reconcile(
4595
5141
  controlledNodes ?? [...store.nodes.values()],
4596
5142
  controlledEdges ?? [...store.edges.values()]
@@ -4643,6 +5189,23 @@ function Rivet({
4643
5189
  const backgroundCanvasRef = useRef(null);
4644
5190
  const edgeCanvasRef = useRef(null);
4645
5191
  const foregroundCanvasRef = useRef(null);
5192
+ useEffect(() => {
5193
+ if (peers === void 0) return;
5194
+ store.presence.setPeers(peers);
5195
+ }, [store, peers]);
5196
+ const canInteractWithLockedRef = useRef(canInteractWithLocked);
5197
+ canInteractWithLockedRef.current = canInteractWithLocked;
5198
+ const hasLockPolicy = Boolean(canInteractWithLocked);
5199
+ useEffect(() => {
5200
+ store.locks.setPolicy(
5201
+ hasLockPolicy ? (event) => canInteractWithLockedRef.current?.(event) ?? true : null
5202
+ );
5203
+ return () => store.locks.setPolicy(null);
5204
+ }, [store, hasLockPolicy]);
5205
+ useEffect(() => {
5206
+ store.setLockedNodes(lockedNodes);
5207
+ }, [store, lockedNodes]);
5208
+ useLocalPresence({ store, paneRef, onLocalPresence, throttleMs: cfg.presenceOptions.throttleMs });
4646
5209
  const { visibleIds, controls, getViewportElements } = useRivetRuntime({
4647
5210
  store,
4648
5211
  paneRef,
@@ -4654,6 +5217,7 @@ function Rivet({
4654
5217
  edgeTypes: cfg.edgeTypes,
4655
5218
  defaultEdgeOptions: cfg.defaultEdgeOptions,
4656
5219
  anchorOptions: cfg.anchorOptions,
5220
+ presenceOptions: cfg.presenceOptions,
4657
5221
  edgeRenderer: renderer,
4658
5222
  edgesReconnectable,
4659
5223
  isValidConnection: cfg.isValidConnection,
@@ -4825,6 +5389,7 @@ function buildInstance(store, controls, getViewportElements) {
4825
5389
  for (const id of edges ?? []) store.removeEdge(id);
4826
5390
  for (const id of nodes ?? []) store.removeNode(id);
4827
5391
  },
5392
+ applyRemote: (changes) => store.applyRemote(changes),
4828
5393
  registerAnchor: (nodeId, anchorId, element, options) => {
4829
5394
  store.registerAnchor(nodeId, anchorId, element, options);
4830
5395
  return {
@@ -4834,6 +5399,10 @@ function buildInstance(store, controls, getViewportElements) {
4834
5399
  },
4835
5400
  unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
4836
5401
  remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
5402
+ setPeerCursor: (peerId, point) => store.presence.setPeerCursor(peerId, point),
5403
+ setPeerNodeTransform: (peerId, nodeId, rect) => store.presence.setPeerNodeTransform(peerId, nodeId, rect),
5404
+ removePeer: (peerId) => store.presence.removePeer(peerId),
5405
+ releaseNodeGesture: (id, ids) => store.endNodeGesture(id, ids),
4837
5406
  copy: (ids) => {
4838
5407
  const targetIds = ids ?? store.getSelectedNodes();
4839
5408
  if (targetIds.length === 0) return;