@kolosal-ai/rivet 0.1.0 → 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
 
@@ -313,6 +315,22 @@ function resolveFacingSide(node, peer, previous, hysteresis = DEFAULT_ALIGNMENT_
313
315
  if (Math.abs(dx) > Math.abs(dy) * hysteresis) return dx > 0 ? "right" : "left";
314
316
  return dy > 0 ? "bottom" : "top";
315
317
  }
318
+ function bestAllowedSide(node, peer, allowed) {
319
+ const dx = peer.x + peer.width / 2 - (node.x + node.width / 2);
320
+ const dy = peer.y + peer.height / 2 - (node.y + node.height / 2);
321
+ const score = { left: -dx, right: dx, top: -dy, bottom: dy };
322
+ let best = null;
323
+ for (const side of allowed) {
324
+ if (best === null || score[side] > score[best]) best = side;
325
+ }
326
+ return best;
327
+ }
328
+ function resolveFacingSideAmong(node, peer, allowed, previous, hysteresis = DEFAULT_ALIGNMENT_HYSTERESIS) {
329
+ const resolved = resolveFacingSide(node, peer, previous, hysteresis);
330
+ if (allowed.length === 0 || allowed.includes(resolved)) return resolved;
331
+ if (previous && allowed.includes(previous)) return previous;
332
+ return bestAllowedSide(node, peer, allowed) ?? resolved;
333
+ }
316
334
 
317
335
  // src/edge-ends.ts
318
336
  function displayedSideKey(edgeId, end) {
@@ -340,6 +358,35 @@ function parseEdgeEndpoint(nodeId, handleId, handles) {
340
358
  return side ? { nodeId, side } : { nodeId };
341
359
  }
342
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
+
343
390
  // src/graph.ts
344
391
  function clampChildToParent(position, childSize, parentSize) {
345
392
  const maxX = Math.max(0, parentSize.width - childSize.width);
@@ -506,6 +553,26 @@ var HistoryManager = class {
506
553
  this.last = this.snapshot();
507
554
  });
508
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
+ }
509
576
  flush() {
510
577
  this.scheduled = false;
511
578
  this.undoStack.push(this.last);
@@ -559,12 +626,407 @@ var HistoryManager = class {
559
626
  }
560
627
  };
561
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
+
562
1020
  // src/store.ts
563
1021
  var RivetGraphStore = class {
564
1022
  nodes = /* @__PURE__ */ new Map();
565
1023
  edges = /* @__PURE__ */ new Map();
566
1024
  handles = /* @__PURE__ */ new Map();
567
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());
568
1030
  viewport;
569
1031
  viewportClamp = null;
570
1032
  pending = null;
@@ -590,6 +1052,11 @@ var RivetGraphStore = class {
590
1052
  nodeResizing = false;
591
1053
  edgeAlignment = "manual";
592
1054
  draggingNodeIds = /* @__PURE__ */ new Set();
1055
+ heldNodeIds = /* @__PURE__ */ new Set();
1056
+ heldListeners = /* @__PURE__ */ new Set();
1057
+ gestureStartHandler = null;
1058
+ gestureEndHandler = null;
1059
+ lockConflictHandler = null;
593
1060
  displayedSides = /* @__PURE__ */ new Map();
594
1061
  edgeLabelAnchors = /* @__PURE__ */ new Map();
595
1062
  alignmentGuides = [];
@@ -599,6 +1066,10 @@ var RivetGraphStore = class {
599
1066
  history = new HistoryManager(() => this.historySnapshot());
600
1067
  lastSelectionKey;
601
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;
602
1073
  renderRequester = () => {
603
1074
  };
604
1075
  constructor(init) {
@@ -613,6 +1084,7 @@ var RivetGraphStore = class {
613
1084
  // --- internal bookkeeping ------------------------------------------------
614
1085
  invalidateWorld() {
615
1086
  this.worldCache = null;
1087
+ this.peerWorldCache = null;
616
1088
  }
617
1089
  /**
618
1090
  * The single node-map write path. Invalidates the world-position cache exactly
@@ -650,17 +1122,22 @@ var RivetGraphStore = class {
650
1122
  const selection = this.buildSelection();
651
1123
  for (const listener of this.selectionListeners) listener(selection);
652
1124
  }
1125
+ notifyHeld() {
1126
+ this.peerWorldCache = null;
1127
+ this.requestRender();
1128
+ for (const listener of this.heldListeners) listener();
1129
+ }
653
1130
  /**
654
1131
  * The one emission policy. Unless a `reconcile` is applying incoming props
655
1132
  * (which must never echo them back out), record the batch to history — when
656
1133
  * `record` is set — then hand it to the controlled consumer. Recording happens
657
1134
  * before the handler so history works in uncontrolled mode too.
658
1135
  */
659
- emit(changes, handler, record) {
1136
+ emit(changes, handler, record, origin = "local") {
660
1137
  if (this.reconciling || changes.length === 0) return;
661
1138
  if (record) this.history.record(hasRecordableChange(changes));
662
1139
  else if (hasRecordableChange(changes)) this.history.rebase();
663
- handler?.(changes);
1140
+ handler?.(changes, { origin });
664
1141
  }
665
1142
  emitNodeChanges(changes, record = true) {
666
1143
  this.emit(changes, this.nodeChangeHandler, record);
@@ -805,10 +1282,11 @@ var RivetGraphStore = class {
805
1282
  this.writeNode(node.id, node);
806
1283
  continue;
807
1284
  }
808
- if (nodeControlledEqual(existing, node)) continue;
809
- 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);
810
1288
  this.bumpNode(node.id);
811
- 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) {
812
1290
  moved.push(node.id);
813
1291
  }
814
1292
  }
@@ -901,12 +1379,47 @@ var RivetGraphStore = class {
901
1379
  this.worldCache = /* @__PURE__ */ new Map();
902
1380
  for (const id of this.nodes.keys()) this.worldCache.set(id, worldPosition(this.nodes, id));
903
1381
  }
904
- 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;
905
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
+ }
906
1418
  getNodeWorldPosition = (id) => this.getWorldPositions().get(id) ?? { x: 0, y: 0 };
907
1419
  getNodeRect = (id) => {
908
1420
  const origin = this.getNodeWorldPosition(id);
909
- 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;
910
1423
  return { x: origin.x, y: origin.y, width: size.width, height: size.height };
911
1424
  };
912
1425
  getNodeDepth = (id) => nodeDepth(this.nodes, id);
@@ -1009,6 +1522,16 @@ var RivetGraphStore = class {
1009
1522
  * candidate (single-handle nodes, custom layouts, a culled peer whose
1010
1523
  * handles are unregistered) means the pin is kept.
1011
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
+ }
1012
1535
  realignEndHandle(nodeId, handleId, side, end) {
1013
1536
  if (!handleId || parseAnchorAutoHandleId(handleId)) return handleId;
1014
1537
  const anchor = parseAnchorHandleId(handleId);
@@ -1050,8 +1573,8 @@ var RivetGraphStore = class {
1050
1573
  const targetRect = this.getNodeRect(edge.target);
1051
1574
  const sourceKey = displayedSideKey(edgeId, "source");
1052
1575
  const targetKey = displayedSideKey(edgeId, "target");
1053
- const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? facingSide(sourceRect, targetRect);
1054
- 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");
1055
1578
  const sourceHandle = this.realignEndHandle(
1056
1579
  edge.source,
1057
1580
  edge.sourceHandle,
@@ -1085,7 +1608,43 @@ var RivetGraphStore = class {
1085
1608
  this.nodeDragging = dragging;
1086
1609
  if (!dragging) this.draggingNodeIds.clear();
1087
1610
  };
1088
- isNodeResizing = () => this.nodeResizing;
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
+ };
1089
1648
  selectNode = (id, additive = false) => {
1090
1649
  this.setSelectionBoxActive(false);
1091
1650
  if (id === null) {
@@ -1204,6 +1763,7 @@ var RivetGraphStore = class {
1204
1763
  }
1205
1764
  if (this.selectedNodeIds.size > 0) {
1206
1765
  for (const id of [...this.selectedNodeIds]) {
1766
+ if (!this.locks.allows(id, "delete")) continue;
1207
1767
  if (this.removeNodeInternal(id, removedEdges)) removedNodes.push(id);
1208
1768
  }
1209
1769
  }
@@ -1233,6 +1793,19 @@ var RivetGraphStore = class {
1233
1793
  this.history.rebase();
1234
1794
  this.requestRender();
1235
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
+ };
1236
1809
  // --- undo / redo ---------------------------------------------------------
1237
1810
  /** Structural snapshot for history — no selection/hover/drag state. */
1238
1811
  historySnapshot() {
@@ -1449,45 +2022,6 @@ function createRivetStore(init) {
1449
2022
  return new RivetGraphStore(init);
1450
2023
  }
1451
2024
 
1452
- // src/viewport.ts
1453
- function worldToScreen(point, viewport) {
1454
- return {
1455
- x: point.x * viewport.zoom + viewport.x,
1456
- y: point.y * viewport.zoom + viewport.y
1457
- };
1458
- }
1459
- function screenToWorld(point, viewport) {
1460
- return {
1461
- x: (point.x - viewport.x) / viewport.zoom,
1462
- y: (point.y - viewport.y) / viewport.zoom
1463
- };
1464
- }
1465
- function zoomAt(viewport, anchor, nextZoom, minZoom = 0.1, maxZoom = 4) {
1466
- const zoom = clamp(nextZoom, minZoom, maxZoom);
1467
- const world = screenToWorld(anchor, viewport);
1468
- return {
1469
- zoom,
1470
- x: anchor.x - world.x * zoom,
1471
- y: anchor.y - world.y * zoom
1472
- };
1473
- }
1474
- function viewportToCss(viewport) {
1475
- return `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
1476
- }
1477
- function visibleWorldRect(viewport, width, height) {
1478
- const topLeft = screenToWorld({ x: 0, y: 0 }, viewport);
1479
- const bottomRight = screenToWorld({ x: width, y: height }, viewport);
1480
- return {
1481
- x: topLeft.x,
1482
- y: topLeft.y,
1483
- width: bottomRight.x - topLeft.x,
1484
- height: bottomRight.y - topLeft.y
1485
- };
1486
- }
1487
- function rectsIntersect(a, b) {
1488
- 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;
1489
- }
1490
-
1491
2025
  // src/connection.ts
1492
2026
  function clientToWorld(store, paneRef, clientX, clientY) {
1493
2027
  const rect = paneRef.current?.getBoundingClientRect();
@@ -1579,8 +2113,17 @@ function startConnectionDrag(config) {
1579
2113
  validate,
1580
2114
  map,
1581
2115
  onEnd,
1582
- resolveHit = resolveConnectionHit
2116
+ resolveHit: resolveHitInput = resolveConnectionHit
1583
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
+ };
1584
2127
  store.beginConnection(begin);
1585
2128
  const onMove = (event) => {
1586
2129
  const hit = resolveHit(store, paneRef, from, event.clientX, event.clientY);
@@ -1683,6 +2226,10 @@ function Handle({
1683
2226
  if (event.key !== "Enter" && event.key !== " ") return;
1684
2227
  event.preventDefault();
1685
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
+ }
1686
2233
  const pending = store.getPending();
1687
2234
  if (!pending) {
1688
2235
  const origin = handleWorldPosition(store, self);
@@ -1735,6 +2282,7 @@ function Handle({
1735
2282
  const onFocus = () => {
1736
2283
  const pending = store.getPending();
1737
2284
  if (!pending || pending.source === nodeId && pending.sourceHandle === handleId) return;
2285
+ if (!store.locks.allows(nodeId, "connect")) return;
1738
2286
  const world = handleWorldPosition(store, { nodeId, handleId});
1739
2287
  if (world) store.updateConnection(world, place);
1740
2288
  };
@@ -1819,7 +2367,12 @@ var baseStyle = {
1819
2367
  minWidth: 120,
1820
2368
  padding: "10px 14px",
1821
2369
  borderRadius: 8,
1822
- 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)",
1823
2376
  background: "#ffffff",
1824
2377
  color: "#0f172a",
1825
2378
  fontSize: 13,
@@ -1851,7 +2404,10 @@ var baseStyle2 = {
1851
2404
  minHeight: 100,
1852
2405
  boxSizing: "border-box",
1853
2406
  borderRadius: 10,
1854
- 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)",
1855
2411
  background: "rgba(100, 116, 139, 0.06)"
1856
2412
  };
1857
2413
  var hoveredStyle2 = {
@@ -2035,6 +2591,11 @@ function NodeResizer({
2035
2591
  }) {
2036
2592
  const { store } = useRivetContext();
2037
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
+ );
2038
2599
  const startResize = (dir) => (event) => {
2039
2600
  if (event.button !== 0) return;
2040
2601
  event.stopPropagation();
@@ -2066,7 +2627,16 @@ function NodeResizer({
2066
2627
  };
2067
2628
  return { size: { width, height }, position };
2068
2629
  };
2630
+ let started = false;
2069
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
+ }
2070
2640
  const { size, position } = compute(moveEvent);
2071
2641
  const moved = dir.left || dir.top;
2072
2642
  store.resizeNode(nodeId, size, moved ? position : void 0, false);
@@ -2075,14 +2645,17 @@ function NodeResizer({
2075
2645
  const onUp = (upEvent) => {
2076
2646
  window.removeEventListener("pointermove", onMove);
2077
2647
  window.removeEventListener("pointerup", onUp);
2648
+ if (started && !store.isNodeHeld(nodeId)) return;
2078
2649
  const { size, position } = compute(upEvent);
2079
2650
  const moved = dir.left || dir.top;
2080
2651
  store.resizeNode(nodeId, size, moved ? position : void 0, true);
2652
+ if (started) store.endNodeGesture(nodeId);
2081
2653
  onResizeEnd?.(size, position);
2082
2654
  };
2083
2655
  window.addEventListener("pointermove", onMove);
2084
2656
  window.addEventListener("pointerup", onUp);
2085
2657
  };
2658
+ if (!resizable) return null;
2086
2659
  return /* @__PURE__ */ jsx(Fragment, { children: HANDLES.map((dir) => /* @__PURE__ */ jsx(
2087
2660
  "div",
2088
2661
  {
@@ -2098,6 +2671,106 @@ function NodeResizer({
2098
2671
  dir.key
2099
2672
  )) });
2100
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
+ }
2101
2774
  function shallowEqual(a, b) {
2102
2775
  if (Object.is(a, b)) return true;
2103
2776
  if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
@@ -2153,6 +2826,11 @@ function useRivetConfig(params) {
2153
2826
  () => ({ ...DEFAULT_ANCHOR_OPTIONS, ...anchorOptionsInput }),
2154
2827
  [anchorOptionsInput]
2155
2828
  );
2829
+ const presenceOptionsInput = useShallowStable(params.presenceOptions);
2830
+ const presenceOptions = useMemo(
2831
+ () => ({ ...DEFAULT_PRESENCE_OPTIONS, ...presenceOptionsInput }),
2832
+ [presenceOptionsInput]
2833
+ );
2156
2834
  const isValidConnection = useStableOptional(params.isValidConnection);
2157
2835
  const mapConnection = useStableOptional(params.mapConnection);
2158
2836
  const onConnect = useStableOptional(params.onConnect);
@@ -2166,6 +2844,7 @@ function useRivetConfig(params) {
2166
2844
  snapGrid,
2167
2845
  swimlaneMargin,
2168
2846
  anchorOptions,
2847
+ presenceOptions,
2169
2848
  panButtons,
2170
2849
  selectionKeys,
2171
2850
  multiSelectionKeys,
@@ -2187,7 +2866,7 @@ var ARROW_DIRS = {
2187
2866
  ArrowLeft: { x: -1, y: 0 },
2188
2867
  ArrowRight: { x: 1, y: 0 }
2189
2868
  };
2190
- function createKeyboardHandler(store) {
2869
+ function createKeyboardHandler(store, getSnapGrid) {
2191
2870
  return (event) => {
2192
2871
  if (event.defaultPrevented) return;
2193
2872
  const active = document.activeElement;
@@ -2205,18 +2884,19 @@ function createKeyboardHandler(store) {
2205
2884
  }
2206
2885
  const dir = ARROW_DIRS[event.key];
2207
2886
  if (!dir) return;
2208
- const movers = store.getMovers(store.getSelectedNodes());
2887
+ const movers = store.getMovers(store.getSelectedNodes()).filter((id) => store.locks.allows(id, "drag"));
2209
2888
  if (movers.length === 0) return;
2210
2889
  event.preventDefault();
2211
- const step = event.shiftKey ? ARROW_STEP * ARROW_STEP_SHIFT : ARROW_STEP;
2890
+ const snapGrid = getSnapGrid?.() ?? null;
2891
+ const factor = event.shiftKey ? ARROW_STEP_SHIFT : 1;
2892
+ const stepX = (snapGrid?.[0] || ARROW_STEP) * factor;
2893
+ const stepY = (snapGrid?.[1] || ARROW_STEP) * factor;
2212
2894
  for (const id of movers) {
2213
2895
  const node = store.nodes.get(id);
2214
2896
  if (!node) continue;
2215
- store.moveNode(
2216
- id,
2217
- { x: node.position.x + dir.x * step, y: node.position.y + dir.y * step },
2218
- true
2219
- );
2897
+ let next = { x: node.position.x + dir.x * stepX, y: node.position.y + dir.y * stepY };
2898
+ if (snapGrid) next = snapToGrid(next, snapGrid);
2899
+ store.moveNode(id, next, true);
2220
2900
  }
2221
2901
  };
2222
2902
  }
@@ -2282,6 +2962,7 @@ function createMarqueeController(deps) {
2282
2962
  const ids = [];
2283
2963
  for (const [id, node] of store.nodes) {
2284
2964
  if (node.selectable === false) continue;
2965
+ if (!store.locks.allows(id, "select")) continue;
2285
2966
  if (rectsIntersect(worldRect, store.getNodeRect(id))) ids.push(id);
2286
2967
  }
2287
2968
  store.selectNodes(additive ? [.../* @__PURE__ */ new Set([...base, ...ids])] : ids);
@@ -2428,363 +3109,138 @@ function createZoomController(deps) {
2428
3109
 
2429
3110
  // src/renderer/background.ts
2430
3111
  function drawDotGrid(ctx, width, height, viewport, options) {
2431
- const gap = options?.gap ?? 24;
2432
- const radius = options?.radius ?? 1;
2433
- const color = options?.color ?? "rgba(100, 116, 139, 0.35)";
2434
- const step = gap * viewport.zoom;
2435
- if (step < 8) return;
2436
- const offsetX = (viewport.x % step + step) % step;
2437
- const offsetY = (viewport.y % step + step) % step;
2438
- ctx.fillStyle = color;
2439
- ctx.beginPath();
2440
- for (let x = offsetX; x < width; x += step) {
2441
- for (let y = offsetY; y < height; y += step) {
2442
- ctx.moveTo(x + radius, y);
2443
- ctx.arc(x, y, radius, 0, Math.PI * 2);
2444
- }
2445
- }
2446
- ctx.fill();
2447
- }
2448
-
2449
- // src/renderer/edge-paths.ts
2450
- var SIDE_DIR = {
2451
- left: { x: -1, y: 0 },
2452
- right: { x: 1, y: 0 },
2453
- top: { x: 0, y: -1 },
2454
- bottom: { x: 0, y: 1 }
2455
- };
2456
- var BEZIER_SEGMENT_PX = 8;
2457
- var BEZIER_MIN_SAMPLES = 16;
2458
- var BEZIER_MAX_SAMPLES = 160;
2459
- var CORNER_RADIUS = 8;
2460
- var ARC_SAMPLES = 6;
2461
- function cubicAt(a, b, c, d, t) {
2462
- const mt = 1 - t;
2463
- return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
2464
- }
2465
- function quadAt(a, b, c, t) {
2466
- const mt = 1 - t;
2467
- return mt * mt * a + 2 * mt * t * b + t * t * c;
2468
- }
2469
- var getStraightPath = ({ sourceX, sourceY, targetX, targetY }) => ({
2470
- points: [
2471
- { x: sourceX, y: sourceY },
2472
- { x: targetX, y: targetY }
2473
- ],
2474
- labelX: (sourceX + targetX) / 2,
2475
- labelY: (sourceY + targetY) / 2
2476
- });
2477
- var getBezierPath = (params) => {
2478
- const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2479
- const sDir = SIDE_DIR[sourcePosition];
2480
- const tDir = SIDE_DIR[targetPosition];
2481
- const dist = Math.hypot(targetX - sourceX, targetY - sourceY);
2482
- const reach = Math.max(40, dist * 0.4);
2483
- const p0 = { x: sourceX, y: sourceY };
2484
- const p1 = { x: sourceX + sDir.x * reach, y: sourceY + sDir.y * reach };
2485
- const p2 = { x: targetX + tDir.x * reach, y: targetY + tDir.y * reach };
2486
- const p3 = { x: targetX, y: targetY };
2487
- 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);
2488
- const samples = Math.max(
2489
- BEZIER_MIN_SAMPLES,
2490
- Math.min(BEZIER_MAX_SAMPLES, Math.ceil(ctrlLen / BEZIER_SEGMENT_PX))
2491
- );
2492
- const points = [];
2493
- for (let i = 0; i <= samples; i++) {
2494
- const t = i / samples;
2495
- points.push({
2496
- x: cubicAt(p0.x, p1.x, p2.x, p3.x, t),
2497
- y: cubicAt(p0.y, p1.y, p2.y, p3.y, t)
2498
- });
2499
- }
2500
- return {
2501
- points,
2502
- labelX: cubicAt(p0.x, p1.x, p2.x, p3.x, 0.5),
2503
- labelY: cubicAt(p0.y, p1.y, p2.y, p3.y, 0.5)
2504
- };
2505
- };
2506
- var STEP_OFFSET = 20;
2507
- function simplifyCorners(corners) {
2508
- const out = [];
2509
- for (const p of corners) {
2510
- const a = out[out.length - 2];
2511
- const b = out[out.length - 1];
2512
- if (b && b.x === p.x && b.y === p.y) continue;
2513
- if (a && b && (a.x === b.x && b.x === p.x || a.y === b.y && b.y === p.y)) out.pop();
2514
- out.push(p);
2515
- }
2516
- return out;
2517
- }
2518
- function stepCorners(params) {
2519
- const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2520
- const s = { x: sourceX, y: sourceY };
2521
- const t = { x: targetX, y: targetY };
2522
- const sDir = SIDE_DIR[sourcePosition];
2523
- const tDir = SIDE_DIR[targetPosition];
2524
- const sg = { x: sourceX + sDir.x * STEP_OFFSET, y: sourceY + sDir.y * STEP_OFFSET };
2525
- const tg = { x: targetX + tDir.x * STEP_OFFSET, y: targetY + tDir.y * STEP_OFFSET };
2526
- return simplifyCorners(routeCorners(s, t, sg, tg, sDir, tDir));
2527
- }
2528
- function routeCorners(s, t, sg, tg, sDir, tDir) {
2529
- const sHoriz = sDir.x !== 0;
2530
- const tHoriz = tDir.x !== 0;
2531
- if (sHoriz && tHoriz) {
2532
- if (sDir.x * tDir.x < 0) {
2533
- if ((t.x - s.x) * sDir.x >= 2 * STEP_OFFSET) {
2534
- const midX = (s.x + t.x) / 2;
2535
- return [s, { x: midX, y: s.y }, { x: midX, y: t.y }, t];
2536
- }
2537
- const midY = (s.y + t.y) / 2;
2538
- return [s, sg, { x: sg.x, y: midY }, { x: tg.x, y: midY }, tg, t];
2539
- }
2540
- const railX = sDir.x > 0 ? Math.max(sg.x, tg.x) : Math.min(sg.x, tg.x);
2541
- return [s, { x: railX, y: s.y }, { x: railX, y: t.y }, t];
2542
- }
2543
- if (!sHoriz && !tHoriz) {
2544
- if (sDir.y * tDir.y < 0) {
2545
- if ((t.y - s.y) * sDir.y >= 2 * STEP_OFFSET) {
2546
- const midY = (s.y + t.y) / 2;
2547
- return [s, { x: s.x, y: midY }, { x: t.x, y: midY }, t];
2548
- }
2549
- const midX = (s.x + t.x) / 2;
2550
- return [s, sg, { x: midX, y: sg.y }, { x: midX, y: tg.y }, tg, t];
2551
- }
2552
- const railY = sDir.y > 0 ? Math.max(sg.y, tg.y) : Math.min(sg.y, tg.y);
2553
- return [s, { x: s.x, y: railY }, { x: t.x, y: railY }, t];
2554
- }
2555
- const corner = sHoriz ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
2556
- const exitsSource = sHoriz ? (corner.x - s.x) * sDir.x >= STEP_OFFSET : (corner.y - s.y) * sDir.y >= STEP_OFFSET;
2557
- const exitsTarget = tHoriz ? (corner.x - t.x) * tDir.x >= STEP_OFFSET : (corner.y - t.y) * tDir.y >= STEP_OFFSET;
2558
- if (exitsSource && exitsTarget) return [s, corner, t];
2559
- const bend = sHoriz ? { x: sg.x, y: tg.y } : { x: tg.x, y: sg.y };
2560
- return [s, sg, bend, tg, t];
2561
- }
2562
- function roundCorners(corners, radius) {
2563
- if (corners.length <= 2) return corners;
2564
- const points = [];
2565
- const first = corners[0];
2566
- if (first) points.push(first);
2567
- for (let i = 1; i < corners.length - 1; i++) {
2568
- const prev = corners[i - 1];
2569
- const curr = corners[i];
2570
- const next = corners[i + 1];
2571
- if (!prev || !curr || !next) continue;
2572
- const inLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
2573
- const outLen = Math.hypot(next.x - curr.x, next.y - curr.y);
2574
- const r = Math.min(radius, inLen / 2, outLen / 2);
2575
- const inPt = {
2576
- x: curr.x + (prev.x - curr.x) / (inLen || 1) * r,
2577
- y: curr.y + (prev.y - curr.y) / (inLen || 1) * r
2578
- };
2579
- const outPt = {
2580
- x: curr.x + (next.x - curr.x) / (outLen || 1) * r,
2581
- y: curr.y + (next.y - curr.y) / (outLen || 1) * r
2582
- };
2583
- points.push(inPt);
2584
- for (let j = 1; j < ARC_SAMPLES; j++) {
2585
- const tt = j / ARC_SAMPLES;
2586
- points.push({
2587
- x: quadAt(inPt.x, curr.x, outPt.x, tt),
2588
- y: quadAt(inPt.y, curr.y, outPt.y, tt)
2589
- });
3112
+ const gap = options?.gap ?? 24;
3113
+ const radius = options?.radius ?? 1;
3114
+ const color = options?.color ?? "rgba(100, 116, 139, 0.35)";
3115
+ const step = gap * viewport.zoom;
3116
+ if (step < 8) return;
3117
+ const offsetX = (viewport.x % step + step) % step;
3118
+ const offsetY = (viewport.y % step + step) % step;
3119
+ ctx.fillStyle = color;
3120
+ ctx.beginPath();
3121
+ for (let x = offsetX; x < width; x += step) {
3122
+ for (let y = offsetY; y < height; y += step) {
3123
+ ctx.moveTo(x + radius, y);
3124
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
2590
3125
  }
2591
- points.push(outPt);
2592
3126
  }
2593
- const last = corners[corners.length - 1];
2594
- if (last) points.push(last);
2595
- return points;
2596
- }
2597
- function stepLabel(corners, params) {
2598
- const i = Math.floor((corners.length - 1) / 2);
2599
- const a = corners[i];
2600
- const b = corners[i + 1];
2601
- if (!a || !b) return { x: params.sourceX, y: params.sourceY };
2602
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
3127
+ ctx.fill();
2603
3128
  }
2604
- var getSmoothStepPath = (params) => {
2605
- const corners = stepCorners(params);
2606
- const label = stepLabel(corners, params);
2607
- return { points: roundCorners(corners, CORNER_RADIUS), labelX: label.x, labelY: label.y };
2608
- };
2609
- var getStepPath = (params) => {
2610
- const corners = stepCorners(params);
2611
- const label = stepLabel(corners, params);
2612
- return { points: corners, labelX: label.x, labelY: label.y };
2613
- };
2614
- var BUILTIN_EDGE_TYPES = {
2615
- bezier: getBezierPath,
2616
- smoothstep: getSmoothStepPath,
2617
- step: getStepPath,
2618
- straight: getStraightPath
2619
- };
2620
3129
 
2621
- // src/renderer/edge-geometry.ts
2622
- var EDGE_COLOR = "rgba(100, 116, 139, 0.75)";
2623
- var EDGE_COLOR_SELECTED = "#6366f1";
2624
- var EDGE_COLOR_HOVERED = "rgba(99, 102, 241, 0.9)";
2625
- var EDGE_WIDTH = 1.5;
2626
- var EDGE_WIDTH_HOVERED = 2.5;
2627
- var PENDING_COLOR = "#6366f1";
2628
- var OPPOSITE = {
2629
- left: "right",
2630
- right: "left",
2631
- top: "bottom",
2632
- bottom: "top"
2633
- };
2634
- var falseToNull = (m) => m || null;
2635
- function buildEdges(edges, nodes, viewport, edgeTypes, defaults, extras) {
2636
- const handles = extras?.handles;
2637
- const hovered = extras?.hovered;
2638
- const reconnecting = extras?.reconnecting;
2639
- const worldPositions = extras?.worldPositions;
2640
- const resolved = [];
2641
- for (const edge of edges) {
2642
- if (edge.id === reconnecting) continue;
2643
- const source = nodes.get(edge.source);
2644
- const target = nodes.get(edge.target);
2645
- if (!source || !target) continue;
2646
- const frame = { handles, viewport, worldPositions, extras };
2647
- const start = resolveEnd(edge, "source", source, target, frame);
2648
- const end = resolveEnd(edge, "target", target, source, frame);
2649
- const params = {
2650
- sourceX: start.point.x,
2651
- sourceY: start.point.y,
2652
- targetX: end.point.x,
2653
- targetY: end.point.y,
2654
- sourcePosition: start.position,
2655
- targetPosition: end.position
2656
- };
2657
- const pathFn = edgeTypes[edge.type ?? defaults.type ?? "bezier"] ?? getBezierPath;
2658
- const path = pathFn(params);
2659
- if (path.points.length < 2) continue;
2660
- const style = { ...defaults.style, ...edge.style };
2661
- const isHovered = edge.id === hovered;
2662
- resolved.push({
2663
- id: edge.id,
2664
- points: path.points,
2665
- source: start.point,
2666
- target: end.point,
2667
- label: edge.label !== void 0 ? { x: path.labelX, y: path.labelY } : void 0,
2668
- stroke: edge.selected ? EDGE_COLOR_SELECTED : isHovered ? EDGE_COLOR_HOVERED : style.stroke ?? EDGE_COLOR,
2669
- width: style.strokeWidth ?? (isHovered ? EDGE_WIDTH_HOVERED : EDGE_WIDTH),
2670
- opacity: style.opacity ?? 1,
2671
- animated: edge.animated ?? defaults.animated ?? false,
2672
- dash: style.strokeDasharray,
2673
- markerStart: falseToNull(edge.markerStart ?? defaults.markerStart),
2674
- markerEnd: falseToNull(edge.markerEnd ?? defaults.markerEnd)
2675
- });
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);
2676
3156
  }
2677
- return resolved;
2678
3157
  }
2679
- function buildPendingPath(pending, handles, viewport) {
2680
- const record = handles?.get(handleKey(pending.source, pending.sourceHandle));
2681
- const sourcePosition = record?.position ?? (pending.sourceType === "target" ? "left" : "right");
2682
- const from = worldToScreen(pending.from, viewport);
2683
- const to = worldToScreen(pending.to, viewport);
2684
- const targetPosition = pending.toPosition ?? OPPOSITE[sourcePosition];
2685
- return getBezierPath({
2686
- sourceX: from.x,
2687
- sourceY: from.y,
2688
- targetX: to.x,
2689
- targetY: to.y,
2690
- sourcePosition,
2691
- targetPosition
2692
- }).points;
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
+ };
2693
3166
  }
2694
- function pointOnSide(rect, side, alongPct = 50) {
2695
- const t = alongPct / 100;
2696
- switch (side) {
2697
- case "left":
2698
- return { x: rect.x, y: rect.y + rect.height * t };
2699
- case "right":
2700
- return { x: rect.x + rect.width, y: rect.y + rect.height * t };
2701
- case "top":
2702
- return { x: rect.x + rect.width * t, y: rect.y };
2703
- case "bottom":
2704
- return { x: rect.x + rect.width * t, y: rect.y + rect.height };
2705
- }
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;
2706
3169
  }
2707
- function nodeRect(node, worldPositions) {
2708
- const origin = worldPositions?.get(node.id) ?? node.position;
2709
- const size = node.size ?? DEFAULT_NODE_SIZE;
2710
- return { x: origin.x, y: origin.y, width: size.width, height: size.height };
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([]);
2711
3177
  }
2712
- var WANTED_TYPES = {
2713
- source: ["source", "either"],
2714
- target: ["target", "either"]
2715
- };
2716
- function handleOnSide(nodeId, side, role, handles) {
2717
- if (!handles) return null;
2718
- for (const record of handles.values()) {
2719
- if (record.nodeId === nodeId && record.position === side && WANTED_TYPES[role].includes(record.type)) {
2720
- return record;
2721
- }
2722
- }
2723
- return null;
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);
2724
3201
  }
2725
- function resolveEnd(edge, role, node, peer, frame) {
2726
- const { handles, viewport, worldPositions, extras } = frame;
2727
- const handleId = role === "source" ? edge.sourceHandle : edge.targetHandle;
2728
- const rect = nodeRect(node, worldPositions);
2729
- const anchorId = parseAnchorAutoHandleId(handleId) ?? parseAnchorHandleId(handleId)?.anchorId ?? null;
2730
- const endAt = (side2) => {
2731
- const alongPct = (anchorId ? extras?.anchorPlacement?.(node.id, anchorId, side2) : null) ?? void 0;
2732
- if (alongPct === void 0 && !anchorId) {
2733
- const candidate = handleOnSide(node.id, side2, role, handles);
2734
- if (candidate) {
2735
- return {
2736
- point: worldToScreen(
2737
- { x: rect.x + candidate.offset.x, y: rect.y + candidate.offset.y },
2738
- viewport
2739
- ),
2740
- position: side2
2741
- };
2742
- }
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");
2743
3215
  }
2744
- return { point: worldToScreen(pointOnSide(rect, side2, alongPct), viewport), position: side2 };
2745
- };
2746
- const registered = handleId && handles ? handles.get(handleKey(node.id, handleId)) : void 0;
2747
- const pinnedSide = parseAnchorHandleId(handleId)?.side ?? registered?.position ?? parseSideHandleId(handleId);
2748
- const live = extras?.liveAlignNodes !== void 0 && (extras.liveAlignNodes.has(edge.source) || extras.liveAlignNodes.has(edge.target)) && edge.source !== edge.target;
2749
- if (pinnedSide && !live) {
2750
- if (registered) {
2751
- return {
2752
- point: worldToScreen(
2753
- { x: rect.x + registered.offset.x, y: rect.y + registered.offset.y },
2754
- viewport
2755
- ),
2756
- position: registered.position
2757
- };
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");
2758
3222
  }
2759
- return endAt(pinnedSide);
2760
3223
  }
2761
- const key = displayedSideKey(edge.id, role);
2762
- const side = resolveFacingSide(
2763
- rect,
2764
- nodeRect(peer, worldPositions),
2765
- extras?.displayedSides?.get(key)
2766
- );
2767
- extras?.displayedSides?.set(key, side);
2768
- return endAt(side);
2769
- }
2770
- function distanceToPolyline(point, points) {
2771
- let best = Number.POSITIVE_INFINITY;
2772
- for (let i = 0; i < points.length - 1; i++) {
2773
- const a = points[i];
2774
- const b = points[i + 1];
2775
- if (!a || !b) continue;
2776
- 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
+ }
2777
3234
  }
2778
- return best;
2779
- }
2780
- function distanceToSegment(p, a, b) {
2781
- const dx = b.x - a.x;
2782
- const dy = b.y - a.y;
2783
- const lenSq = dx * dx + dy * dy;
2784
- if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
2785
- let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
2786
- t = Math.max(0, Math.min(1, t));
2787
- 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();
2788
3244
  }
2789
3245
 
2790
3246
  // src/renderer/swimlane.ts
@@ -2817,7 +3273,7 @@ function drawSwimlanes(ctx, width, height, viewport, lanes, style) {
2817
3273
  }
2818
3274
 
2819
3275
  // src/hooks/use-rivet-runtime.ts
2820
- var CULL_MARGIN_PX = 240;
3276
+ var CULL_MARGIN_PX2 = 240;
2821
3277
  var ZOOM_STEP = 1.2;
2822
3278
  var WILL_CHANGE_IDLE_MS = 180;
2823
3279
  var GUIDE_COLOR = "rgba(236, 72, 153, 0.9)";
@@ -2902,6 +3358,31 @@ function useRivetRuntime(params) {
2902
3358
  let frame = 0;
2903
3359
  let dirty = true;
2904
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
+ };
2905
3386
  const anchorPlacement = (nodeId, anchorId, side) => {
2906
3387
  const record = store.anchors.get(handleKey(nodeId, anchorId));
2907
3388
  return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
@@ -2919,6 +3400,7 @@ function useRivetRuntime(params) {
2919
3400
  });
2920
3401
  const render = () => {
2921
3402
  zoom.step();
3403
+ if (store.presence.step(performance.now())) schedule();
2922
3404
  const viewport = store.getViewport();
2923
3405
  layerHint.apply(viewportToCss(viewport));
2924
3406
  bgCtx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
@@ -2926,6 +3408,7 @@ function useRivetRuntime(params) {
2926
3408
  drawDotGrid(bgCtx, size.width, size.height, viewport, { gap: gridGap });
2927
3409
  drawSwimlanes(bgCtx, size.width, size.height, viewport, swimlaneLanesRef.current);
2928
3410
  const worldPositions = store.getWorldPositions();
3411
+ positionPeerNodes(worldPositions);
2929
3412
  const edgeList = [...store.edges.values()];
2930
3413
  const pending = store.getPending();
2931
3414
  const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
@@ -2954,9 +3437,18 @@ function useRivetRuntime(params) {
2954
3437
  if (ends) drawEndpointBubbles(fgCtx, ends, size);
2955
3438
  }
2956
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
+ }
2957
3449
  if (edgeList.some((edge) => edge.animated ?? defaultEdgeOptions?.animated)) schedule();
2958
3450
  const rect = visibleWorldRect(viewport, size.width, size.height);
2959
- const margin = CULL_MARGIN_PX / viewport.zoom;
3451
+ const margin = CULL_MARGIN_PX2 / viewport.zoom;
2960
3452
  const view = {
2961
3453
  x: rect.x - margin,
2962
3454
  y: rect.y - margin,
@@ -3164,7 +3656,7 @@ function useRivetRuntime(params) {
3164
3656
  }
3165
3657
  pan.end(event);
3166
3658
  };
3167
- const onKeyDown = createKeyboardHandler(store);
3659
+ const onKeyDown = createKeyboardHandler(store, () => reconnectRef.current.snapGrid);
3168
3660
  let focusRetryFrame = 0;
3169
3661
  const unsubscribeFocus = store.subscribeFocus((id) => {
3170
3662
  if (focusRetryFrame) {
@@ -3640,7 +4132,7 @@ var labelStyle2 = {
3640
4132
  whiteSpace: "nowrap",
3641
4133
  pointerEvents: "none"
3642
4134
  };
3643
- function sameIds(a, b) {
4135
+ function sameIds2(a, b) {
3644
4136
  if (a.length !== b.length) return false;
3645
4137
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
3646
4138
  return true;
@@ -3666,7 +4158,7 @@ function EdgeLabelLayer() {
3666
4158
  return store.subscribeFrame(() => {
3667
4159
  const anchors = store.getEdgeLabelAnchors();
3668
4160
  const next = [...anchors.keys()];
3669
- setIds((prev) => sameIds(prev, next) ? prev : next);
4161
+ setIds((prev) => sameIds2(prev, next) ? prev : next);
3670
4162
  position();
3671
4163
  });
3672
4164
  }, [store, position]);
@@ -3702,6 +4194,8 @@ function startGroupDrag(params) {
3702
4194
  let started = false;
3703
4195
  let starts = /* @__PURE__ */ new Map();
3704
4196
  let affected = /* @__PURE__ */ new Set();
4197
+ let initiator;
4198
+ let anchor;
3705
4199
  const repositionAffected = () => {
3706
4200
  for (const affectedId of affected) {
3707
4201
  const affectedEl = store.getNodeElement(affectedId);
@@ -3712,8 +4206,9 @@ function startGroupDrag(params) {
3712
4206
  onFrame?.();
3713
4207
  };
3714
4208
  const beginDrag = () => {
4209
+ const movers = store.getMovers(store.getSelectedNodes()).filter((moverId) => store.locks.allows(moverId, "drag"));
4210
+ if (movers.length === 0) return false;
3715
4211
  started = true;
3716
- const movers = store.getMovers(store.getSelectedNodes());
3717
4212
  starts = /* @__PURE__ */ new Map();
3718
4213
  for (const moverId of movers) {
3719
4214
  const moverNode = store.nodes.get(moverId);
@@ -3725,17 +4220,42 @@ function startGroupDrag(params) {
3725
4220
  for (const descendant of store.getDescendantIds(moverId)) affected.add(descendant);
3726
4221
  }
3727
4222
  store.setNodeDragging(true);
4223
+ initiator = params.nodeId ?? movers[0];
4224
+ anchor = movers[0];
4225
+ if (initiator) store.beginNodeGesture(initiator, movers);
3728
4226
  el.setPointerCapture(pointerId);
3729
4227
  el.style.cursor = "grabbing";
3730
4228
  window.getSelection()?.removeAllRanges();
3731
4229
  document.body.style.userSelect = "none";
3732
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);
3733
4244
  };
4245
+ const yielded = () => anchor !== void 0 && !store.isNodeHeld(anchor);
3734
4246
  const onMove = (moveEvent) => {
3735
4247
  if (!started) {
3736
4248
  const moved = Math.hypot(moveEvent.clientX - originX, moveEvent.clientY - originY);
3737
4249
  if (moved < DRAG_THRESHOLD) return;
3738
- 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;
3739
4259
  }
3740
4260
  const dx = (moveEvent.clientX - originX) / zoom;
3741
4261
  const dy = (moveEvent.clientY - originY) / zoom;
@@ -3770,6 +4290,10 @@ function startGroupDrag(params) {
3770
4290
  window.removeEventListener("pointermove", onMove);
3771
4291
  window.removeEventListener("pointerup", onUp);
3772
4292
  if (!started) return;
4293
+ if (yielded()) {
4294
+ teardown();
4295
+ return;
4296
+ }
3773
4297
  el.style.cursor = "grab";
3774
4298
  document.body.style.userSelect = "";
3775
4299
  document.body.style.webkitUserSelect = "";
@@ -3796,6 +4320,7 @@ function startGroupDrag(params) {
3796
4320
  }
3797
4321
  store.moveNode(moverId, position, true);
3798
4322
  }
4323
+ if (initiator) store.endNodeGesture(initiator, [...starts.keys()]);
3799
4324
  repositionAffected();
3800
4325
  };
3801
4326
  window.addEventListener("pointermove", onMove);
@@ -3923,7 +4448,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3923
4448
  return node2?.ariaLabel ?? `node ${id}`;
3924
4449
  };
3925
4450
  const grab = () => {
3926
- 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
+ }
3927
4456
  const origins = /* @__PURE__ */ new Map();
3928
4457
  for (const moverId of movers) {
3929
4458
  const mover = store.nodes.get(moverId);
@@ -3931,6 +4460,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3931
4460
  }
3932
4461
  grabOrigins.current = origins;
3933
4462
  setGrabbed(true);
4463
+ store.beginNodeGesture(id, movers);
3934
4464
  announce(`Grabbed ${nodeName()}. Use the arrow keys to move, Enter to drop, Escape to cancel.`);
3935
4465
  };
3936
4466
  const releaseGrab = (revert) => {
@@ -3940,9 +4470,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3940
4470
  if (!origins) return;
3941
4471
  if (revert) {
3942
4472
  for (const [moverId, origin] of origins) store.moveNode(moverId, origin, true);
4473
+ store.endNodeGesture(id, [...origins.keys()]);
3943
4474
  announce("Move cancelled.");
3944
4475
  return;
3945
4476
  }
4477
+ store.endNodeGesture(id, [...origins.keys()]);
3946
4478
  const node2 = store.nodes.get(id);
3947
4479
  if (node2) {
3948
4480
  const { x, y } = node2.position;
@@ -3955,6 +4487,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3955
4487
  );
3956
4488
  const getSnapshot = useCallback(() => store.getNodeVersion(id), [store, id]);
3957
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
+ );
3958
4495
  const nodeContext = useMemo(() => ({ nodeId: id, wrapperRef: ref }), [id]);
3959
4496
  useEffect(() => {
3960
4497
  const el = ref.current;
@@ -3987,6 +4524,13 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3987
4524
  if (event.target !== event.currentTarget) return;
3988
4525
  const node2 = store.nodes.get(id);
3989
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;
3990
4534
  if (event.key === "Enter" || event.key === " ") {
3991
4535
  event.preventDefault();
3992
4536
  if (grabOrigins.current) {
@@ -4039,8 +4583,8 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4039
4583
  const node2 = store.nodes.get(id);
4040
4584
  if (!el || !node2) return;
4041
4585
  const target = event.target instanceof Element ? event.target : null;
4042
- const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect));
4043
- 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");
4044
4588
  const additive = keyHeld(event, multiSelectionKeys);
4045
4589
  if (additive) {
4046
4590
  if (!noSelect) store.selectNode(id, true);
@@ -4050,6 +4594,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4050
4594
  if (noDrag) return;
4051
4595
  startGroupDrag({
4052
4596
  store,
4597
+ nodeId: id,
4053
4598
  el,
4054
4599
  event,
4055
4600
  snapGrid,
@@ -4067,6 +4612,9 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4067
4612
  const Component = nodeTypes[key] ?? BUILTIN_NODE_TYPES[key] ?? DefaultNode;
4068
4613
  const world = store.getNodeWorldPosition(id);
4069
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");
4070
4618
  return (
4071
4619
  // biome-ignore lint/a11y/useSemanticElements: a node wraps arbitrary (often interactive) content, so it can't be a native <button>
4072
4620
  /* @__PURE__ */ jsx(
@@ -4075,12 +4623,14 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4075
4623
  ref,
4076
4624
  "data-rivet-node": id,
4077
4625
  "data-rivet-grabbed": grabbed ? "" : void 0,
4078
- tabIndex: node.selectable === false ? -1 : 0,
4626
+ "data-rivet-locked": locked ? lockHolder ?? "" : void 0,
4627
+ tabIndex: node.selectable === false || lockedForSelect ? -1 : 0,
4079
4628
  role: "button",
4080
4629
  "aria-roledescription": "graph node",
4081
4630
  "aria-pressed": Boolean(node.selected),
4082
4631
  "aria-label": node.ariaLabel,
4083
- "aria-describedby": node.selectable === false ? void 0 : nodeDescriptionId,
4632
+ "aria-disabled": lockedForSelect || void 0,
4633
+ "aria-describedby": node.selectable === false || lockedForSelect ? void 0 : nodeDescriptionId,
4084
4634
  onPointerDown,
4085
4635
  onPointerEnter,
4086
4636
  onPointerLeave,
@@ -4089,6 +4639,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
4089
4639
  onBlur,
4090
4640
  style: {
4091
4641
  ...wrapperStyle,
4642
+ ...lockedForDrag ? { cursor: "not-allowed" } : {},
4092
4643
  transform: `translate(${world.x}px, ${world.y}px)`,
4093
4644
  zIndex: depth,
4094
4645
  // Explicit dimensions from NodeResizer override the intrinsic size.
@@ -4466,6 +5017,12 @@ function Rivet({
4466
5017
  snapGrid,
4467
5018
  alignmentGuides = false,
4468
5019
  anchorOptions,
5020
+ peers,
5021
+ onLocalPresence,
5022
+ presenceOptions,
5023
+ lockedNodes,
5024
+ canInteractWithLocked,
5025
+ onNodeLockConflict,
4469
5026
  panOnDrag = true,
4470
5027
  selectionOnDrag = false,
4471
5028
  selectionKeyCode = "Shift",
@@ -4493,6 +5050,8 @@ function Rivet({
4493
5050
  onReconnect,
4494
5051
  onReconnectStart,
4495
5052
  onReconnectEnd,
5053
+ onNodeDragStart,
5054
+ onNodeDragEnd,
4496
5055
  onSelectionChange,
4497
5056
  onFocusChange,
4498
5057
  className,
@@ -4515,6 +5074,7 @@ function Rivet({
4515
5074
  snapGrid,
4516
5075
  swimlaneMargin,
4517
5076
  anchorOptions,
5077
+ presenceOptions,
4518
5078
  panOnDrag,
4519
5079
  selectionKeyCode,
4520
5080
  multiSelectionKeyCode,
@@ -4555,14 +5115,28 @@ function Rivet({
4555
5115
  const hasEdgeChange = Boolean(onEdgesChange);
4556
5116
  useEffect(() => {
4557
5117
  store.setChangeHandlers({
4558
- nodes: hasNodeChange ? (changes) => onNodesChangeRef.current?.(changes) : void 0,
4559
- 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
4560
5120
  });
4561
5121
  return () => store.setChangeHandlers({});
4562
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]);
4563
5137
  const isControlled = controlledNodes !== void 0 || controlledEdges !== void 0;
4564
5138
  useEffect(() => {
4565
- if (!isControlled || store.isNodeDragging() || store.isNodeResizing()) return;
5139
+ if (!isControlled) return;
4566
5140
  store.reconcile(
4567
5141
  controlledNodes ?? [...store.nodes.values()],
4568
5142
  controlledEdges ?? [...store.edges.values()]
@@ -4615,6 +5189,23 @@ function Rivet({
4615
5189
  const backgroundCanvasRef = useRef(null);
4616
5190
  const edgeCanvasRef = useRef(null);
4617
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 });
4618
5209
  const { visibleIds, controls, getViewportElements } = useRivetRuntime({
4619
5210
  store,
4620
5211
  paneRef,
@@ -4626,6 +5217,7 @@ function Rivet({
4626
5217
  edgeTypes: cfg.edgeTypes,
4627
5218
  defaultEdgeOptions: cfg.defaultEdgeOptions,
4628
5219
  anchorOptions: cfg.anchorOptions,
5220
+ presenceOptions: cfg.presenceOptions,
4629
5221
  edgeRenderer: renderer,
4630
5222
  edgesReconnectable,
4631
5223
  isValidConnection: cfg.isValidConnection,
@@ -4641,7 +5233,8 @@ function Rivet({
4641
5233
  zoomSpeed,
4642
5234
  minZoom,
4643
5235
  maxZoom,
4644
- gridGap
5236
+ gridGap,
5237
+ snapGrid: cfg.snapGrid
4645
5238
  });
4646
5239
  const contextValue = useMemo(
4647
5240
  () => ({
@@ -4796,6 +5389,7 @@ function buildInstance(store, controls, getViewportElements) {
4796
5389
  for (const id of edges ?? []) store.removeEdge(id);
4797
5390
  for (const id of nodes ?? []) store.removeNode(id);
4798
5391
  },
5392
+ applyRemote: (changes) => store.applyRemote(changes),
4799
5393
  registerAnchor: (nodeId, anchorId, element, options) => {
4800
5394
  store.registerAnchor(nodeId, anchorId, element, options);
4801
5395
  return {
@@ -4805,6 +5399,10 @@ function buildInstance(store, controls, getViewportElements) {
4805
5399
  },
4806
5400
  unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
4807
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),
4808
5406
  copy: (ids) => {
4809
5407
  const targetIds = ids ?? store.getSelectedNodes();
4810
5408
  if (targetIds.length === 0) return;
@@ -4844,6 +5442,6 @@ function useRivet() {
4844
5442
  // src/index.ts
4845
5443
  var VERSION = "0.0.0";
4846
5444
 
4847
- 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, screenToWorld, serializeGraph, snapToGrid, useRivet, viewportToCss, visibleWorldRect, worldToScreen, zoomAt };
5445
+ 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 };
4848
5446
  //# sourceMappingURL=index.js.map
4849
5447
  //# sourceMappingURL=index.js.map