@kolosal-ai/rivet 0.1.1 → 0.3.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-A52HZWLH.js +314 -0
- package/dist/chunk-A52HZWLH.js.map +1 -0
- package/dist/index.d.ts +503 -255
- package/dist/index.js +1330 -680
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +108 -0
- package/dist/presence/index.js +38 -0
- package/dist/presence/index.js.map +1 -0
- package/dist/{registry-Dkk4ZKt-.d.ts → registry-CCBgd7FN.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-C9Vn1rzy.d.ts} +55 -1
- package/dist/use-node-lock-DH0dB1yA.d.ts +327 -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-A52HZWLH.js';
|
|
3
|
+
export { LOCK_DEFAULT_REFUSED, useNodeLock, useNodeLockAllows } from './chunk-A52HZWLH.js';
|
|
2
4
|
import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
3
5
|
export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
4
|
-
import {
|
|
6
|
+
import { clamp, clampNodeToLanes, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
|
|
5
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
8
|
import { memo, useRef, useState, useCallback, useSyncExternalStore, useMemo, useEffect, Fragment as Fragment$1, useId, useLayoutEffect } from 'react';
|
|
7
9
|
|
|
@@ -356,6 +358,35 @@ function parseEdgeEndpoint(nodeId, handleId, handles) {
|
|
|
356
358
|
return side ? { nodeId, side } : { nodeId };
|
|
357
359
|
}
|
|
358
360
|
|
|
361
|
+
// src/gesture-guard.ts
|
|
362
|
+
function keepLocalGeometry(incoming, local) {
|
|
363
|
+
return {
|
|
364
|
+
...incoming,
|
|
365
|
+
position: local.position,
|
|
366
|
+
width: local.width,
|
|
367
|
+
height: local.height,
|
|
368
|
+
size: local.size
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function guardNodeChanges(changes, isHeld, getLocal) {
|
|
372
|
+
if (!changes.some((change) => change.type !== "add" && isHeld(change.id))) return changes;
|
|
373
|
+
const guarded = [];
|
|
374
|
+
for (const change of changes) {
|
|
375
|
+
if (change.type === "add" || !isHeld(change.id)) {
|
|
376
|
+
guarded.push(change);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (change.type === "position" || change.type === "dimensions") continue;
|
|
380
|
+
if (change.type === "replace") {
|
|
381
|
+
const local = getLocal(change.id);
|
|
382
|
+
guarded.push(local ? { ...change, item: keepLocalGeometry(change.item, local) } : change);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
guarded.push(change);
|
|
386
|
+
}
|
|
387
|
+
return guarded;
|
|
388
|
+
}
|
|
389
|
+
|
|
359
390
|
// src/graph.ts
|
|
360
391
|
function clampChildToParent(position, childSize, parentSize) {
|
|
361
392
|
const maxX = Math.max(0, parentSize.width - childSize.width);
|
|
@@ -522,6 +553,26 @@ var HistoryManager = class {
|
|
|
522
553
|
this.last = this.snapshot();
|
|
523
554
|
});
|
|
524
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Fold an external change into every stored snapshot.
|
|
558
|
+
*
|
|
559
|
+
* Snapshots are structural, so a restore rewrites the whole graph — which in a
|
|
560
|
+
* shared document means undoing past a peer's edit would silently erase it.
|
|
561
|
+
* {@link rebase} can't help: it only refreshes the rolling baseline, while the
|
|
562
|
+
* stale states are the ones already on the stacks. Replaying the incoming
|
|
563
|
+
* change over each of them keeps every stored step consistent with work this
|
|
564
|
+
* client doesn't own.
|
|
565
|
+
*
|
|
566
|
+
* The trade-off is that an undo touching a node a peer has since changed no
|
|
567
|
+
* longer moves it — the peer's newer value survives instead. That's the
|
|
568
|
+
* intended precedence: a local undo may drop its own effect, never someone
|
|
569
|
+
* else's edit.
|
|
570
|
+
*/
|
|
571
|
+
rewrite(transform) {
|
|
572
|
+
this.undoStack = this.undoStack.map(transform);
|
|
573
|
+
this.redoStack = this.redoStack.map(transform);
|
|
574
|
+
this.last = transform(this.last);
|
|
575
|
+
}
|
|
525
576
|
flush() {
|
|
526
577
|
this.scheduled = false;
|
|
527
578
|
this.undoStack.push(this.last);
|
|
@@ -575,177 +626,596 @@ var HistoryManager = class {
|
|
|
575
626
|
}
|
|
576
627
|
};
|
|
577
628
|
|
|
578
|
-
// src/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
anchors = /* @__PURE__ */ new Map();
|
|
584
|
-
viewport;
|
|
585
|
-
viewportClamp = null;
|
|
586
|
-
pending = null;
|
|
587
|
-
nodeVersions = /* @__PURE__ */ new Map();
|
|
588
|
-
nodeListeners = /* @__PURE__ */ new Map();
|
|
589
|
-
anchorVersions = /* @__PURE__ */ new Map();
|
|
590
|
-
anchorListeners = /* @__PURE__ */ new Map();
|
|
591
|
-
edgesVersion = 0;
|
|
592
|
-
edgesListeners = /* @__PURE__ */ new Set();
|
|
593
|
-
nodeElements = /* @__PURE__ */ new Map();
|
|
594
|
-
viewportListeners = /* @__PURE__ */ new Set();
|
|
595
|
-
frameListeners = /* @__PURE__ */ new Set();
|
|
596
|
-
selectionListeners = /* @__PURE__ */ new Set();
|
|
597
|
-
selectedNodeIds = /* @__PURE__ */ new Set();
|
|
598
|
-
selectionBoxActive = false;
|
|
599
|
-
selectionBoxVersion = 0;
|
|
600
|
-
selectionBoxListeners = /* @__PURE__ */ new Set();
|
|
601
|
-
hoveredNodeId = null;
|
|
602
|
-
focusedNodeId = null;
|
|
603
|
-
focusListeners = /* @__PURE__ */ new Set();
|
|
604
|
-
selectedEdgeId = null;
|
|
605
|
-
nodeDragging = false;
|
|
606
|
-
nodeResizing = false;
|
|
607
|
-
edgeAlignment = "manual";
|
|
608
|
-
draggingNodeIds = /* @__PURE__ */ new Set();
|
|
609
|
-
displayedSides = /* @__PURE__ */ new Map();
|
|
610
|
-
edgeLabelAnchors = /* @__PURE__ */ new Map();
|
|
611
|
-
alignmentGuides = [];
|
|
612
|
-
nodeChangeHandler = null;
|
|
613
|
-
edgeChangeHandler = null;
|
|
614
|
-
reconciling = false;
|
|
615
|
-
history = new HistoryManager(() => this.historySnapshot());
|
|
616
|
-
lastSelectionKey;
|
|
617
|
-
worldCache = null;
|
|
618
|
-
renderRequester = () => {
|
|
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
|
|
619
634
|
};
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
});
|
|
628
718
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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);
|
|
632
734
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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];
|
|
644
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];
|
|
645
761
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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];
|
|
653
773
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
|
658
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);
|
|
659
811
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
});
|
|
701
895
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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 };
|
|
705
924
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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;
|
|
717
940
|
}
|
|
718
|
-
this.selectedNodeIds.clear();
|
|
719
|
-
for (const id of target) if (this.nodes.has(id)) this.selectedNodeIds.add(id);
|
|
720
|
-
this.emitNodeChanges(changes);
|
|
721
941
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
+
};
|
|
731
971
|
}
|
|
732
972
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
};
|
|
739
987
|
}
|
|
988
|
+
return endAt(pinnedSide);
|
|
740
989
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
|
|
1020
|
+
// src/store.ts
|
|
1021
|
+
var RivetGraphStore = class {
|
|
1022
|
+
nodes = /* @__PURE__ */ new Map();
|
|
1023
|
+
edges = /* @__PURE__ */ new Map();
|
|
1024
|
+
handles = /* @__PURE__ */ new Map();
|
|
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({
|
|
1029
|
+
requestRender: () => this.requestRender(),
|
|
1030
|
+
requestCursorRender: () => this.requestCursorRender()
|
|
1031
|
+
});
|
|
1032
|
+
locks = createLockRegistry(() => this.requestRender());
|
|
1033
|
+
viewport;
|
|
1034
|
+
viewportClamp = null;
|
|
1035
|
+
pending = null;
|
|
1036
|
+
nodeVersions = /* @__PURE__ */ new Map();
|
|
1037
|
+
nodeListeners = /* @__PURE__ */ new Map();
|
|
1038
|
+
anchorVersions = /* @__PURE__ */ new Map();
|
|
1039
|
+
anchorListeners = /* @__PURE__ */ new Map();
|
|
1040
|
+
edgesVersion = 0;
|
|
1041
|
+
edgesListeners = /* @__PURE__ */ new Set();
|
|
1042
|
+
nodeElements = /* @__PURE__ */ new Map();
|
|
1043
|
+
viewportListeners = /* @__PURE__ */ new Set();
|
|
1044
|
+
frameListeners = /* @__PURE__ */ new Set();
|
|
1045
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
1046
|
+
selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1047
|
+
selectionBoxActive = false;
|
|
1048
|
+
selectionBoxVersion = 0;
|
|
1049
|
+
selectionBoxListeners = /* @__PURE__ */ new Set();
|
|
1050
|
+
hoveredNodeId = null;
|
|
1051
|
+
focusedNodeId = null;
|
|
1052
|
+
focusListeners = /* @__PURE__ */ new Set();
|
|
1053
|
+
selectedEdgeId = null;
|
|
1054
|
+
nodeDragging = false;
|
|
1055
|
+
nodeResizing = false;
|
|
1056
|
+
edgeAlignment = "manual";
|
|
1057
|
+
draggingNodeIds = /* @__PURE__ */ new Set();
|
|
1058
|
+
heldNodeIds = /* @__PURE__ */ new Set();
|
|
1059
|
+
heldListeners = /* @__PURE__ */ new Set();
|
|
1060
|
+
gestureStartHandler = null;
|
|
1061
|
+
gestureEndHandler = null;
|
|
1062
|
+
lockConflictHandler = null;
|
|
1063
|
+
displayedSides = /* @__PURE__ */ new Map();
|
|
1064
|
+
edgeLabelAnchors = /* @__PURE__ */ new Map();
|
|
1065
|
+
alignmentGuides = [];
|
|
1066
|
+
nodeChangeHandler = null;
|
|
1067
|
+
edgeChangeHandler = null;
|
|
1068
|
+
reconciling = false;
|
|
1069
|
+
history = new HistoryManager(() => this.historySnapshot());
|
|
1070
|
+
lastSelectionKey;
|
|
1071
|
+
worldCache = null;
|
|
1072
|
+
/** {@link worldCache} with peers' live gesture boxes folded in. */
|
|
1073
|
+
peerWorldCache = null;
|
|
1074
|
+
/** Presence transform version {@link peerWorldCache} was built from. */
|
|
1075
|
+
peerWorldVersion = -1;
|
|
1076
|
+
renderRequester = () => {
|
|
1077
|
+
};
|
|
1078
|
+
/**
|
|
1079
|
+
* Null until a runtime claims it, and then `requestCursorRender` narrows to
|
|
1080
|
+
* the cursor layer. Falling back to a full frame is the safe default: a
|
|
1081
|
+
* consumer without the layer mounted still gets its paint.
|
|
1082
|
+
*/
|
|
1083
|
+
cursorRenderRequester = null;
|
|
1084
|
+
constructor(init) {
|
|
1085
|
+
this.viewport = init.viewport;
|
|
1086
|
+
for (const node of init.nodes) this.writeNode(node.id, node);
|
|
1087
|
+
for (const edge of init.edges) this.edges.set(edge.id, edge);
|
|
1088
|
+
for (const node of init.nodes) if (node.selected) this.selectedNodeIds.add(node.id);
|
|
1089
|
+
this.selectedEdgeId = init.edges.find((edge) => edge.selected)?.id ?? null;
|
|
1090
|
+
this.lastSelectionKey = this.selectionKey();
|
|
1091
|
+
this.history.clear();
|
|
1092
|
+
}
|
|
1093
|
+
// --- internal bookkeeping ------------------------------------------------
|
|
1094
|
+
invalidateWorld() {
|
|
1095
|
+
this.worldCache = null;
|
|
1096
|
+
this.peerWorldCache = null;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* The single node-map write path. Invalidates the world-position cache exactly
|
|
1100
|
+
* when a layout-affecting field changed (the node's position or its parent
|
|
1101
|
+
* chain) or the node is new — so size/lane/selection/hover writes skip the
|
|
1102
|
+
* O(n) rebuild, and no mutator can forget to invalidate.
|
|
1103
|
+
*/
|
|
1104
|
+
writeNode(id, next) {
|
|
1105
|
+
const prev = this.nodes.get(id);
|
|
1106
|
+
this.nodes.set(id, next);
|
|
1107
|
+
if (!prev || prev.parentId !== next.parentId || prev.position.x !== next.position.x || prev.position.y !== next.position.y) {
|
|
1108
|
+
this.invalidateWorld();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/** The single node-map delete path. Always invalidates — a removal shifts descendants. */
|
|
1112
|
+
deleteNode(id) {
|
|
1113
|
+
this.nodes.delete(id);
|
|
1114
|
+
this.invalidateWorld();
|
|
1115
|
+
}
|
|
1116
|
+
selectionKey() {
|
|
1117
|
+
return `${[...this.selectedNodeIds].sort().join(",")}|${this.selectedEdgeId ?? ""}`;
|
|
1118
|
+
}
|
|
1119
|
+
buildSelection() {
|
|
1120
|
+
return {
|
|
1121
|
+
nodes: [...this.selectedNodeIds].map((id) => this.nodes.get(id)).filter((n) => n !== void 0),
|
|
1122
|
+
edges: this.selectedEdgeId ? [this.edges.get(this.selectedEdgeId)].filter((e) => e !== void 0) : []
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
notifySelection() {
|
|
1126
|
+
const key = this.selectionKey();
|
|
1127
|
+
if (key === this.lastSelectionKey) return;
|
|
1128
|
+
this.lastSelectionKey = key;
|
|
1129
|
+
if (this.selectionBoxActive) this.bumpSelectionBox();
|
|
1130
|
+
if (this.selectionListeners.size === 0) return;
|
|
1131
|
+
const selection = this.buildSelection();
|
|
1132
|
+
for (const listener of this.selectionListeners) listener(selection);
|
|
1133
|
+
}
|
|
1134
|
+
notifyHeld() {
|
|
1135
|
+
this.peerWorldCache = null;
|
|
1136
|
+
this.requestRender();
|
|
1137
|
+
for (const listener of this.heldListeners) listener();
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The one emission policy. Unless a `reconcile` is applying incoming props
|
|
1141
|
+
* (which must never echo them back out), record the batch to history — when
|
|
1142
|
+
* `record` is set — then hand it to the controlled consumer. Recording happens
|
|
1143
|
+
* before the handler so history works in uncontrolled mode too.
|
|
1144
|
+
*/
|
|
1145
|
+
emit(changes, handler, record, origin = "local") {
|
|
1146
|
+
if (this.reconciling || changes.length === 0) return;
|
|
1147
|
+
if (record) this.history.record(hasRecordableChange(changes));
|
|
1148
|
+
else if (hasRecordableChange(changes)) this.history.rebase();
|
|
1149
|
+
handler?.(changes, { origin });
|
|
1150
|
+
}
|
|
1151
|
+
emitNodeChanges(changes, record = true) {
|
|
1152
|
+
this.emit(changes, this.nodeChangeHandler, record);
|
|
1153
|
+
}
|
|
1154
|
+
emitEdgeChanges(changes, record = true) {
|
|
1155
|
+
this.emit(changes, this.edgeChangeHandler, record);
|
|
1156
|
+
}
|
|
1157
|
+
bumpNode(id) {
|
|
1158
|
+
this.nodeVersions.set(id, (this.nodeVersions.get(id) ?? 0) + 1);
|
|
1159
|
+
const listeners = this.nodeListeners.get(id);
|
|
1160
|
+
if (listeners) for (const listener of listeners) listener();
|
|
1161
|
+
if (this.selectionBoxActive && this.selectedNodeIds.has(id)) this.bumpSelectionBox();
|
|
1162
|
+
}
|
|
1163
|
+
bumpSelectionBox() {
|
|
1164
|
+
this.selectionBoxVersion += 1;
|
|
1165
|
+
for (const listener of this.selectionBoxListeners) listener();
|
|
1166
|
+
}
|
|
1167
|
+
bumpAnchors(id) {
|
|
1168
|
+
this.anchorVersions.set(id, (this.anchorVersions.get(id) ?? 0) + 1);
|
|
1169
|
+
const listeners = this.anchorListeners.get(id);
|
|
1170
|
+
if (listeners) for (const listener of listeners) listener();
|
|
1171
|
+
}
|
|
1172
|
+
bumpEdges() {
|
|
1173
|
+
this.edgesVersion += 1;
|
|
1174
|
+
for (const listener of this.edgesListeners) listener();
|
|
1175
|
+
}
|
|
1176
|
+
// Node selection is React state (nodes are DOM), so bump only what flips.
|
|
1177
|
+
// `target` is the exact set that should end up selected.
|
|
1178
|
+
applyNodeSelection(target) {
|
|
1179
|
+
const changes = [];
|
|
1180
|
+
for (const [nodeId, node] of this.nodes) {
|
|
1181
|
+
const selected = target.has(nodeId);
|
|
1182
|
+
if (Boolean(node.selected) !== selected) {
|
|
1183
|
+
this.writeNode(nodeId, { ...node, selected });
|
|
1184
|
+
this.bumpNode(nodeId);
|
|
1185
|
+
changes.push({ type: "select", id: nodeId, selected });
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
this.selectedNodeIds.clear();
|
|
1189
|
+
for (const id of target) if (this.nodes.has(id)) this.selectedNodeIds.add(id);
|
|
1190
|
+
this.emitNodeChanges(changes);
|
|
1191
|
+
}
|
|
1192
|
+
setNodeHover(id) {
|
|
1193
|
+
if (this.hoveredNodeId === id) return;
|
|
1194
|
+
const prev = this.hoveredNodeId;
|
|
1195
|
+
this.hoveredNodeId = id;
|
|
1196
|
+
if (prev !== null) {
|
|
1197
|
+
const node = this.nodes.get(prev);
|
|
1198
|
+
if (node) {
|
|
1199
|
+
this.writeNode(prev, { ...node, hovered: false });
|
|
1200
|
+
this.bumpNode(prev);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (id !== null) {
|
|
1204
|
+
const node = this.nodes.get(id);
|
|
1205
|
+
if (node) {
|
|
1206
|
+
this.writeNode(id, { ...node, hovered: true });
|
|
1207
|
+
this.bumpNode(id);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Edge selection is canvas-only, so it just invalidates the frame.
|
|
1212
|
+
setEdgeSelection(id) {
|
|
1213
|
+
if (this.selectedEdgeId === id) return;
|
|
1214
|
+
const changes = [];
|
|
1215
|
+
if (this.selectedEdgeId) {
|
|
1216
|
+
const prev = this.edges.get(this.selectedEdgeId);
|
|
1217
|
+
if (prev) {
|
|
1218
|
+
this.edges.set(this.selectedEdgeId, { ...prev, selected: false });
|
|
749
1219
|
changes.push({ type: "select", id: this.selectedEdgeId, selected: false });
|
|
750
1220
|
}
|
|
751
1221
|
}
|
|
@@ -821,10 +1291,11 @@ var RivetGraphStore = class {
|
|
|
821
1291
|
this.writeNode(node.id, node);
|
|
822
1292
|
continue;
|
|
823
1293
|
}
|
|
824
|
-
|
|
825
|
-
|
|
1294
|
+
const settled = this.heldNodeIds.has(node.id) ? keepLocalGeometry(node, existing) : node;
|
|
1295
|
+
if (nodeControlledEqual(existing, settled)) continue;
|
|
1296
|
+
this.writeNode(node.id, existing.hovered ? { ...settled, hovered: true } : settled);
|
|
826
1297
|
this.bumpNode(node.id);
|
|
827
|
-
if (existing.position.x !==
|
|
1298
|
+
if (existing.position.x !== settled.position.x || existing.position.y !== settled.position.y) {
|
|
828
1299
|
moved.push(node.id);
|
|
829
1300
|
}
|
|
830
1301
|
}
|
|
@@ -917,13 +1388,54 @@ var RivetGraphStore = class {
|
|
|
917
1388
|
this.worldCache = /* @__PURE__ */ new Map();
|
|
918
1389
|
for (const id of this.nodes.keys()) this.worldCache.set(id, worldPosition(this.nodes, id));
|
|
919
1390
|
}
|
|
920
|
-
|
|
1391
|
+
const version = this.presence.getTransformVersion();
|
|
1392
|
+
if (this.presence.getNodeTransforms().size === 0) return this.worldCache;
|
|
1393
|
+
if (this.peerWorldCache && this.peerWorldVersion === version) return this.peerWorldCache;
|
|
1394
|
+
this.peerWorldVersion = version;
|
|
1395
|
+
this.peerWorldCache = this.withPeerGestures(this.worldCache);
|
|
1396
|
+
return this.peerWorldCache;
|
|
921
1397
|
};
|
|
1398
|
+
/**
|
|
1399
|
+
* Fold peers' live gesture boxes into world positions.
|
|
1400
|
+
*
|
|
1401
|
+
* A node somebody else is dragging is drawn, hit-tested and wired at the box
|
|
1402
|
+
* they last sent — the node moves, rather than a ghost of it appearing
|
|
1403
|
+
* elsewhere. The graph underneath is untouched: nothing here is written back,
|
|
1404
|
+
* so their drag never enters the document, the change stream, or history, and
|
|
1405
|
+
* the node lands for real when their commit arrives.
|
|
1406
|
+
*
|
|
1407
|
+
* Descendants ride along by the same delta, since their own positions are
|
|
1408
|
+
* relative to a parent this map has just moved.
|
|
1409
|
+
*/
|
|
1410
|
+
withPeerGestures(base) {
|
|
1411
|
+
const merged = new Map(base);
|
|
1412
|
+
for (const [id, rect] of this.presence.getNodeTransforms()) {
|
|
1413
|
+
if (this.heldNodeIds.has(id)) continue;
|
|
1414
|
+
const origin = base.get(id);
|
|
1415
|
+
if (!origin) continue;
|
|
1416
|
+
const dx = rect.x - origin.x;
|
|
1417
|
+
const dy = rect.y - origin.y;
|
|
1418
|
+
if (dx === 0 && dy === 0) continue;
|
|
1419
|
+
merged.set(id, { x: rect.x, y: rect.y });
|
|
1420
|
+
for (const childId of this.getDescendantIds(id)) {
|
|
1421
|
+
const child = merged.get(childId);
|
|
1422
|
+
if (child) merged.set(childId, { x: child.x + dx, y: child.y + dy });
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return merged;
|
|
1426
|
+
}
|
|
922
1427
|
getNodeWorldPosition = (id) => this.getWorldPositions().get(id) ?? { x: 0, y: 0 };
|
|
923
1428
|
getNodeRect = (id) => {
|
|
924
1429
|
const origin = this.getNodeWorldPosition(id);
|
|
925
|
-
const
|
|
926
|
-
return { x: origin.x, y: origin.y, width:
|
|
1430
|
+
const live = this.heldNodeIds.has(id) ? void 0 : this.presence.getNodeTransforms().get(id);
|
|
1431
|
+
if (live) return { x: origin.x, y: origin.y, width: live.width, height: live.height };
|
|
1432
|
+
const node = this.nodes.get(id);
|
|
1433
|
+
return {
|
|
1434
|
+
x: origin.x,
|
|
1435
|
+
y: origin.y,
|
|
1436
|
+
width: node?.width ?? node?.size?.width ?? DEFAULT_NODE_SIZE.width,
|
|
1437
|
+
height: node?.height ?? node?.size?.height ?? DEFAULT_NODE_SIZE.height
|
|
1438
|
+
};
|
|
927
1439
|
};
|
|
928
1440
|
getNodeDepth = (id) => nodeDepth(this.nodes, id);
|
|
929
1441
|
getDescendantIds = (id) => descendantIds(this.nodes, id);
|
|
@@ -1025,6 +1537,16 @@ var RivetGraphStore = class {
|
|
|
1025
1537
|
* candidate (single-handle nodes, custom layouts, a culled peer whose
|
|
1026
1538
|
* handles are unregistered) means the pin is kept.
|
|
1027
1539
|
*/
|
|
1540
|
+
/**
|
|
1541
|
+
* The side of `nodeId` that faces `peerRect` and can actually be reached by
|
|
1542
|
+
* this end. An anchor end is unconstrained — its stray places itself along
|
|
1543
|
+
* whichever side faces the peer — and so is a node with no compatible handle
|
|
1544
|
+
* at all, which attaches at side midpoints.
|
|
1545
|
+
*/
|
|
1546
|
+
reachableFacingSide(nodeId, handleId, rect, peerRect, end) {
|
|
1547
|
+
const allowed = parseAnchorHandleId(handleId) || parseAnchorAutoHandleId(handleId) ? [] : reachableSides(nodeId, end, this.handles);
|
|
1548
|
+
return resolveFacingSideAmong(rect, peerRect, allowed);
|
|
1549
|
+
}
|
|
1028
1550
|
realignEndHandle(nodeId, handleId, side, end) {
|
|
1029
1551
|
if (!handleId || parseAnchorAutoHandleId(handleId)) return handleId;
|
|
1030
1552
|
const anchor = parseAnchorHandleId(handleId);
|
|
@@ -1066,8 +1588,8 @@ var RivetGraphStore = class {
|
|
|
1066
1588
|
const targetRect = this.getNodeRect(edge.target);
|
|
1067
1589
|
const sourceKey = displayedSideKey(edgeId, "source");
|
|
1068
1590
|
const targetKey = displayedSideKey(edgeId, "target");
|
|
1069
|
-
const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ??
|
|
1070
|
-
const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ??
|
|
1591
|
+
const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? this.reachableFacingSide(edge.source, edge.sourceHandle, sourceRect, targetRect, "source");
|
|
1592
|
+
const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ?? this.reachableFacingSide(edge.target, edge.targetHandle, targetRect, sourceRect, "target");
|
|
1071
1593
|
const sourceHandle = this.realignEndHandle(
|
|
1072
1594
|
edge.source,
|
|
1073
1595
|
edge.sourceHandle,
|
|
@@ -1102,6 +1624,42 @@ var RivetGraphStore = class {
|
|
|
1102
1624
|
if (!dragging) this.draggingNodeIds.clear();
|
|
1103
1625
|
};
|
|
1104
1626
|
isNodeResizing = () => this.nodeResizing;
|
|
1627
|
+
setGestureHandlers = (handlers) => {
|
|
1628
|
+
this.gestureStartHandler = handlers.start ?? null;
|
|
1629
|
+
this.gestureEndHandler = handlers.end ?? null;
|
|
1630
|
+
this.lockConflictHandler = handlers.conflict ?? null;
|
|
1631
|
+
};
|
|
1632
|
+
setLockedNodes = (locks) => {
|
|
1633
|
+
const opened = this.locks.setLocks(locks);
|
|
1634
|
+
if (opened.length === 0 || this.heldNodeIds.size === 0) return;
|
|
1635
|
+
const held = [...this.heldNodeIds];
|
|
1636
|
+
for (const nodeId of opened) {
|
|
1637
|
+
if (!this.heldNodeIds.has(nodeId)) continue;
|
|
1638
|
+
const holderId = this.locks.getHolder(nodeId);
|
|
1639
|
+
if (holderId) this.lockConflictHandler?.({ nodeId, holderId, ids: held });
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
isNodeHeld = (id) => this.heldNodeIds.has(id);
|
|
1643
|
+
getHeldNodeIds = () => this.heldNodeIds;
|
|
1644
|
+
beginNodeGesture = (id, ids = [id]) => {
|
|
1645
|
+
const opened = ids.filter((nodeId) => !this.heldNodeIds.has(nodeId));
|
|
1646
|
+
if (opened.length === 0) return;
|
|
1647
|
+
for (const nodeId of opened) this.heldNodeIds.add(nodeId);
|
|
1648
|
+
this.gestureStartHandler?.({ id, ids: [...ids] });
|
|
1649
|
+
this.notifyHeld();
|
|
1650
|
+
};
|
|
1651
|
+
endNodeGesture = (id, ids = [id]) => {
|
|
1652
|
+
const closed = ids.filter((nodeId) => this.heldNodeIds.delete(nodeId));
|
|
1653
|
+
if (closed.length === 0) return;
|
|
1654
|
+
this.gestureEndHandler?.({ id, ids: [...ids] });
|
|
1655
|
+
this.notifyHeld();
|
|
1656
|
+
};
|
|
1657
|
+
subscribeHeldNodes = (listener) => {
|
|
1658
|
+
this.heldListeners.add(listener);
|
|
1659
|
+
return () => {
|
|
1660
|
+
this.heldListeners.delete(listener);
|
|
1661
|
+
};
|
|
1662
|
+
};
|
|
1105
1663
|
selectNode = (id, additive = false) => {
|
|
1106
1664
|
this.setSelectionBoxActive(false);
|
|
1107
1665
|
if (id === null) {
|
|
@@ -1220,6 +1778,7 @@ var RivetGraphStore = class {
|
|
|
1220
1778
|
}
|
|
1221
1779
|
if (this.selectedNodeIds.size > 0) {
|
|
1222
1780
|
for (const id of [...this.selectedNodeIds]) {
|
|
1781
|
+
if (!this.locks.allows(id, "delete")) continue;
|
|
1223
1782
|
if (this.removeNodeInternal(id, removedEdges)) removedNodes.push(id);
|
|
1224
1783
|
}
|
|
1225
1784
|
}
|
|
@@ -1249,6 +1808,19 @@ var RivetGraphStore = class {
|
|
|
1249
1808
|
this.history.rebase();
|
|
1250
1809
|
this.requestRender();
|
|
1251
1810
|
};
|
|
1811
|
+
applyRemote = ({ nodes = [], edges = [], origin = "remote" }) => {
|
|
1812
|
+
const guarded = guardNodeChanges(nodes, this.isNodeHeld, (id) => this.nodes.get(id));
|
|
1813
|
+
if (guarded.length === 0 && edges.length === 0) return;
|
|
1814
|
+
const currentNodes = [...this.nodes.values()];
|
|
1815
|
+
const currentEdges = [...this.edges.values()];
|
|
1816
|
+
this.reconcile(applyNodeChanges(guarded, currentNodes), applyEdgeChanges(edges, currentEdges));
|
|
1817
|
+
this.history.rewrite((snapshot) => ({
|
|
1818
|
+
nodes: guarded.length > 0 ? applyNodeChanges(guarded, snapshot.nodes) : snapshot.nodes,
|
|
1819
|
+
edges: edges.length > 0 ? applyEdgeChanges(edges, snapshot.edges) : snapshot.edges
|
|
1820
|
+
}));
|
|
1821
|
+
this.emit(guarded, this.nodeChangeHandler, false, origin);
|
|
1822
|
+
this.emit(edges, this.edgeChangeHandler, false, origin);
|
|
1823
|
+
};
|
|
1252
1824
|
// --- undo / redo ---------------------------------------------------------
|
|
1253
1825
|
/** Structural snapshot for history — no selection/hover/drag state. */
|
|
1254
1826
|
historySnapshot() {
|
|
@@ -1453,6 +2025,13 @@ var RivetGraphStore = class {
|
|
|
1453
2025
|
bindRenderRequester = (fn) => {
|
|
1454
2026
|
this.renderRequester = fn;
|
|
1455
2027
|
};
|
|
2028
|
+
requestCursorRender = () => {
|
|
2029
|
+
if (this.cursorRenderRequester) this.cursorRenderRequester();
|
|
2030
|
+
else this.renderRequester();
|
|
2031
|
+
};
|
|
2032
|
+
bindCursorRenderRequester = (fn) => {
|
|
2033
|
+
this.cursorRenderRequester = fn;
|
|
2034
|
+
};
|
|
1456
2035
|
// --- reconnect gateway (runtime-bound; consulted by <Handle>) ------------
|
|
1457
2036
|
reconnectDelegate = null;
|
|
1458
2037
|
getSelectedEdge = () => this.selectedEdgeId;
|
|
@@ -1465,66 +2044,27 @@ function createRivetStore(init) {
|
|
|
1465
2044
|
return new RivetGraphStore(init);
|
|
1466
2045
|
}
|
|
1467
2046
|
|
|
1468
|
-
// src/
|
|
1469
|
-
function
|
|
2047
|
+
// src/connection.ts
|
|
2048
|
+
function clientToWorld(store, paneRef, clientX, clientY) {
|
|
2049
|
+
const rect = paneRef.current?.getBoundingClientRect();
|
|
2050
|
+
const point = { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };
|
|
2051
|
+
return screenToWorld(point, store.getViewport());
|
|
2052
|
+
}
|
|
2053
|
+
function isCompatible(from, hit) {
|
|
2054
|
+
const canSource = from.type !== "target";
|
|
2055
|
+
const canTarget = from.type !== "source";
|
|
2056
|
+
return hit.nodeId !== from.nodeId && (canSource && hit.type !== "source" || canTarget && hit.type !== "target");
|
|
2057
|
+
}
|
|
2058
|
+
function findHandleAt(clientX, clientY) {
|
|
2059
|
+
const element = document.elementFromPoint(clientX, clientY);
|
|
2060
|
+
const handleEl = element instanceof Element ? element.closest(SELECTOR.handle) : null;
|
|
2061
|
+
if (!handleEl) return null;
|
|
2062
|
+
const { rivetHandleNode, rivetHandleId, rivetHandleType } = handleEl.dataset;
|
|
2063
|
+
if (!rivetHandleNode || !rivetHandleId || !rivetHandleType) return null;
|
|
1470
2064
|
return {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
function screenToWorld(point, viewport) {
|
|
1476
|
-
return {
|
|
1477
|
-
x: (point.x - viewport.x) / viewport.zoom,
|
|
1478
|
-
y: (point.y - viewport.y) / viewport.zoom
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
function zoomAt(viewport, anchor, nextZoom, minZoom = 0.1, maxZoom = 4) {
|
|
1482
|
-
const zoom = clamp(nextZoom, minZoom, maxZoom);
|
|
1483
|
-
const world = screenToWorld(anchor, viewport);
|
|
1484
|
-
return {
|
|
1485
|
-
zoom,
|
|
1486
|
-
x: anchor.x - world.x * zoom,
|
|
1487
|
-
y: anchor.y - world.y * zoom
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
function viewportToCss(viewport) {
|
|
1491
|
-
return `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
|
|
1492
|
-
}
|
|
1493
|
-
function visibleWorldRect(viewport, width, height) {
|
|
1494
|
-
const topLeft = screenToWorld({ x: 0, y: 0 }, viewport);
|
|
1495
|
-
const bottomRight = screenToWorld({ x: width, y: height }, viewport);
|
|
1496
|
-
return {
|
|
1497
|
-
x: topLeft.x,
|
|
1498
|
-
y: topLeft.y,
|
|
1499
|
-
width: bottomRight.x - topLeft.x,
|
|
1500
|
-
height: bottomRight.y - topLeft.y
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
function rectsIntersect(a, b) {
|
|
1504
|
-
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// src/connection.ts
|
|
1508
|
-
function clientToWorld(store, paneRef, clientX, clientY) {
|
|
1509
|
-
const rect = paneRef.current?.getBoundingClientRect();
|
|
1510
|
-
const point = { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };
|
|
1511
|
-
return screenToWorld(point, store.getViewport());
|
|
1512
|
-
}
|
|
1513
|
-
function isCompatible(from, hit) {
|
|
1514
|
-
const canSource = from.type !== "target";
|
|
1515
|
-
const canTarget = from.type !== "source";
|
|
1516
|
-
return hit.nodeId !== from.nodeId && (canSource && hit.type !== "source" || canTarget && hit.type !== "target");
|
|
1517
|
-
}
|
|
1518
|
-
function findHandleAt(clientX, clientY) {
|
|
1519
|
-
const element = document.elementFromPoint(clientX, clientY);
|
|
1520
|
-
const handleEl = element instanceof Element ? element.closest(SELECTOR.handle) : null;
|
|
1521
|
-
if (!handleEl) return null;
|
|
1522
|
-
const { rivetHandleNode, rivetHandleId, rivetHandleType } = handleEl.dataset;
|
|
1523
|
-
if (!rivetHandleNode || !rivetHandleId || !rivetHandleType) return null;
|
|
1524
|
-
return {
|
|
1525
|
-
nodeId: rivetHandleNode,
|
|
1526
|
-
handleId: rivetHandleId,
|
|
1527
|
-
type: rivetHandleType
|
|
2065
|
+
nodeId: rivetHandleNode,
|
|
2066
|
+
handleId: rivetHandleId,
|
|
2067
|
+
type: rivetHandleType
|
|
1528
2068
|
};
|
|
1529
2069
|
}
|
|
1530
2070
|
function handleWorldPosition(store, hit) {
|
|
@@ -1595,8 +2135,17 @@ function startConnectionDrag(config) {
|
|
|
1595
2135
|
validate,
|
|
1596
2136
|
map,
|
|
1597
2137
|
onEnd,
|
|
1598
|
-
resolveHit = resolveConnectionHit
|
|
2138
|
+
resolveHit: resolveHitInput = resolveConnectionHit
|
|
1599
2139
|
} = config;
|
|
2140
|
+
if (!store.locks.allows(from.nodeId, "connect")) {
|
|
2141
|
+
onEnd?.();
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
const resolveHit = (...args) => {
|
|
2145
|
+
const hit = resolveHitInput(...args);
|
|
2146
|
+
if (hit && !store.locks.allows(hit.nodeId, "connect")) return null;
|
|
2147
|
+
return hit;
|
|
2148
|
+
};
|
|
1600
2149
|
store.beginConnection(begin);
|
|
1601
2150
|
const onMove = (event) => {
|
|
1602
2151
|
const hit = resolveHit(store, paneRef, from, event.clientX, event.clientY);
|
|
@@ -1699,6 +2248,10 @@ function Handle({
|
|
|
1699
2248
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1700
2249
|
event.preventDefault();
|
|
1701
2250
|
const self = { nodeId, handleId, type: resolvedType };
|
|
2251
|
+
if (!store.locks.allows(nodeId, "connect")) {
|
|
2252
|
+
announce(`${nodeName(nodeId)} is locked by someone else.`);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
1702
2255
|
const pending = store.getPending();
|
|
1703
2256
|
if (!pending) {
|
|
1704
2257
|
const origin = handleWorldPosition(store, self);
|
|
@@ -1751,6 +2304,7 @@ function Handle({
|
|
|
1751
2304
|
const onFocus = () => {
|
|
1752
2305
|
const pending = store.getPending();
|
|
1753
2306
|
if (!pending || pending.source === nodeId && pending.sourceHandle === handleId) return;
|
|
2307
|
+
if (!store.locks.allows(nodeId, "connect")) return;
|
|
1754
2308
|
const world = handleWorldPosition(store, { nodeId, handleId});
|
|
1755
2309
|
if (world) store.updateConnection(world, place);
|
|
1756
2310
|
};
|
|
@@ -1835,7 +2389,12 @@ var baseStyle = {
|
|
|
1835
2389
|
minWidth: 120,
|
|
1836
2390
|
padding: "10px 14px",
|
|
1837
2391
|
borderRadius: 8,
|
|
1838
|
-
border:
|
|
2392
|
+
// Longhand, not the `border` shorthand: the hover/selected styles below
|
|
2393
|
+
// override `borderColor` alone, and React warns when a shorthand and a
|
|
2394
|
+
// longhand for the same value are mixed across renders.
|
|
2395
|
+
borderWidth: 1,
|
|
2396
|
+
borderStyle: "solid",
|
|
2397
|
+
borderColor: "rgba(100, 116, 139, 0.4)",
|
|
1839
2398
|
background: "#ffffff",
|
|
1840
2399
|
color: "#0f172a",
|
|
1841
2400
|
fontSize: 13,
|
|
@@ -1867,7 +2426,10 @@ var baseStyle2 = {
|
|
|
1867
2426
|
minHeight: 100,
|
|
1868
2427
|
boxSizing: "border-box",
|
|
1869
2428
|
borderRadius: 10,
|
|
1870
|
-
|
|
2429
|
+
// Longhand — see the note in `default-node.tsx`.
|
|
2430
|
+
borderWidth: 1.5,
|
|
2431
|
+
borderStyle: "dashed",
|
|
2432
|
+
borderColor: "rgba(100, 116, 139, 0.45)",
|
|
1871
2433
|
background: "rgba(100, 116, 139, 0.06)"
|
|
1872
2434
|
};
|
|
1873
2435
|
var hoveredStyle2 = {
|
|
@@ -2051,6 +2613,11 @@ function NodeResizer({
|
|
|
2051
2613
|
}) {
|
|
2052
2614
|
const { store } = useRivetContext();
|
|
2053
2615
|
const { nodeId } = useRivetNodeContext();
|
|
2616
|
+
const resizable = useSyncExternalStore(
|
|
2617
|
+
store.locks.subscribe,
|
|
2618
|
+
useCallback(() => store.locks.allows(nodeId, "resize"), [store, nodeId]),
|
|
2619
|
+
useCallback(() => store.locks.allows(nodeId, "resize"), [store, nodeId])
|
|
2620
|
+
);
|
|
2054
2621
|
const startResize = (dir) => (event) => {
|
|
2055
2622
|
if (event.button !== 0) return;
|
|
2056
2623
|
event.stopPropagation();
|
|
@@ -2082,7 +2649,16 @@ function NodeResizer({
|
|
|
2082
2649
|
};
|
|
2083
2650
|
return { size: { width, height }, position };
|
|
2084
2651
|
};
|
|
2652
|
+
let started = false;
|
|
2085
2653
|
const onMove = (moveEvent) => {
|
|
2654
|
+
if (!started) {
|
|
2655
|
+
started = true;
|
|
2656
|
+
store.beginNodeGesture(nodeId);
|
|
2657
|
+
} else if (!store.isNodeHeld(nodeId)) {
|
|
2658
|
+
window.removeEventListener("pointermove", onMove);
|
|
2659
|
+
window.removeEventListener("pointerup", onUp);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2086
2662
|
const { size, position } = compute(moveEvent);
|
|
2087
2663
|
const moved = dir.left || dir.top;
|
|
2088
2664
|
store.resizeNode(nodeId, size, moved ? position : void 0, false);
|
|
@@ -2091,14 +2667,17 @@ function NodeResizer({
|
|
|
2091
2667
|
const onUp = (upEvent) => {
|
|
2092
2668
|
window.removeEventListener("pointermove", onMove);
|
|
2093
2669
|
window.removeEventListener("pointerup", onUp);
|
|
2670
|
+
if (started && !store.isNodeHeld(nodeId)) return;
|
|
2094
2671
|
const { size, position } = compute(upEvent);
|
|
2095
2672
|
const moved = dir.left || dir.top;
|
|
2096
2673
|
store.resizeNode(nodeId, size, moved ? position : void 0, true);
|
|
2674
|
+
if (started) store.endNodeGesture(nodeId);
|
|
2097
2675
|
onResizeEnd?.(size, position);
|
|
2098
2676
|
};
|
|
2099
2677
|
window.addEventListener("pointermove", onMove);
|
|
2100
2678
|
window.addEventListener("pointerup", onUp);
|
|
2101
2679
|
};
|
|
2680
|
+
if (!resizable) return null;
|
|
2102
2681
|
return /* @__PURE__ */ jsx(Fragment, { children: HANDLES.map((dir) => /* @__PURE__ */ jsx(
|
|
2103
2682
|
"div",
|
|
2104
2683
|
{
|
|
@@ -2114,6 +2693,106 @@ function NodeResizer({
|
|
|
2114
2693
|
dir.key
|
|
2115
2694
|
)) });
|
|
2116
2695
|
}
|
|
2696
|
+
|
|
2697
|
+
// src/presence/local.ts
|
|
2698
|
+
var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
2699
|
+
var samePresence = (a, b) => a.cursor?.x === b.cursor?.x && a.cursor?.y === b.cursor?.y && sameIds(a.selection, b.selection) && sameIds(a.holding, b.holding);
|
|
2700
|
+
function trackLocalPresence({
|
|
2701
|
+
store,
|
|
2702
|
+
pane,
|
|
2703
|
+
emit,
|
|
2704
|
+
throttleMs
|
|
2705
|
+
}) {
|
|
2706
|
+
let cursor = null;
|
|
2707
|
+
let screenPoint = null;
|
|
2708
|
+
let last = null;
|
|
2709
|
+
let timer = null;
|
|
2710
|
+
let lastEmit = Number.NEGATIVE_INFINITY;
|
|
2711
|
+
const publish = () => {
|
|
2712
|
+
if (timer !== null) {
|
|
2713
|
+
clearTimeout(timer);
|
|
2714
|
+
timer = null;
|
|
2715
|
+
}
|
|
2716
|
+
lastEmit = performance.now();
|
|
2717
|
+
const next = {
|
|
2718
|
+
cursor,
|
|
2719
|
+
selection: store.getSelectedNodes(),
|
|
2720
|
+
holding: [...store.getHeldNodeIds()]
|
|
2721
|
+
};
|
|
2722
|
+
if (last && samePresence(last, next)) return;
|
|
2723
|
+
last = next;
|
|
2724
|
+
emit(next);
|
|
2725
|
+
};
|
|
2726
|
+
const schedule = () => {
|
|
2727
|
+
if (timer !== null) return;
|
|
2728
|
+
const wait = throttleMs - (performance.now() - lastEmit);
|
|
2729
|
+
if (wait <= 0) {
|
|
2730
|
+
publish();
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
timer = setTimeout(() => {
|
|
2734
|
+
timer = null;
|
|
2735
|
+
publish();
|
|
2736
|
+
}, wait);
|
|
2737
|
+
};
|
|
2738
|
+
const project = () => {
|
|
2739
|
+
if (!screenPoint) {
|
|
2740
|
+
cursor = null;
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
cursor = screenToWorld(screenPoint, store.getViewport());
|
|
2744
|
+
};
|
|
2745
|
+
const onPointerMove = (event) => {
|
|
2746
|
+
const rect = pane.getBoundingClientRect();
|
|
2747
|
+
screenPoint = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
2748
|
+
project();
|
|
2749
|
+
schedule();
|
|
2750
|
+
};
|
|
2751
|
+
const onPointerLeave = () => {
|
|
2752
|
+
screenPoint = null;
|
|
2753
|
+
cursor = null;
|
|
2754
|
+
publish();
|
|
2755
|
+
};
|
|
2756
|
+
const unsubscribeViewport = store.subscribeViewport(() => {
|
|
2757
|
+
if (!screenPoint) return;
|
|
2758
|
+
project();
|
|
2759
|
+
schedule();
|
|
2760
|
+
});
|
|
2761
|
+
const unsubscribeSelection = store.subscribeSelection(() => publish());
|
|
2762
|
+
const unsubscribeHeld = store.subscribeHeldNodes(() => publish());
|
|
2763
|
+
pane.addEventListener("pointermove", onPointerMove);
|
|
2764
|
+
pane.addEventListener("pointerleave", onPointerLeave);
|
|
2765
|
+
return () => {
|
|
2766
|
+
if (timer !== null) clearTimeout(timer);
|
|
2767
|
+
pane.removeEventListener("pointermove", onPointerMove);
|
|
2768
|
+
pane.removeEventListener("pointerleave", onPointerLeave);
|
|
2769
|
+
unsubscribeViewport();
|
|
2770
|
+
unsubscribeSelection();
|
|
2771
|
+
unsubscribeHeld();
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/hooks/use-local-presence.ts
|
|
2776
|
+
function useLocalPresence({
|
|
2777
|
+
store,
|
|
2778
|
+
paneRef,
|
|
2779
|
+
onLocalPresence,
|
|
2780
|
+
throttleMs
|
|
2781
|
+
}) {
|
|
2782
|
+
const handlerRef = useRef(onLocalPresence);
|
|
2783
|
+
handlerRef.current = onLocalPresence;
|
|
2784
|
+
const enabled = Boolean(onLocalPresence);
|
|
2785
|
+
useEffect(() => {
|
|
2786
|
+
const pane = paneRef.current;
|
|
2787
|
+
if (!enabled || !pane) return;
|
|
2788
|
+
return trackLocalPresence({
|
|
2789
|
+
store,
|
|
2790
|
+
pane,
|
|
2791
|
+
emit: (presence) => handlerRef.current?.(presence),
|
|
2792
|
+
throttleMs
|
|
2793
|
+
});
|
|
2794
|
+
}, [store, paneRef, enabled, throttleMs]);
|
|
2795
|
+
}
|
|
2117
2796
|
function shallowEqual(a, b) {
|
|
2118
2797
|
if (Object.is(a, b)) return true;
|
|
2119
2798
|
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
@@ -2169,6 +2848,11 @@ function useRivetConfig(params) {
|
|
|
2169
2848
|
() => ({ ...DEFAULT_ANCHOR_OPTIONS, ...anchorOptionsInput }),
|
|
2170
2849
|
[anchorOptionsInput]
|
|
2171
2850
|
);
|
|
2851
|
+
const presenceOptionsInput = useShallowStable(params.presenceOptions);
|
|
2852
|
+
const presenceOptions = useMemo(
|
|
2853
|
+
() => ({ ...DEFAULT_PRESENCE_OPTIONS, ...presenceOptionsInput }),
|
|
2854
|
+
[presenceOptionsInput]
|
|
2855
|
+
);
|
|
2172
2856
|
const isValidConnection = useStableOptional(params.isValidConnection);
|
|
2173
2857
|
const mapConnection = useStableOptional(params.mapConnection);
|
|
2174
2858
|
const onConnect = useStableOptional(params.onConnect);
|
|
@@ -2182,6 +2866,7 @@ function useRivetConfig(params) {
|
|
|
2182
2866
|
snapGrid,
|
|
2183
2867
|
swimlaneMargin,
|
|
2184
2868
|
anchorOptions,
|
|
2869
|
+
presenceOptions,
|
|
2185
2870
|
panButtons,
|
|
2186
2871
|
selectionKeys,
|
|
2187
2872
|
multiSelectionKeys,
|
|
@@ -2221,7 +2906,7 @@ function createKeyboardHandler(store, getSnapGrid) {
|
|
|
2221
2906
|
}
|
|
2222
2907
|
const dir = ARROW_DIRS[event.key];
|
|
2223
2908
|
if (!dir) return;
|
|
2224
|
-
const movers = store.getMovers(store.getSelectedNodes());
|
|
2909
|
+
const movers = store.getMovers(store.getSelectedNodes()).filter((id) => store.locks.allows(id, "drag"));
|
|
2225
2910
|
if (movers.length === 0) return;
|
|
2226
2911
|
event.preventDefault();
|
|
2227
2912
|
const snapGrid = getSnapGrid?.() ?? null;
|
|
@@ -2299,6 +2984,7 @@ function createMarqueeController(deps) {
|
|
|
2299
2984
|
const ids = [];
|
|
2300
2985
|
for (const [id, node] of store.nodes) {
|
|
2301
2986
|
if (node.selectable === false) continue;
|
|
2987
|
+
if (!store.locks.allows(id, "select")) continue;
|
|
2302
2988
|
if (rectsIntersect(worldRect, store.getNodeRect(id))) ids.push(id);
|
|
2303
2989
|
}
|
|
2304
2990
|
store.selectNodes(additive ? [.../* @__PURE__ */ new Set([...base, ...ids])] : ids);
|
|
@@ -2365,454 +3051,221 @@ function createPanController(deps) {
|
|
|
2365
3051
|
schedule();
|
|
2366
3052
|
},
|
|
2367
3053
|
end(event) {
|
|
2368
|
-
if (!panning) return;
|
|
2369
|
-
panning = false;
|
|
2370
|
-
pane.releasePointerCapture(event.pointerId);
|
|
2371
|
-
},
|
|
2372
|
-
suppressNextContextMenu() {
|
|
2373
|
-
if (!suppressContextMenu) return false;
|
|
2374
|
-
suppressContextMenu = false;
|
|
2375
|
-
return true;
|
|
2376
|
-
}
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
// src/input/resize-controller.ts
|
|
2381
|
-
function createResizeController(deps) {
|
|
2382
|
-
const { pane, bgCanvas, size, edgeRenderer, foregroundRenderer, schedule } = deps;
|
|
2383
|
-
const resize = () => {
|
|
2384
|
-
const rect = pane.getBoundingClientRect();
|
|
2385
|
-
size.width = rect.width;
|
|
2386
|
-
size.height = rect.height;
|
|
2387
|
-
size.dpr = window.devicePixelRatio || 1;
|
|
2388
|
-
bgCanvas.width = Math.max(1, Math.round(rect.width * size.dpr));
|
|
2389
|
-
bgCanvas.height = Math.max(1, Math.round(rect.height * size.dpr));
|
|
2390
|
-
bgCanvas.style.width = `${rect.width}px`;
|
|
2391
|
-
bgCanvas.style.height = `${rect.height}px`;
|
|
2392
|
-
edgeRenderer.resize(rect.width, rect.height, size.dpr);
|
|
2393
|
-
foregroundRenderer.resize(rect.width, rect.height, size.dpr);
|
|
2394
|
-
schedule();
|
|
2395
|
-
};
|
|
2396
|
-
const observer = new ResizeObserver(resize);
|
|
2397
|
-
observer.observe(pane);
|
|
2398
|
-
resize();
|
|
2399
|
-
return { dispose: () => observer.disconnect() };
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
// src/input/zoom-controller.ts
|
|
2403
|
-
var WHEEL_LINE_PX = 16;
|
|
2404
|
-
var ZOOM_EASE = 0.22;
|
|
2405
|
-
function createZoomController(deps) {
|
|
2406
|
-
const { store, pane, size, minZoom, maxZoom, zoomSpeed, scrollToPan, schedule } = deps;
|
|
2407
|
-
let target = null;
|
|
2408
|
-
return {
|
|
2409
|
-
cancel() {
|
|
2410
|
-
target = null;
|
|
2411
|
-
},
|
|
2412
|
-
step() {
|
|
2413
|
-
if (!target) return;
|
|
2414
|
-
const vp = store.getViewport();
|
|
2415
|
-
const diff = target.zoom - vp.zoom;
|
|
2416
|
-
if (Math.abs(diff) < 5e-4) {
|
|
2417
|
-
store.setViewport(zoomAt(vp, target.anchor, target.zoom, minZoom, maxZoom));
|
|
2418
|
-
target = null;
|
|
2419
|
-
return;
|
|
2420
|
-
}
|
|
2421
|
-
store.setViewport(zoomAt(vp, target.anchor, vp.zoom + diff * ZOOM_EASE, minZoom, maxZoom));
|
|
2422
|
-
schedule();
|
|
2423
|
-
},
|
|
2424
|
-
onWheel(event) {
|
|
2425
|
-
event.preventDefault();
|
|
2426
|
-
const scale = event.deltaMode === 1 ? WHEEL_LINE_PX : event.deltaMode === 2 ? size.height : 1;
|
|
2427
|
-
const deltaX = event.deltaX * scale;
|
|
2428
|
-
const deltaY = event.deltaY * scale;
|
|
2429
|
-
if (scrollToPan && !event.ctrlKey) {
|
|
2430
|
-
target = null;
|
|
2431
|
-
const viewport = store.getViewport();
|
|
2432
|
-
store.setViewport({ ...viewport, x: viewport.x - deltaX, y: viewport.y - deltaY });
|
|
2433
|
-
schedule();
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2436
|
-
const rect = pane.getBoundingClientRect();
|
|
2437
|
-
const anchor = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
2438
|
-
const base = target?.zoom ?? store.getViewport().zoom;
|
|
2439
|
-
const nextZoom = clamp(base * Math.exp(-deltaY * zoomSpeed), minZoom, maxZoom);
|
|
2440
|
-
target = { zoom: nextZoom, anchor };
|
|
2441
|
-
schedule();
|
|
2442
|
-
}
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
// src/renderer/background.ts
|
|
2447
|
-
function drawDotGrid(ctx, width, height, viewport, options) {
|
|
2448
|
-
const gap = options?.gap ?? 24;
|
|
2449
|
-
const radius = options?.radius ?? 1;
|
|
2450
|
-
const color = options?.color ?? "rgba(100, 116, 139, 0.35)";
|
|
2451
|
-
const step = gap * viewport.zoom;
|
|
2452
|
-
if (step < 8) return;
|
|
2453
|
-
const offsetX = (viewport.x % step + step) % step;
|
|
2454
|
-
const offsetY = (viewport.y % step + step) % step;
|
|
2455
|
-
ctx.fillStyle = color;
|
|
2456
|
-
ctx.beginPath();
|
|
2457
|
-
for (let x = offsetX; x < width; x += step) {
|
|
2458
|
-
for (let y = offsetY; y < height; y += step) {
|
|
2459
|
-
ctx.moveTo(x + radius, y);
|
|
2460
|
-
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
ctx.fill();
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
// src/renderer/edge-paths.ts
|
|
2467
|
-
var SIDE_DIR = {
|
|
2468
|
-
left: { x: -1, y: 0 },
|
|
2469
|
-
right: { x: 1, y: 0 },
|
|
2470
|
-
top: { x: 0, y: -1 },
|
|
2471
|
-
bottom: { x: 0, y: 1 }
|
|
2472
|
-
};
|
|
2473
|
-
var BEZIER_SEGMENT_PX = 8;
|
|
2474
|
-
var BEZIER_MIN_SAMPLES = 16;
|
|
2475
|
-
var BEZIER_MAX_SAMPLES = 160;
|
|
2476
|
-
var CORNER_RADIUS = 8;
|
|
2477
|
-
var ARC_SAMPLES = 6;
|
|
2478
|
-
function cubicAt(a, b, c, d, t) {
|
|
2479
|
-
const mt = 1 - t;
|
|
2480
|
-
return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
|
|
2481
|
-
}
|
|
2482
|
-
function quadAt(a, b, c, t) {
|
|
2483
|
-
const mt = 1 - t;
|
|
2484
|
-
return mt * mt * a + 2 * mt * t * b + t * t * c;
|
|
2485
|
-
}
|
|
2486
|
-
var getStraightPath = ({ sourceX, sourceY, targetX, targetY }) => ({
|
|
2487
|
-
points: [
|
|
2488
|
-
{ x: sourceX, y: sourceY },
|
|
2489
|
-
{ x: targetX, y: targetY }
|
|
2490
|
-
],
|
|
2491
|
-
labelX: (sourceX + targetX) / 2,
|
|
2492
|
-
labelY: (sourceY + targetY) / 2
|
|
2493
|
-
});
|
|
2494
|
-
var getBezierPath = (params) => {
|
|
2495
|
-
const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
|
|
2496
|
-
const sDir = SIDE_DIR[sourcePosition];
|
|
2497
|
-
const tDir = SIDE_DIR[targetPosition];
|
|
2498
|
-
const dist = Math.hypot(targetX - sourceX, targetY - sourceY);
|
|
2499
|
-
const reach = Math.max(40, dist * 0.4);
|
|
2500
|
-
const p0 = { x: sourceX, y: sourceY };
|
|
2501
|
-
const p1 = { x: sourceX + sDir.x * reach, y: sourceY + sDir.y * reach };
|
|
2502
|
-
const p2 = { x: targetX + tDir.x * reach, y: targetY + tDir.y * reach };
|
|
2503
|
-
const p3 = { x: targetX, y: targetY };
|
|
2504
|
-
const ctrlLen = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y) + Math.hypot(p3.x - p2.x, p3.y - p2.y);
|
|
2505
|
-
const samples = Math.max(
|
|
2506
|
-
BEZIER_MIN_SAMPLES,
|
|
2507
|
-
Math.min(BEZIER_MAX_SAMPLES, Math.ceil(ctrlLen / BEZIER_SEGMENT_PX))
|
|
2508
|
-
);
|
|
2509
|
-
const points = [];
|
|
2510
|
-
for (let i = 0; i <= samples; i++) {
|
|
2511
|
-
const t = i / samples;
|
|
2512
|
-
points.push({
|
|
2513
|
-
x: cubicAt(p0.x, p1.x, p2.x, p3.x, t),
|
|
2514
|
-
y: cubicAt(p0.y, p1.y, p2.y, p3.y, t)
|
|
2515
|
-
});
|
|
2516
|
-
}
|
|
2517
|
-
return {
|
|
2518
|
-
points,
|
|
2519
|
-
labelX: cubicAt(p0.x, p1.x, p2.x, p3.x, 0.5),
|
|
2520
|
-
labelY: cubicAt(p0.y, p1.y, p2.y, p3.y, 0.5)
|
|
2521
|
-
};
|
|
2522
|
-
};
|
|
2523
|
-
var STEP_OFFSET = 20;
|
|
2524
|
-
function simplifyCorners(corners) {
|
|
2525
|
-
const out = [];
|
|
2526
|
-
for (const p of corners) {
|
|
2527
|
-
const a = out[out.length - 2];
|
|
2528
|
-
const b = out[out.length - 1];
|
|
2529
|
-
if (b && b.x === p.x && b.y === p.y) continue;
|
|
2530
|
-
if (a && b && (a.x === b.x && b.x === p.x || a.y === b.y && b.y === p.y)) out.pop();
|
|
2531
|
-
out.push(p);
|
|
2532
|
-
}
|
|
2533
|
-
return out;
|
|
2534
|
-
}
|
|
2535
|
-
function stepCorners(params) {
|
|
2536
|
-
const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
|
|
2537
|
-
const s = { x: sourceX, y: sourceY };
|
|
2538
|
-
const t = { x: targetX, y: targetY };
|
|
2539
|
-
const sDir = SIDE_DIR[sourcePosition];
|
|
2540
|
-
const tDir = SIDE_DIR[targetPosition];
|
|
2541
|
-
const sg = { x: sourceX + sDir.x * STEP_OFFSET, y: sourceY + sDir.y * STEP_OFFSET };
|
|
2542
|
-
const tg = { x: targetX + tDir.x * STEP_OFFSET, y: targetY + tDir.y * STEP_OFFSET };
|
|
2543
|
-
return simplifyCorners(routeCorners(s, t, sg, tg, sDir, tDir));
|
|
2544
|
-
}
|
|
2545
|
-
function routeCorners(s, t, sg, tg, sDir, tDir) {
|
|
2546
|
-
const sHoriz = sDir.x !== 0;
|
|
2547
|
-
const tHoriz = tDir.x !== 0;
|
|
2548
|
-
if (sHoriz && tHoriz) {
|
|
2549
|
-
if (sDir.x * tDir.x < 0) {
|
|
2550
|
-
if ((t.x - s.x) * sDir.x >= 2 * STEP_OFFSET) {
|
|
2551
|
-
const midX = (s.x + t.x) / 2;
|
|
2552
|
-
return [s, { x: midX, y: s.y }, { x: midX, y: t.y }, t];
|
|
2553
|
-
}
|
|
2554
|
-
const midY = (s.y + t.y) / 2;
|
|
2555
|
-
return [s, sg, { x: sg.x, y: midY }, { x: tg.x, y: midY }, tg, t];
|
|
2556
|
-
}
|
|
2557
|
-
const railX = sDir.x > 0 ? Math.max(sg.x, tg.x) : Math.min(sg.x, tg.x);
|
|
2558
|
-
return [s, { x: railX, y: s.y }, { x: railX, y: t.y }, t];
|
|
2559
|
-
}
|
|
2560
|
-
if (!sHoriz && !tHoriz) {
|
|
2561
|
-
if (sDir.y * tDir.y < 0) {
|
|
2562
|
-
if ((t.y - s.y) * sDir.y >= 2 * STEP_OFFSET) {
|
|
2563
|
-
const midY = (s.y + t.y) / 2;
|
|
2564
|
-
return [s, { x: s.x, y: midY }, { x: t.x, y: midY }, t];
|
|
2565
|
-
}
|
|
2566
|
-
const midX = (s.x + t.x) / 2;
|
|
2567
|
-
return [s, sg, { x: midX, y: sg.y }, { x: midX, y: tg.y }, tg, t];
|
|
2568
|
-
}
|
|
2569
|
-
const railY = sDir.y > 0 ? Math.max(sg.y, tg.y) : Math.min(sg.y, tg.y);
|
|
2570
|
-
return [s, { x: s.x, y: railY }, { x: t.x, y: railY }, t];
|
|
2571
|
-
}
|
|
2572
|
-
const corner = sHoriz ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
|
|
2573
|
-
const exitsSource = sHoriz ? (corner.x - s.x) * sDir.x >= STEP_OFFSET : (corner.y - s.y) * sDir.y >= STEP_OFFSET;
|
|
2574
|
-
const exitsTarget = tHoriz ? (corner.x - t.x) * tDir.x >= STEP_OFFSET : (corner.y - t.y) * tDir.y >= STEP_OFFSET;
|
|
2575
|
-
if (exitsSource && exitsTarget) return [s, corner, t];
|
|
2576
|
-
const bend = sHoriz ? { x: sg.x, y: tg.y } : { x: tg.x, y: sg.y };
|
|
2577
|
-
return [s, sg, bend, tg, t];
|
|
2578
|
-
}
|
|
2579
|
-
function roundCorners(corners, radius) {
|
|
2580
|
-
if (corners.length <= 2) return corners;
|
|
2581
|
-
const points = [];
|
|
2582
|
-
const first = corners[0];
|
|
2583
|
-
if (first) points.push(first);
|
|
2584
|
-
for (let i = 1; i < corners.length - 1; i++) {
|
|
2585
|
-
const prev = corners[i - 1];
|
|
2586
|
-
const curr = corners[i];
|
|
2587
|
-
const next = corners[i + 1];
|
|
2588
|
-
if (!prev || !curr || !next) continue;
|
|
2589
|
-
const inLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
2590
|
-
const outLen = Math.hypot(next.x - curr.x, next.y - curr.y);
|
|
2591
|
-
const r = Math.min(radius, inLen / 2, outLen / 2);
|
|
2592
|
-
const inPt = {
|
|
2593
|
-
x: curr.x + (prev.x - curr.x) / (inLen || 1) * r,
|
|
2594
|
-
y: curr.y + (prev.y - curr.y) / (inLen || 1) * r
|
|
2595
|
-
};
|
|
2596
|
-
const outPt = {
|
|
2597
|
-
x: curr.x + (next.x - curr.x) / (outLen || 1) * r,
|
|
2598
|
-
y: curr.y + (next.y - curr.y) / (outLen || 1) * r
|
|
2599
|
-
};
|
|
2600
|
-
points.push(inPt);
|
|
2601
|
-
for (let j = 1; j < ARC_SAMPLES; j++) {
|
|
2602
|
-
const tt = j / ARC_SAMPLES;
|
|
2603
|
-
points.push({
|
|
2604
|
-
x: quadAt(inPt.x, curr.x, outPt.x, tt),
|
|
2605
|
-
y: quadAt(inPt.y, curr.y, outPt.y, tt)
|
|
2606
|
-
});
|
|
2607
|
-
}
|
|
2608
|
-
points.push(outPt);
|
|
2609
|
-
}
|
|
2610
|
-
const last = corners[corners.length - 1];
|
|
2611
|
-
if (last) points.push(last);
|
|
2612
|
-
return points;
|
|
2613
|
-
}
|
|
2614
|
-
function stepLabel(corners, params) {
|
|
2615
|
-
const i = Math.floor((corners.length - 1) / 2);
|
|
2616
|
-
const a = corners[i];
|
|
2617
|
-
const b = corners[i + 1];
|
|
2618
|
-
if (!a || !b) return { x: params.sourceX, y: params.sourceY };
|
|
2619
|
-
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2620
|
-
}
|
|
2621
|
-
var getSmoothStepPath = (params) => {
|
|
2622
|
-
const corners = stepCorners(params);
|
|
2623
|
-
const label = stepLabel(corners, params);
|
|
2624
|
-
return { points: roundCorners(corners, CORNER_RADIUS), labelX: label.x, labelY: label.y };
|
|
2625
|
-
};
|
|
2626
|
-
var getStepPath = (params) => {
|
|
2627
|
-
const corners = stepCorners(params);
|
|
2628
|
-
const label = stepLabel(corners, params);
|
|
2629
|
-
return { points: corners, labelX: label.x, labelY: label.y };
|
|
2630
|
-
};
|
|
2631
|
-
var BUILTIN_EDGE_TYPES = {
|
|
2632
|
-
bezier: getBezierPath,
|
|
2633
|
-
smoothstep: getSmoothStepPath,
|
|
2634
|
-
step: getStepPath,
|
|
2635
|
-
straight: getStraightPath
|
|
2636
|
-
};
|
|
2637
|
-
|
|
2638
|
-
// src/renderer/edge-geometry.ts
|
|
2639
|
-
var EDGE_COLOR = "rgba(100, 116, 139, 0.75)";
|
|
2640
|
-
var EDGE_COLOR_SELECTED = "#6366f1";
|
|
2641
|
-
var EDGE_COLOR_HOVERED = "rgba(99, 102, 241, 0.9)";
|
|
2642
|
-
var EDGE_WIDTH = 1.5;
|
|
2643
|
-
var EDGE_WIDTH_HOVERED = 2.5;
|
|
2644
|
-
var PENDING_COLOR = "#6366f1";
|
|
2645
|
-
var OPPOSITE = {
|
|
2646
|
-
left: "right",
|
|
2647
|
-
right: "left",
|
|
2648
|
-
top: "bottom",
|
|
2649
|
-
bottom: "top"
|
|
2650
|
-
};
|
|
2651
|
-
var falseToNull = (m) => m || null;
|
|
2652
|
-
function buildEdges(edges, nodes, viewport, edgeTypes, defaults, extras) {
|
|
2653
|
-
const handles = extras?.handles;
|
|
2654
|
-
const hovered = extras?.hovered;
|
|
2655
|
-
const reconnecting = extras?.reconnecting;
|
|
2656
|
-
const worldPositions = extras?.worldPositions;
|
|
2657
|
-
const resolved = [];
|
|
2658
|
-
for (const edge of edges) {
|
|
2659
|
-
if (edge.id === reconnecting) continue;
|
|
2660
|
-
const source = nodes.get(edge.source);
|
|
2661
|
-
const target = nodes.get(edge.target);
|
|
2662
|
-
if (!source || !target) continue;
|
|
2663
|
-
const frame = { handles, viewport, worldPositions, extras };
|
|
2664
|
-
const start = resolveEnd(edge, "source", source, target, frame);
|
|
2665
|
-
const end = resolveEnd(edge, "target", target, source, frame);
|
|
2666
|
-
const params = {
|
|
2667
|
-
sourceX: start.point.x,
|
|
2668
|
-
sourceY: start.point.y,
|
|
2669
|
-
targetX: end.point.x,
|
|
2670
|
-
targetY: end.point.y,
|
|
2671
|
-
sourcePosition: start.position,
|
|
2672
|
-
targetPosition: end.position
|
|
2673
|
-
};
|
|
2674
|
-
const pathFn = edgeTypes[edge.type ?? defaults.type ?? "bezier"] ?? getBezierPath;
|
|
2675
|
-
const path = pathFn(params);
|
|
2676
|
-
if (path.points.length < 2) continue;
|
|
2677
|
-
const style = { ...defaults.style, ...edge.style };
|
|
2678
|
-
const isHovered = edge.id === hovered;
|
|
2679
|
-
resolved.push({
|
|
2680
|
-
id: edge.id,
|
|
2681
|
-
points: path.points,
|
|
2682
|
-
source: start.point,
|
|
2683
|
-
target: end.point,
|
|
2684
|
-
label: edge.label !== void 0 ? { x: path.labelX, y: path.labelY } : void 0,
|
|
2685
|
-
stroke: edge.selected ? EDGE_COLOR_SELECTED : isHovered ? EDGE_COLOR_HOVERED : style.stroke ?? EDGE_COLOR,
|
|
2686
|
-
width: style.strokeWidth ?? (isHovered ? EDGE_WIDTH_HOVERED : EDGE_WIDTH),
|
|
2687
|
-
opacity: style.opacity ?? 1,
|
|
2688
|
-
animated: edge.animated ?? defaults.animated ?? false,
|
|
2689
|
-
dash: style.strokeDasharray,
|
|
2690
|
-
markerStart: falseToNull(edge.markerStart ?? defaults.markerStart),
|
|
2691
|
-
markerEnd: falseToNull(edge.markerEnd ?? defaults.markerEnd)
|
|
2692
|
-
});
|
|
2693
|
-
}
|
|
2694
|
-
return resolved;
|
|
2695
|
-
}
|
|
2696
|
-
function buildPendingPath(pending, handles, viewport) {
|
|
2697
|
-
const record = handles?.get(handleKey(pending.source, pending.sourceHandle));
|
|
2698
|
-
const sourcePosition = record?.position ?? (pending.sourceType === "target" ? "left" : "right");
|
|
2699
|
-
const from = worldToScreen(pending.from, viewport);
|
|
2700
|
-
const to = worldToScreen(pending.to, viewport);
|
|
2701
|
-
const targetPosition = pending.toPosition ?? OPPOSITE[sourcePosition];
|
|
2702
|
-
return getBezierPath({
|
|
2703
|
-
sourceX: from.x,
|
|
2704
|
-
sourceY: from.y,
|
|
2705
|
-
targetX: to.x,
|
|
2706
|
-
targetY: to.y,
|
|
2707
|
-
sourcePosition,
|
|
2708
|
-
targetPosition
|
|
2709
|
-
}).points;
|
|
3054
|
+
if (!panning) return;
|
|
3055
|
+
panning = false;
|
|
3056
|
+
pane.releasePointerCapture(event.pointerId);
|
|
3057
|
+
},
|
|
3058
|
+
suppressNextContextMenu() {
|
|
3059
|
+
if (!suppressContextMenu) return false;
|
|
3060
|
+
suppressContextMenu = false;
|
|
3061
|
+
return true;
|
|
3062
|
+
}
|
|
3063
|
+
};
|
|
2710
3064
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3065
|
+
|
|
3066
|
+
// src/input/resize-controller.ts
|
|
3067
|
+
function createResizeController(deps) {
|
|
3068
|
+
const { pane, bgCanvas, size, edgeRenderer, foregroundRenderer, onResize, schedule } = deps;
|
|
3069
|
+
const resize = () => {
|
|
3070
|
+
const rect = pane.getBoundingClientRect();
|
|
3071
|
+
size.width = rect.width;
|
|
3072
|
+
size.height = rect.height;
|
|
3073
|
+
size.dpr = window.devicePixelRatio || 1;
|
|
3074
|
+
bgCanvas.width = Math.max(1, Math.round(rect.width * size.dpr));
|
|
3075
|
+
bgCanvas.height = Math.max(1, Math.round(rect.height * size.dpr));
|
|
3076
|
+
bgCanvas.style.width = `${rect.width}px`;
|
|
3077
|
+
bgCanvas.style.height = `${rect.height}px`;
|
|
3078
|
+
edgeRenderer.resize(rect.width, rect.height, size.dpr);
|
|
3079
|
+
foregroundRenderer.resize(rect.width, rect.height, size.dpr);
|
|
3080
|
+
onResize?.();
|
|
3081
|
+
schedule();
|
|
3082
|
+
};
|
|
3083
|
+
const observer = new ResizeObserver(resize);
|
|
3084
|
+
observer.observe(pane);
|
|
3085
|
+
resize();
|
|
3086
|
+
return { dispose: () => observer.disconnect() };
|
|
2723
3087
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3088
|
+
|
|
3089
|
+
// src/input/zoom-controller.ts
|
|
3090
|
+
var WHEEL_LINE_PX = 16;
|
|
3091
|
+
var ZOOM_EASE = 0.22;
|
|
3092
|
+
function createZoomController(deps) {
|
|
3093
|
+
const { store, pane, size, minZoom, maxZoom, zoomSpeed, scrollToPan, schedule } = deps;
|
|
3094
|
+
let target = null;
|
|
3095
|
+
return {
|
|
3096
|
+
cancel() {
|
|
3097
|
+
target = null;
|
|
3098
|
+
},
|
|
3099
|
+
step() {
|
|
3100
|
+
if (!target) return;
|
|
3101
|
+
const vp = store.getViewport();
|
|
3102
|
+
const diff = target.zoom - vp.zoom;
|
|
3103
|
+
if (Math.abs(diff) < 5e-4) {
|
|
3104
|
+
store.setViewport(zoomAt(vp, target.anchor, target.zoom, minZoom, maxZoom));
|
|
3105
|
+
target = null;
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
store.setViewport(zoomAt(vp, target.anchor, vp.zoom + diff * ZOOM_EASE, minZoom, maxZoom));
|
|
3109
|
+
schedule();
|
|
3110
|
+
},
|
|
3111
|
+
onWheel(event) {
|
|
3112
|
+
event.preventDefault();
|
|
3113
|
+
const scale = event.deltaMode === 1 ? WHEEL_LINE_PX : event.deltaMode === 2 ? size.height : 1;
|
|
3114
|
+
const deltaX = event.deltaX * scale;
|
|
3115
|
+
const deltaY = event.deltaY * scale;
|
|
3116
|
+
if (scrollToPan && !event.ctrlKey) {
|
|
3117
|
+
target = null;
|
|
3118
|
+
const viewport = store.getViewport();
|
|
3119
|
+
store.setViewport({ ...viewport, x: viewport.x - deltaX, y: viewport.y - deltaY });
|
|
3120
|
+
schedule();
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
const rect = pane.getBoundingClientRect();
|
|
3124
|
+
const anchor = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
3125
|
+
const base = target?.zoom ?? store.getViewport().zoom;
|
|
3126
|
+
const nextZoom = clamp(base * Math.exp(-deltaY * zoomSpeed), minZoom, maxZoom);
|
|
3127
|
+
target = { zoom: nextZoom, anchor };
|
|
3128
|
+
schedule();
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
2728
3131
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3132
|
+
|
|
3133
|
+
// src/renderer/background.ts
|
|
3134
|
+
function drawDotGrid(ctx, width, height, viewport, options) {
|
|
3135
|
+
const gap = options?.gap ?? 24;
|
|
3136
|
+
const radius = options?.radius ?? 1;
|
|
3137
|
+
const color = options?.color ?? "rgba(100, 116, 139, 0.35)";
|
|
3138
|
+
const step = gap * viewport.zoom;
|
|
3139
|
+
if (step < 8) return;
|
|
3140
|
+
const offsetX = (viewport.x % step + step) % step;
|
|
3141
|
+
const offsetY = (viewport.y % step + step) % step;
|
|
3142
|
+
ctx.fillStyle = color;
|
|
3143
|
+
ctx.beginPath();
|
|
3144
|
+
for (let x = offsetX; x < width; x += step) {
|
|
3145
|
+
for (let y = offsetY; y < height; y += step) {
|
|
3146
|
+
ctx.moveTo(x + radius, y);
|
|
3147
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
2738
3148
|
}
|
|
2739
3149
|
}
|
|
2740
|
-
|
|
3150
|
+
ctx.fill();
|
|
2741
3151
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3152
|
+
|
|
3153
|
+
// src/renderer/presence.ts
|
|
3154
|
+
var HELD_LINE_WIDTH = 2;
|
|
3155
|
+
var SELECTED_LINE_WIDTH = 1.5;
|
|
3156
|
+
var SELECTED_DASH = [4, 4];
|
|
3157
|
+
var OUTLINE_RADIUS = 6;
|
|
3158
|
+
var CURSOR_POINTS = [
|
|
3159
|
+
{ x: 0, y: 0 },
|
|
3160
|
+
{ x: 0, y: 16 },
|
|
3161
|
+
{ x: 4.2, y: 12.2 },
|
|
3162
|
+
{ x: 6.8, y: 17.6 },
|
|
3163
|
+
{ x: 9.2, y: 16.6 },
|
|
3164
|
+
{ x: 6.6, y: 11.4 },
|
|
3165
|
+
{ x: 11.4, y: 11.4 }
|
|
3166
|
+
];
|
|
3167
|
+
var CURSOR_LABEL_OFFSET = { x: 12, y: 17 };
|
|
3168
|
+
var LABEL_FONT = "500 11px ui-sans-serif, system-ui, -apple-system, sans-serif";
|
|
3169
|
+
var LABEL_PADDING_X = 6;
|
|
3170
|
+
var LABEL_HEIGHT = 17;
|
|
3171
|
+
var LABEL_RADIUS = 4;
|
|
3172
|
+
var CULL_MARGIN_PX = 64;
|
|
3173
|
+
function traceRect(ctx, rect, radius) {
|
|
3174
|
+
ctx.beginPath();
|
|
3175
|
+
if (typeof ctx.roundRect === "function") {
|
|
3176
|
+
ctx.roundRect(rect.x, rect.y, rect.width, rect.height, radius);
|
|
3177
|
+
} else {
|
|
3178
|
+
ctx.rect(rect.x, rect.y, rect.width, rect.height);
|
|
2749
3179
|
}
|
|
2750
|
-
return sides;
|
|
2751
3180
|
}
|
|
2752
|
-
function
|
|
2753
|
-
const {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
if (alongPct === void 0 && !anchorId) {
|
|
2760
|
-
const candidate = handleOnSide(node.id, side2, role, handles);
|
|
2761
|
-
if (candidate) {
|
|
2762
|
-
return {
|
|
2763
|
-
point: worldToScreen(
|
|
2764
|
-
{ x: rect.x + candidate.offset.x, y: rect.y + candidate.offset.y },
|
|
2765
|
-
viewport
|
|
2766
|
-
),
|
|
2767
|
-
position: side2
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
return { point: worldToScreen(pointOnSide(rect, side2, alongPct), viewport), position: side2 };
|
|
3181
|
+
function projectRect(rect, viewport) {
|
|
3182
|
+
const origin = worldToScreen({ x: rect.x, y: rect.y }, viewport);
|
|
3183
|
+
return {
|
|
3184
|
+
x: origin.x,
|
|
3185
|
+
y: origin.y,
|
|
3186
|
+
width: rect.width * viewport.zoom,
|
|
3187
|
+
height: rect.height * viewport.zoom
|
|
2772
3188
|
};
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
3189
|
+
}
|
|
3190
|
+
function onScreen(rect, size) {
|
|
3191
|
+
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;
|
|
3192
|
+
}
|
|
3193
|
+
function drawNodeOutline(ctx, rect, color, mode) {
|
|
3194
|
+
ctx.strokeStyle = color;
|
|
3195
|
+
ctx.lineWidth = mode === "selected" ? SELECTED_LINE_WIDTH : HELD_LINE_WIDTH;
|
|
3196
|
+
ctx.setLineDash(mode === "selected" ? SELECTED_DASH : []);
|
|
3197
|
+
traceRect(ctx, rect, OUTLINE_RADIUS);
|
|
3198
|
+
ctx.stroke();
|
|
3199
|
+
ctx.setLineDash([]);
|
|
3200
|
+
}
|
|
3201
|
+
function drawCursor(ctx, point, color, name) {
|
|
3202
|
+
ctx.beginPath();
|
|
3203
|
+
const [first, ...rest] = CURSOR_POINTS;
|
|
3204
|
+
if (!first) return;
|
|
3205
|
+
ctx.moveTo(point.x + first.x, point.y + first.y);
|
|
3206
|
+
for (const offset of rest) ctx.lineTo(point.x + offset.x, point.y + offset.y);
|
|
3207
|
+
ctx.closePath();
|
|
3208
|
+
ctx.fillStyle = color;
|
|
3209
|
+
ctx.fill();
|
|
3210
|
+
ctx.strokeStyle = "#ffffff";
|
|
3211
|
+
ctx.lineWidth = 1;
|
|
3212
|
+
ctx.stroke();
|
|
3213
|
+
if (!name) return;
|
|
3214
|
+
ctx.font = LABEL_FONT;
|
|
3215
|
+
ctx.textBaseline = "middle";
|
|
3216
|
+
const width = ctx.measureText(name).width + LABEL_PADDING_X * 2;
|
|
3217
|
+
const x = point.x + CURSOR_LABEL_OFFSET.x;
|
|
3218
|
+
const y = point.y + CURSOR_LABEL_OFFSET.y;
|
|
3219
|
+
traceRect(ctx, { x, y, width, height: LABEL_HEIGHT }, LABEL_RADIUS);
|
|
3220
|
+
ctx.fillStyle = color;
|
|
3221
|
+
ctx.fill();
|
|
3222
|
+
ctx.fillStyle = "#ffffff";
|
|
3223
|
+
ctx.fillText(name, x + LABEL_PADDING_X, y + LABEL_HEIGHT / 2);
|
|
3224
|
+
}
|
|
3225
|
+
function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
|
|
3226
|
+
if (peers.length === 0 && locks.size === 0) return;
|
|
3227
|
+
ctx.save();
|
|
3228
|
+
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3229
|
+
const painted = /* @__PURE__ */ new Set();
|
|
3230
|
+
for (const peer of peers) {
|
|
3231
|
+
const held = /* @__PURE__ */ new Set([...peer.holding, ...peer.transforms.keys()]);
|
|
3232
|
+
for (const id of peer.selection) {
|
|
3233
|
+
if (held.has(id)) continue;
|
|
3234
|
+
const rect = getNodeRect(id);
|
|
3235
|
+
if (!rect) continue;
|
|
3236
|
+
const screen = projectRect(rect, viewport);
|
|
3237
|
+
if (onScreen(screen, size)) drawNodeOutline(ctx, screen, peer.color, "selected");
|
|
3238
|
+
}
|
|
3239
|
+
for (const id of held) {
|
|
3240
|
+
painted.add(id);
|
|
3241
|
+
const rect = getNodeRect(id);
|
|
3242
|
+
if (!rect) continue;
|
|
3243
|
+
const screen = projectRect(rect, viewport);
|
|
3244
|
+
if (onScreen(screen, size)) drawNodeOutline(ctx, screen, peer.color, "held");
|
|
2785
3245
|
}
|
|
2786
|
-
return endAt(pinnedSide);
|
|
2787
3246
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
}
|
|
2798
|
-
function distanceToPolyline(point, points) {
|
|
2799
|
-
let best = Number.POSITIVE_INFINITY;
|
|
2800
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
2801
|
-
const a = points[i];
|
|
2802
|
-
const b = points[i + 1];
|
|
2803
|
-
if (!a || !b) continue;
|
|
2804
|
-
best = Math.min(best, distanceToSegment(point, a, b));
|
|
3247
|
+
if (locks.size > 0) {
|
|
3248
|
+
const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
|
|
3249
|
+
for (const [id, holderId] of locks) {
|
|
3250
|
+
if (painted.has(id)) continue;
|
|
3251
|
+
const rect = getNodeRect(id);
|
|
3252
|
+
if (!rect) continue;
|
|
3253
|
+
const screen = projectRect(rect, viewport);
|
|
3254
|
+
if (!onScreen(screen, size)) continue;
|
|
3255
|
+
drawNodeOutline(ctx, screen, declared.get(holderId) ?? peerColor(holderId), "held");
|
|
3256
|
+
}
|
|
2805
3257
|
}
|
|
2806
|
-
|
|
3258
|
+
ctx.restore();
|
|
2807
3259
|
}
|
|
2808
|
-
function
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3260
|
+
function drawPeerCursors(ctx, peers, { viewport, size }) {
|
|
3261
|
+
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3262
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
3263
|
+
for (const peer of peers) {
|
|
3264
|
+
if (!peer.cursor) continue;
|
|
3265
|
+
const point = worldToScreen(peer.cursor, viewport);
|
|
3266
|
+
if (!onScreen({ ...point, width: 0, height: 0 }, size)) continue;
|
|
3267
|
+
drawCursor(ctx, point, peer.color, peer.name);
|
|
3268
|
+
}
|
|
2816
3269
|
}
|
|
2817
3270
|
|
|
2818
3271
|
// src/renderer/swimlane.ts
|
|
@@ -2845,7 +3298,7 @@ function drawSwimlanes(ctx, width, height, viewport, lanes, style) {
|
|
|
2845
3298
|
}
|
|
2846
3299
|
|
|
2847
3300
|
// src/hooks/use-rivet-runtime.ts
|
|
2848
|
-
var
|
|
3301
|
+
var CULL_MARGIN_PX2 = 240;
|
|
2849
3302
|
var ZOOM_STEP = 1.2;
|
|
2850
3303
|
var WILL_CHANGE_IDLE_MS = 180;
|
|
2851
3304
|
var GUIDE_COLOR = "rgba(236, 72, 153, 0.9)";
|
|
@@ -2890,7 +3343,7 @@ function drawEndpointBubbles(ctx, ends, size) {
|
|
|
2890
3343
|
}
|
|
2891
3344
|
function useRivetRuntime(params) {
|
|
2892
3345
|
const { store, paneRef, nodeContainerRef } = params;
|
|
2893
|
-
const { backgroundCanvasRef, edgeCanvasRef, foregroundCanvasRef } = params;
|
|
3346
|
+
const { backgroundCanvasRef, edgeCanvasRef, foregroundCanvasRef, cursorCanvasRef } = params;
|
|
2894
3347
|
const { swimlaneLanes, scrollToPan, zoomSpeed } = params;
|
|
2895
3348
|
const { minZoom, maxZoom, gridGap, edgeTypes, defaultEdgeOptions, anchorOptions } = params;
|
|
2896
3349
|
const { edgeRenderer: createEdgeRenderer } = params;
|
|
@@ -2929,7 +3382,66 @@ function useRivetRuntime(params) {
|
|
|
2929
3382
|
const size = sizeRef.current;
|
|
2930
3383
|
let frame = 0;
|
|
2931
3384
|
let dirty = true;
|
|
3385
|
+
let cursorsDirty = true;
|
|
2932
3386
|
let hoveredEdgeId = null;
|
|
3387
|
+
let peerPositioned = /* @__PURE__ */ new Set();
|
|
3388
|
+
const positionPeerNodes = (worldPositions) => {
|
|
3389
|
+
const transforms = store.presence.getNodeTransforms();
|
|
3390
|
+
if (transforms.size === 0 && peerPositioned.size === 0) return;
|
|
3391
|
+
const current = /* @__PURE__ */ new Set();
|
|
3392
|
+
for (const id of transforms.keys()) {
|
|
3393
|
+
current.add(id);
|
|
3394
|
+
for (const childId of store.getDescendantIds(id)) current.add(childId);
|
|
3395
|
+
}
|
|
3396
|
+
for (const id of /* @__PURE__ */ new Set([...current, ...peerPositioned])) {
|
|
3397
|
+
const element = store.getNodeElement(id);
|
|
3398
|
+
const world = worldPositions.get(id);
|
|
3399
|
+
if (!element || !world) continue;
|
|
3400
|
+
element.style.transform = `translate(${world.x}px, ${world.y}px)`;
|
|
3401
|
+
const live = transforms.get(id);
|
|
3402
|
+
if (live && !store.isNodeHeld(id)) {
|
|
3403
|
+
element.style.width = `${live.width}px`;
|
|
3404
|
+
element.style.height = `${live.height}px`;
|
|
3405
|
+
} else if (peerPositioned.has(id)) {
|
|
3406
|
+
element.style.width = "";
|
|
3407
|
+
element.style.height = "";
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
peerPositioned = current;
|
|
3411
|
+
};
|
|
3412
|
+
let cursorCanvas = null;
|
|
3413
|
+
let cursorCtx = null;
|
|
3414
|
+
let cursorAllocated = false;
|
|
3415
|
+
const sizeCursorLayer = () => {
|
|
3416
|
+
if (!cursorCanvas || !cursorAllocated) return;
|
|
3417
|
+
cursorCanvas.width = Math.max(1, Math.round(size.width * size.dpr));
|
|
3418
|
+
cursorCanvas.height = Math.max(1, Math.round(size.height * size.dpr));
|
|
3419
|
+
cursorCanvas.style.width = `${size.width}px`;
|
|
3420
|
+
cursorCanvas.style.height = `${size.height}px`;
|
|
3421
|
+
};
|
|
3422
|
+
const drawCursorLayer = () => {
|
|
3423
|
+
const canvas = cursorCanvasRef.current;
|
|
3424
|
+
if (canvas !== cursorCanvas) {
|
|
3425
|
+
cursorCanvas = canvas;
|
|
3426
|
+
cursorCtx = canvas?.getContext("2d") ?? null;
|
|
3427
|
+
cursorAllocated = false;
|
|
3428
|
+
}
|
|
3429
|
+
if (!cursorCtx) return;
|
|
3430
|
+
const peers = store.presence.getPeers();
|
|
3431
|
+
const wanted = peers.some((peer) => peer.cursor);
|
|
3432
|
+
if (!wanted && !cursorAllocated) return;
|
|
3433
|
+
if (!cursorAllocated) {
|
|
3434
|
+
cursorAllocated = true;
|
|
3435
|
+
sizeCursorLayer();
|
|
3436
|
+
}
|
|
3437
|
+
drawPeerCursors(cursorCtx, peers, { viewport: store.getViewport(), size });
|
|
3438
|
+
};
|
|
3439
|
+
const stepPresence = () => {
|
|
3440
|
+
const version = store.presence.getTransformVersion();
|
|
3441
|
+
if (!store.presence.step(performance.now())) return;
|
|
3442
|
+
if (store.presence.getTransformVersion() !== version) schedule();
|
|
3443
|
+
else scheduleCursors();
|
|
3444
|
+
};
|
|
2933
3445
|
const anchorPlacement = (nodeId, anchorId, side) => {
|
|
2934
3446
|
const record = store.anchors.get(handleKey(nodeId, anchorId));
|
|
2935
3447
|
return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
|
|
@@ -2947,6 +3459,7 @@ function useRivetRuntime(params) {
|
|
|
2947
3459
|
});
|
|
2948
3460
|
const render = () => {
|
|
2949
3461
|
zoom.step();
|
|
3462
|
+
stepPresence();
|
|
2950
3463
|
const viewport = store.getViewport();
|
|
2951
3464
|
layerHint.apply(viewportToCss(viewport));
|
|
2952
3465
|
bgCtx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
@@ -2954,6 +3467,7 @@ function useRivetRuntime(params) {
|
|
|
2954
3467
|
drawDotGrid(bgCtx, size.width, size.height, viewport, { gap: gridGap });
|
|
2955
3468
|
drawSwimlanes(bgCtx, size.width, size.height, viewport, swimlaneLanesRef.current);
|
|
2956
3469
|
const worldPositions = store.getWorldPositions();
|
|
3470
|
+
positionPeerNodes(worldPositions);
|
|
2957
3471
|
const edgeList = [...store.edges.values()];
|
|
2958
3472
|
const pending = store.getPending();
|
|
2959
3473
|
const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
|
|
@@ -2982,9 +3496,18 @@ function useRivetRuntime(params) {
|
|
|
2982
3496
|
if (ends) drawEndpointBubbles(fgCtx, ends, size);
|
|
2983
3497
|
}
|
|
2984
3498
|
}
|
|
3499
|
+
if (fgCtx && (!store.presence.isEmpty() || store.locks.size() > 0)) {
|
|
3500
|
+
drawPeerOutlines(fgCtx, store.presence.getPeers(), {
|
|
3501
|
+
viewport,
|
|
3502
|
+
size,
|
|
3503
|
+
getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
|
|
3504
|
+
locks: store.locks.getLocks()
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
drawCursorLayer();
|
|
2985
3508
|
if (edgeList.some((edge) => edge.animated ?? defaultEdgeOptions?.animated)) schedule();
|
|
2986
3509
|
const rect = visibleWorldRect(viewport, size.width, size.height);
|
|
2987
|
-
const margin =
|
|
3510
|
+
const margin = CULL_MARGIN_PX2 / viewport.zoom;
|
|
2988
3511
|
const view = {
|
|
2989
3512
|
x: rect.x - margin,
|
|
2990
3513
|
y: rect.y - margin,
|
|
@@ -3013,16 +3536,29 @@ function useRivetRuntime(params) {
|
|
|
3013
3536
|
};
|
|
3014
3537
|
const tick = () => {
|
|
3015
3538
|
frame = 0;
|
|
3016
|
-
if (
|
|
3017
|
-
|
|
3018
|
-
|
|
3539
|
+
if (dirty) {
|
|
3540
|
+
dirty = false;
|
|
3541
|
+
cursorsDirty = false;
|
|
3542
|
+
render();
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
if (!cursorsDirty) return;
|
|
3546
|
+
cursorsDirty = false;
|
|
3547
|
+
stepPresence();
|
|
3548
|
+
drawCursorLayer();
|
|
3019
3549
|
};
|
|
3020
3550
|
const schedule = () => {
|
|
3021
3551
|
dirty = true;
|
|
3022
3552
|
if (frame) return;
|
|
3023
3553
|
frame = requestAnimationFrame(tick);
|
|
3024
3554
|
};
|
|
3555
|
+
const scheduleCursors = () => {
|
|
3556
|
+
cursorsDirty = true;
|
|
3557
|
+
if (frame) return;
|
|
3558
|
+
frame = requestAnimationFrame(tick);
|
|
3559
|
+
};
|
|
3025
3560
|
store.bindRenderRequester(schedule);
|
|
3561
|
+
store.bindCursorRenderRequester(scheduleCursors);
|
|
3026
3562
|
const setEdgeHover = (id) => {
|
|
3027
3563
|
if (hoveredEdgeId === id) return;
|
|
3028
3564
|
hoveredEdgeId = id;
|
|
@@ -3232,6 +3768,7 @@ function useRivetRuntime(params) {
|
|
|
3232
3768
|
size,
|
|
3233
3769
|
edgeRenderer,
|
|
3234
3770
|
foregroundRenderer,
|
|
3771
|
+
onResize: sizeCursorLayer,
|
|
3235
3772
|
schedule
|
|
3236
3773
|
});
|
|
3237
3774
|
pane.addEventListener("wheel", zoom.onWheel, { passive: false });
|
|
@@ -3248,6 +3785,7 @@ function useRivetRuntime(params) {
|
|
|
3248
3785
|
resizer.dispose();
|
|
3249
3786
|
store.bindRenderRequester(() => {
|
|
3250
3787
|
});
|
|
3788
|
+
store.bindCursorRenderRequester(null);
|
|
3251
3789
|
store.bindReconnectDelegate(null);
|
|
3252
3790
|
unsubscribeFocus();
|
|
3253
3791
|
if (focusRetryFrame) cancelAnimationFrame(focusRetryFrame);
|
|
@@ -3275,6 +3813,7 @@ function useRivetRuntime(params) {
|
|
|
3275
3813
|
backgroundCanvasRef,
|
|
3276
3814
|
edgeCanvasRef,
|
|
3277
3815
|
foregroundCanvasRef,
|
|
3816
|
+
cursorCanvasRef,
|
|
3278
3817
|
scrollToPan,
|
|
3279
3818
|
zoomSpeed,
|
|
3280
3819
|
minZoom,
|
|
@@ -3668,7 +4207,7 @@ var labelStyle2 = {
|
|
|
3668
4207
|
whiteSpace: "nowrap",
|
|
3669
4208
|
pointerEvents: "none"
|
|
3670
4209
|
};
|
|
3671
|
-
function
|
|
4210
|
+
function sameIds2(a, b) {
|
|
3672
4211
|
if (a.length !== b.length) return false;
|
|
3673
4212
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
3674
4213
|
return true;
|
|
@@ -3694,7 +4233,7 @@ function EdgeLabelLayer() {
|
|
|
3694
4233
|
return store.subscribeFrame(() => {
|
|
3695
4234
|
const anchors = store.getEdgeLabelAnchors();
|
|
3696
4235
|
const next = [...anchors.keys()];
|
|
3697
|
-
setIds((prev) =>
|
|
4236
|
+
setIds((prev) => sameIds2(prev, next) ? prev : next);
|
|
3698
4237
|
position();
|
|
3699
4238
|
});
|
|
3700
4239
|
}, [store, position]);
|
|
@@ -3730,6 +4269,8 @@ function startGroupDrag(params) {
|
|
|
3730
4269
|
let started = false;
|
|
3731
4270
|
let starts = /* @__PURE__ */ new Map();
|
|
3732
4271
|
let affected = /* @__PURE__ */ new Set();
|
|
4272
|
+
let initiator;
|
|
4273
|
+
let anchor;
|
|
3733
4274
|
const repositionAffected = () => {
|
|
3734
4275
|
for (const affectedId of affected) {
|
|
3735
4276
|
const affectedEl = store.getNodeElement(affectedId);
|
|
@@ -3740,8 +4281,9 @@ function startGroupDrag(params) {
|
|
|
3740
4281
|
onFrame?.();
|
|
3741
4282
|
};
|
|
3742
4283
|
const beginDrag = () => {
|
|
4284
|
+
const movers = store.getMovers(store.getSelectedNodes()).filter((moverId) => store.locks.allows(moverId, "drag"));
|
|
4285
|
+
if (movers.length === 0) return false;
|
|
3743
4286
|
started = true;
|
|
3744
|
-
const movers = store.getMovers(store.getSelectedNodes());
|
|
3745
4287
|
starts = /* @__PURE__ */ new Map();
|
|
3746
4288
|
for (const moverId of movers) {
|
|
3747
4289
|
const moverNode = store.nodes.get(moverId);
|
|
@@ -3753,17 +4295,42 @@ function startGroupDrag(params) {
|
|
|
3753
4295
|
for (const descendant of store.getDescendantIds(moverId)) affected.add(descendant);
|
|
3754
4296
|
}
|
|
3755
4297
|
store.setNodeDragging(true);
|
|
4298
|
+
initiator = params.nodeId ?? movers[0];
|
|
4299
|
+
anchor = movers[0];
|
|
4300
|
+
if (initiator) store.beginNodeGesture(initiator, movers);
|
|
3756
4301
|
el.setPointerCapture(pointerId);
|
|
3757
4302
|
el.style.cursor = "grabbing";
|
|
3758
4303
|
window.getSelection()?.removeAllRanges();
|
|
3759
4304
|
document.body.style.userSelect = "none";
|
|
3760
4305
|
document.body.style.webkitUserSelect = "none";
|
|
4306
|
+
return true;
|
|
4307
|
+
};
|
|
4308
|
+
const teardown = () => {
|
|
4309
|
+
window.removeEventListener("pointermove", onMove);
|
|
4310
|
+
window.removeEventListener("pointerup", onUp);
|
|
4311
|
+
if (!started) return;
|
|
4312
|
+
started = false;
|
|
4313
|
+
el.style.cursor = "grab";
|
|
4314
|
+
document.body.style.userSelect = "";
|
|
4315
|
+
document.body.style.webkitUserSelect = "";
|
|
4316
|
+
store.setAlignmentGuides([]);
|
|
4317
|
+
store.setNodeDragging(false);
|
|
4318
|
+
if (el.hasPointerCapture(pointerId)) el.releasePointerCapture(pointerId);
|
|
3761
4319
|
};
|
|
4320
|
+
const yielded = () => anchor !== void 0 && !store.isNodeHeld(anchor);
|
|
3762
4321
|
const onMove = (moveEvent) => {
|
|
3763
4322
|
if (!started) {
|
|
3764
4323
|
const moved = Math.hypot(moveEvent.clientX - originX, moveEvent.clientY - originY);
|
|
3765
4324
|
if (moved < DRAG_THRESHOLD) return;
|
|
3766
|
-
beginDrag()
|
|
4325
|
+
if (!beginDrag()) {
|
|
4326
|
+
window.removeEventListener("pointermove", onMove);
|
|
4327
|
+
window.removeEventListener("pointerup", onUp);
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
if (yielded()) {
|
|
4332
|
+
teardown();
|
|
4333
|
+
return;
|
|
3767
4334
|
}
|
|
3768
4335
|
const dx = (moveEvent.clientX - originX) / zoom;
|
|
3769
4336
|
const dy = (moveEvent.clientY - originY) / zoom;
|
|
@@ -3798,6 +4365,10 @@ function startGroupDrag(params) {
|
|
|
3798
4365
|
window.removeEventListener("pointermove", onMove);
|
|
3799
4366
|
window.removeEventListener("pointerup", onUp);
|
|
3800
4367
|
if (!started) return;
|
|
4368
|
+
if (yielded()) {
|
|
4369
|
+
teardown();
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
3801
4372
|
el.style.cursor = "grab";
|
|
3802
4373
|
document.body.style.userSelect = "";
|
|
3803
4374
|
document.body.style.webkitUserSelect = "";
|
|
@@ -3824,6 +4395,7 @@ function startGroupDrag(params) {
|
|
|
3824
4395
|
}
|
|
3825
4396
|
store.moveNode(moverId, position, true);
|
|
3826
4397
|
}
|
|
4398
|
+
if (initiator) store.endNodeGesture(initiator, [...starts.keys()]);
|
|
3827
4399
|
repositionAffected();
|
|
3828
4400
|
};
|
|
3829
4401
|
window.addEventListener("pointermove", onMove);
|
|
@@ -3951,7 +4523,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
3951
4523
|
return node2?.ariaLabel ?? `node ${id}`;
|
|
3952
4524
|
};
|
|
3953
4525
|
const grab = () => {
|
|
3954
|
-
const movers = store.getMovers(store.isNodeSelected(id) ? store.getSelectedNodes() : [id]);
|
|
4526
|
+
const movers = store.getMovers(store.isNodeSelected(id) ? store.getSelectedNodes() : [id]).filter((moverId) => store.locks.allows(moverId, "drag"));
|
|
4527
|
+
if (movers.length === 0) {
|
|
4528
|
+
announce(`${nodeName()} is locked by someone else.`);
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
3955
4531
|
const origins = /* @__PURE__ */ new Map();
|
|
3956
4532
|
for (const moverId of movers) {
|
|
3957
4533
|
const mover = store.nodes.get(moverId);
|
|
@@ -3959,6 +4535,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
3959
4535
|
}
|
|
3960
4536
|
grabOrigins.current = origins;
|
|
3961
4537
|
setGrabbed(true);
|
|
4538
|
+
store.beginNodeGesture(id, movers);
|
|
3962
4539
|
announce(`Grabbed ${nodeName()}. Use the arrow keys to move, Enter to drop, Escape to cancel.`);
|
|
3963
4540
|
};
|
|
3964
4541
|
const releaseGrab = (revert) => {
|
|
@@ -3968,9 +4545,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
3968
4545
|
if (!origins) return;
|
|
3969
4546
|
if (revert) {
|
|
3970
4547
|
for (const [moverId, origin] of origins) store.moveNode(moverId, origin, true);
|
|
4548
|
+
store.endNodeGesture(id, [...origins.keys()]);
|
|
3971
4549
|
announce("Move cancelled.");
|
|
3972
4550
|
return;
|
|
3973
4551
|
}
|
|
4552
|
+
store.endNodeGesture(id, [...origins.keys()]);
|
|
3974
4553
|
const node2 = store.nodes.get(id);
|
|
3975
4554
|
if (node2) {
|
|
3976
4555
|
const { x, y } = node2.position;
|
|
@@ -3983,6 +4562,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
3983
4562
|
);
|
|
3984
4563
|
const getSnapshot = useCallback(() => store.getNodeVersion(id), [store, id]);
|
|
3985
4564
|
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
4565
|
+
const lockHolder = useSyncExternalStore(
|
|
4566
|
+
store.locks.subscribe,
|
|
4567
|
+
useCallback(() => store.locks.getHolder(id), [store, id]),
|
|
4568
|
+
useCallback(() => store.locks.getHolder(id), [store, id])
|
|
4569
|
+
);
|
|
3986
4570
|
const nodeContext = useMemo(() => ({ nodeId: id, wrapperRef: ref }), [id]);
|
|
3987
4571
|
useEffect(() => {
|
|
3988
4572
|
const el = ref.current;
|
|
@@ -4015,6 +4599,13 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4015
4599
|
if (event.target !== event.currentTarget) return;
|
|
4016
4600
|
const node2 = store.nodes.get(id);
|
|
4017
4601
|
if (!node2 || node2.selectable === false) return;
|
|
4602
|
+
if (grabOrigins.current && !store.isNodeHeld(id)) {
|
|
4603
|
+
grabOrigins.current = null;
|
|
4604
|
+
setGrabbed(false);
|
|
4605
|
+
announce(`${nodeName()} was taken by someone else.`);
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
if (!store.locks.allows(id, "select")) return;
|
|
4018
4609
|
if (event.key === "Enter" || event.key === " ") {
|
|
4019
4610
|
event.preventDefault();
|
|
4020
4611
|
if (grabOrigins.current) {
|
|
@@ -4067,8 +4658,8 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4067
4658
|
const node2 = store.nodes.get(id);
|
|
4068
4659
|
if (!el || !node2) return;
|
|
4069
4660
|
const target = event.target instanceof Element ? event.target : null;
|
|
4070
|
-
const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect));
|
|
4071
|
-
const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag));
|
|
4661
|
+
const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect)) || !store.locks.allows(id, "select");
|
|
4662
|
+
const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag)) || !store.locks.allows(id, "drag");
|
|
4072
4663
|
const additive = keyHeld(event, multiSelectionKeys);
|
|
4073
4664
|
if (additive) {
|
|
4074
4665
|
if (!noSelect) store.selectNode(id, true);
|
|
@@ -4078,6 +4669,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4078
4669
|
if (noDrag) return;
|
|
4079
4670
|
startGroupDrag({
|
|
4080
4671
|
store,
|
|
4672
|
+
nodeId: id,
|
|
4081
4673
|
el,
|
|
4082
4674
|
event,
|
|
4083
4675
|
snapGrid,
|
|
@@ -4095,6 +4687,9 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4095
4687
|
const Component = nodeTypes[key] ?? BUILTIN_NODE_TYPES[key] ?? DefaultNode;
|
|
4096
4688
|
const world = store.getNodeWorldPosition(id);
|
|
4097
4689
|
const depth = store.getNodeDepth(id);
|
|
4690
|
+
const locked = lockHolder !== null;
|
|
4691
|
+
const lockedForSelect = locked && !store.locks.allows(id, "select");
|
|
4692
|
+
const lockedForDrag = locked && !store.locks.allows(id, "drag");
|
|
4098
4693
|
return (
|
|
4099
4694
|
// biome-ignore lint/a11y/useSemanticElements: a node wraps arbitrary (often interactive) content, so it can't be a native <button>
|
|
4100
4695
|
/* @__PURE__ */ jsx(
|
|
@@ -4103,12 +4698,14 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4103
4698
|
ref,
|
|
4104
4699
|
"data-rivet-node": id,
|
|
4105
4700
|
"data-rivet-grabbed": grabbed ? "" : void 0,
|
|
4106
|
-
|
|
4701
|
+
"data-rivet-locked": locked ? lockHolder ?? "" : void 0,
|
|
4702
|
+
tabIndex: node.selectable === false || lockedForSelect ? -1 : 0,
|
|
4107
4703
|
role: "button",
|
|
4108
4704
|
"aria-roledescription": "graph node",
|
|
4109
4705
|
"aria-pressed": Boolean(node.selected),
|
|
4110
4706
|
"aria-label": node.ariaLabel,
|
|
4111
|
-
"aria-
|
|
4707
|
+
"aria-disabled": lockedForSelect || void 0,
|
|
4708
|
+
"aria-describedby": node.selectable === false || lockedForSelect ? void 0 : nodeDescriptionId,
|
|
4112
4709
|
onPointerDown,
|
|
4113
4710
|
onPointerEnter,
|
|
4114
4711
|
onPointerLeave,
|
|
@@ -4117,6 +4714,7 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4117
4714
|
onBlur,
|
|
4118
4715
|
style: {
|
|
4119
4716
|
...wrapperStyle,
|
|
4717
|
+
...lockedForDrag ? { cursor: "not-allowed" } : {},
|
|
4120
4718
|
transform: `translate(${world.x}px, ${world.y}px)`,
|
|
4121
4719
|
zIndex: depth,
|
|
4122
4720
|
// Explicit dimensions from NodeResizer override the intrinsic size.
|
|
@@ -4494,6 +5092,12 @@ function Rivet({
|
|
|
4494
5092
|
snapGrid,
|
|
4495
5093
|
alignmentGuides = false,
|
|
4496
5094
|
anchorOptions,
|
|
5095
|
+
peers,
|
|
5096
|
+
onLocalPresence,
|
|
5097
|
+
presenceOptions,
|
|
5098
|
+
lockedNodes,
|
|
5099
|
+
canInteractWithLocked,
|
|
5100
|
+
onNodeLockConflict,
|
|
4497
5101
|
panOnDrag = true,
|
|
4498
5102
|
selectionOnDrag = false,
|
|
4499
5103
|
selectionKeyCode = "Shift",
|
|
@@ -4521,6 +5125,8 @@ function Rivet({
|
|
|
4521
5125
|
onReconnect,
|
|
4522
5126
|
onReconnectStart,
|
|
4523
5127
|
onReconnectEnd,
|
|
5128
|
+
onNodeDragStart,
|
|
5129
|
+
onNodeDragEnd,
|
|
4524
5130
|
onSelectionChange,
|
|
4525
5131
|
onFocusChange,
|
|
4526
5132
|
className,
|
|
@@ -4543,6 +5149,7 @@ function Rivet({
|
|
|
4543
5149
|
snapGrid,
|
|
4544
5150
|
swimlaneMargin,
|
|
4545
5151
|
anchorOptions,
|
|
5152
|
+
presenceOptions,
|
|
4546
5153
|
panOnDrag,
|
|
4547
5154
|
selectionKeyCode,
|
|
4548
5155
|
multiSelectionKeyCode,
|
|
@@ -4583,14 +5190,28 @@ function Rivet({
|
|
|
4583
5190
|
const hasEdgeChange = Boolean(onEdgesChange);
|
|
4584
5191
|
useEffect(() => {
|
|
4585
5192
|
store.setChangeHandlers({
|
|
4586
|
-
nodes: hasNodeChange ? (changes) => onNodesChangeRef.current?.(changes) : void 0,
|
|
4587
|
-
edges: hasEdgeChange ? (changes) => onEdgesChangeRef.current?.(changes) : void 0
|
|
5193
|
+
nodes: hasNodeChange ? (changes, meta) => onNodesChangeRef.current?.(changes, meta) : void 0,
|
|
5194
|
+
edges: hasEdgeChange ? (changes, meta) => onEdgesChangeRef.current?.(changes, meta) : void 0
|
|
4588
5195
|
});
|
|
4589
5196
|
return () => store.setChangeHandlers({});
|
|
4590
5197
|
}, [store, hasNodeChange, hasEdgeChange]);
|
|
5198
|
+
const onNodeDragStartRef = useRef(onNodeDragStart);
|
|
5199
|
+
onNodeDragStartRef.current = onNodeDragStart;
|
|
5200
|
+
const onNodeDragEndRef = useRef(onNodeDragEnd);
|
|
5201
|
+
onNodeDragEndRef.current = onNodeDragEnd;
|
|
5202
|
+
const onNodeLockConflictRef = useRef(onNodeLockConflict);
|
|
5203
|
+
onNodeLockConflictRef.current = onNodeLockConflict;
|
|
5204
|
+
useEffect(() => {
|
|
5205
|
+
store.setGestureHandlers({
|
|
5206
|
+
start: (event) => onNodeDragStartRef.current?.(event),
|
|
5207
|
+
end: (event) => onNodeDragEndRef.current?.(event),
|
|
5208
|
+
conflict: (event) => onNodeLockConflictRef.current?.(event)
|
|
5209
|
+
});
|
|
5210
|
+
return () => store.setGestureHandlers({});
|
|
5211
|
+
}, [store]);
|
|
4591
5212
|
const isControlled = controlledNodes !== void 0 || controlledEdges !== void 0;
|
|
4592
5213
|
useEffect(() => {
|
|
4593
|
-
if (!isControlled
|
|
5214
|
+
if (!isControlled) return;
|
|
4594
5215
|
store.reconcile(
|
|
4595
5216
|
controlledNodes ?? [...store.nodes.values()],
|
|
4596
5217
|
controlledEdges ?? [...store.edges.values()]
|
|
@@ -4643,6 +5264,24 @@ function Rivet({
|
|
|
4643
5264
|
const backgroundCanvasRef = useRef(null);
|
|
4644
5265
|
const edgeCanvasRef = useRef(null);
|
|
4645
5266
|
const foregroundCanvasRef = useRef(null);
|
|
5267
|
+
const cursorCanvasRef = useRef(null);
|
|
5268
|
+
useEffect(() => {
|
|
5269
|
+
if (peers === void 0) return;
|
|
5270
|
+
store.presence.setPeers(peers);
|
|
5271
|
+
}, [store, peers]);
|
|
5272
|
+
const canInteractWithLockedRef = useRef(canInteractWithLocked);
|
|
5273
|
+
canInteractWithLockedRef.current = canInteractWithLocked;
|
|
5274
|
+
const hasLockPolicy = Boolean(canInteractWithLocked);
|
|
5275
|
+
useEffect(() => {
|
|
5276
|
+
store.locks.setPolicy(
|
|
5277
|
+
hasLockPolicy ? (event) => canInteractWithLockedRef.current?.(event) ?? true : null
|
|
5278
|
+
);
|
|
5279
|
+
return () => store.locks.setPolicy(null);
|
|
5280
|
+
}, [store, hasLockPolicy]);
|
|
5281
|
+
useEffect(() => {
|
|
5282
|
+
store.setLockedNodes(lockedNodes);
|
|
5283
|
+
}, [store, lockedNodes]);
|
|
5284
|
+
useLocalPresence({ store, paneRef, onLocalPresence, throttleMs: cfg.presenceOptions.throttleMs });
|
|
4646
5285
|
const { visibleIds, controls, getViewportElements } = useRivetRuntime({
|
|
4647
5286
|
store,
|
|
4648
5287
|
paneRef,
|
|
@@ -4650,6 +5289,7 @@ function Rivet({
|
|
|
4650
5289
|
backgroundCanvasRef,
|
|
4651
5290
|
edgeCanvasRef,
|
|
4652
5291
|
foregroundCanvasRef,
|
|
5292
|
+
cursorCanvasRef,
|
|
4653
5293
|
swimlaneLanes: swimlanes.lanes,
|
|
4654
5294
|
edgeTypes: cfg.edgeTypes,
|
|
4655
5295
|
defaultEdgeOptions: cfg.defaultEdgeOptions,
|
|
@@ -4742,6 +5382,7 @@ function Rivet({
|
|
|
4742
5382
|
/* @__PURE__ */ jsx("canvas", { ref: edgeCanvasRef, style: canvasStyle }),
|
|
4743
5383
|
/* @__PURE__ */ jsx(NodeLayer, { containerRef: nodeContainerRef, visibleIds }),
|
|
4744
5384
|
/* @__PURE__ */ jsx("canvas", { ref: foregroundCanvasRef, style: canvasStyle }),
|
|
5385
|
+
cfg.presenceOptions.renderCursors && /* @__PURE__ */ jsx("canvas", { ref: cursorCanvasRef, style: canvasStyle }),
|
|
4745
5386
|
/* @__PURE__ */ jsx(EdgeLabelLayer, {}),
|
|
4746
5387
|
/* @__PURE__ */ jsx(SwimlaneOverlay, {}),
|
|
4747
5388
|
children,
|
|
@@ -4801,6 +5442,10 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
4801
5442
|
),
|
|
4802
5443
|
getNodes,
|
|
4803
5444
|
getNode: (id) => store.nodes.get(id),
|
|
5445
|
+
// The store's own `getNodeRect` answers for any id, falling back to the
|
|
5446
|
+
// origin and the default size; a public caller needs to tell "it's there,
|
|
5447
|
+
// at 0,0" from "it isn't there".
|
|
5448
|
+
getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
|
|
4804
5449
|
setNodes,
|
|
4805
5450
|
addNodes: (nodes) => {
|
|
4806
5451
|
for (const node of Array.isArray(nodes) ? nodes : [nodes]) store.addNode(node);
|
|
@@ -4825,6 +5470,7 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
4825
5470
|
for (const id of edges ?? []) store.removeEdge(id);
|
|
4826
5471
|
for (const id of nodes ?? []) store.removeNode(id);
|
|
4827
5472
|
},
|
|
5473
|
+
applyRemote: (changes) => store.applyRemote(changes),
|
|
4828
5474
|
registerAnchor: (nodeId, anchorId, element, options) => {
|
|
4829
5475
|
store.registerAnchor(nodeId, anchorId, element, options);
|
|
4830
5476
|
return {
|
|
@@ -4834,6 +5480,10 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
4834
5480
|
},
|
|
4835
5481
|
unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
|
|
4836
5482
|
remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
|
|
5483
|
+
setPeerCursor: (peerId, point) => store.presence.setPeerCursor(peerId, point),
|
|
5484
|
+
setPeerNodeTransform: (peerId, nodeId, rect) => store.presence.setPeerNodeTransform(peerId, nodeId, rect),
|
|
5485
|
+
removePeer: (peerId) => store.presence.removePeer(peerId),
|
|
5486
|
+
releaseNodeGesture: (id, ids) => store.endNodeGesture(id, ids),
|
|
4837
5487
|
copy: (ids) => {
|
|
4838
5488
|
const targetIds = ids ?? store.getSelectedNodes();
|
|
4839
5489
|
if (targetIds.length === 0) return;
|