@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/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 +498 -252
- package/dist/index.js +1019 -421
- 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
|
|
|
@@ -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
|
-
|
|
809
|
-
|
|
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 !==
|
|
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
|
-
|
|
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
|
|
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) ??
|
|
1054
|
-
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");
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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/
|
|
2622
|
-
var
|
|
2623
|
-
var
|
|
2624
|
-
var
|
|
2625
|
-
var
|
|
2626
|
-
var
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
|
2680
|
-
const
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
|
2695
|
-
|
|
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
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
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
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
const
|
|
2730
|
-
const
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
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
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
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
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|