@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/README.md +4 -0
- package/dist/anchor/index.d.ts +2 -2
- package/dist/chunk-RG5ALD3X.js +306 -0
- package/dist/chunk-RG5ALD3X.js.map +1 -0
- package/dist/index.d.ts +481 -254
- package/dist/index.js +979 -410
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +19 -0
- package/dist/presence/index.js +16 -0
- package/dist/presence/index.js.map +1 -0
- package/dist/{registry-Dkk4ZKt-.d.ts → registry-t5ldbviG.d.ts} +1 -1
- package/dist/svg/index.d.ts +2 -2
- package/dist/swimlane/index.d.ts +2 -2
- package/dist/{types-B8AAJ60T.d.ts → types-CQqnm4yW.d.ts} +55 -1
- package/dist/use-node-lock-Csg9pUeC.d.ts +323 -0
- package/package.json +5 -1
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
|
-
|
|
825
|
-
|
|
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 !==
|
|
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
|
-
|
|
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
|
|
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) ??
|
|
1070
|
-
const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ??
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
2639
|
-
var
|
|
2640
|
-
var
|
|
2641
|
-
var
|
|
2642
|
-
var
|
|
2643
|
-
var
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
|
2712
|
-
const
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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
|
|
2725
|
-
|
|
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
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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
|
|
2743
|
-
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
const
|
|
2757
|
-
const
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
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
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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-
|
|
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
|
|
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;
|