@kolosal-ai/rivet 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anchor/index.d.ts +31 -4
- package/dist/anchor/index.js +22 -6
- package/dist/anchor/index.js.map +1 -1
- package/dist/chunk-TT2N7KWU.js +676 -0
- package/dist/chunk-TT2N7KWU.js.map +1 -0
- package/dist/{chunk-AXT35ZJV.js → chunk-W5HBJ4VF.js} +25 -3
- package/dist/chunk-W5HBJ4VF.js.map +1 -0
- package/dist/index.d.ts +159 -24
- package/dist/index.js +644 -149
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +158 -4
- package/dist/presence/index.js +24 -2
- package/dist/presence/index.js.map +1 -1
- package/dist/{registry-t5ldbviG.d.ts → registry-Z_oiPyjm.d.ts} +43 -2
- package/dist/svg/index.d.ts +18 -3
- package/dist/svg/index.js +5 -2
- package/dist/svg/index.js.map +1 -1
- package/dist/swimlane/index.d.ts +2 -2
- package/dist/{types-CQqnm4yW.d.ts → types-D3aldmsO.d.ts} +27 -1
- package/dist/use-node-lock-CbS59CRE.d.ts +716 -0
- package/package.json +1 -1
- package/dist/chunk-AXT35ZJV.js.map +0 -1
- package/dist/chunk-RG5ALD3X.js +0 -306
- package/dist/chunk-RG5ALD3X.js.map +0 -1
- package/dist/use-node-lock-Csg9pUeC.d.ts +0 -323
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId, strayPlacementPct, anchorHandleId, anchorAutoHandleId, parseAnchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS } from './chunk-
|
|
2
|
-
import { createPresenceRegistry, createLockRegistry, DEFAULT_PRESENCE_OPTIONS, peerColor } from './chunk-
|
|
3
|
-
export { LOCK_DEFAULT_REFUSED, useNodeLock, useNodeLockAllows } from './chunk-
|
|
4
|
-
import { clamp, clampNodeToLanes, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
|
|
1
|
+
import { findSelectableAnchor, parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId, strayPlacementPct, anchorHandleId, anchorAutoHandleId, parseAnchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS, anchorRect } from './chunk-W5HBJ4VF.js';
|
|
2
|
+
import { snapToGrid, boundingRect, clampChildToParent, anchorTargetKey, createPresenceRegistry, createLockRegistry, nodeControlledEqual, buildChildIndex, collectDescendants, worldPosition, nodeDepth, descendantIds, hasAncestorIn, edgeTargetKey, DEFAULT_PRESENCE_OPTIONS, collectPeerOutlines, peerOutlinesEqual, usePeerIdentities, serializeGraph, presenceTargetKey, peerColor, isNodeTarget, presenceTargetEdgeId } from './chunk-TT2N7KWU.js';
|
|
3
|
+
export { LOCK_DEFAULT_REFUSED, boundingRect, clampChildToParent, serializeGraph, snapToGrid, useNodeLock, useNodeLockAllows } from './chunk-TT2N7KWU.js';
|
|
5
4
|
import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
6
5
|
export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
6
|
+
import { clamp, clampNodeToLanes, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
import { memo, useRef, useState, useCallback, useSyncExternalStore, useMemo, useEffect, Fragment as Fragment$1, useId, useLayoutEffect } from 'react';
|
|
9
9
|
|
|
@@ -282,6 +282,7 @@ var DEFAULT_NODE_SIZE = { width: 160, height: 44 };
|
|
|
282
282
|
function handleKey(nodeId, handleId) {
|
|
283
283
|
return `${nodeId} ${handleId}`;
|
|
284
284
|
}
|
|
285
|
+
var DRAG_THRESHOLD = 4;
|
|
285
286
|
var SELECTOR = {
|
|
286
287
|
node: "[data-rivet-node]",
|
|
287
288
|
handle: "[data-rivet-handle]",
|
|
@@ -387,111 +388,6 @@ function guardNodeChanges(changes, isHeld, getLocal) {
|
|
|
387
388
|
return guarded;
|
|
388
389
|
}
|
|
389
390
|
|
|
390
|
-
// src/graph.ts
|
|
391
|
-
function clampChildToParent(position, childSize, parentSize) {
|
|
392
|
-
const maxX = Math.max(0, parentSize.width - childSize.width);
|
|
393
|
-
const maxY = Math.max(0, parentSize.height - childSize.height);
|
|
394
|
-
return {
|
|
395
|
-
x: Math.min(maxX, Math.max(0, position.x)),
|
|
396
|
-
y: Math.min(maxY, Math.max(0, position.y))
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function snapToGrid(position, grid) {
|
|
400
|
-
const [gx, gy] = grid;
|
|
401
|
-
return {
|
|
402
|
-
x: gx > 0 ? Math.round(position.x / gx) * gx : position.x,
|
|
403
|
-
y: gy > 0 ? Math.round(position.y / gy) * gy : position.y
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
function boundingRect(rects) {
|
|
407
|
-
if (rects.length === 0) return null;
|
|
408
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
409
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
410
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
411
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
412
|
-
for (const r of rects) {
|
|
413
|
-
minX = Math.min(minX, r.x);
|
|
414
|
-
minY = Math.min(minY, r.y);
|
|
415
|
-
maxX = Math.max(maxX, r.x + r.width);
|
|
416
|
-
maxY = Math.max(maxY, r.y + r.height);
|
|
417
|
-
}
|
|
418
|
-
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
419
|
-
}
|
|
420
|
-
var MAX_PARENT_DEPTH = 50;
|
|
421
|
-
function worldPosition(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
|
|
422
|
-
const node = nodes.get(id);
|
|
423
|
-
if (!node) return { x: 0, y: 0 };
|
|
424
|
-
let x = node.position.x;
|
|
425
|
-
let y = node.position.y;
|
|
426
|
-
let parentId = node.parentId;
|
|
427
|
-
let guard = 0;
|
|
428
|
-
while (parentId && guard++ < maxDepth) {
|
|
429
|
-
const parent = nodes.get(parentId);
|
|
430
|
-
if (!parent) break;
|
|
431
|
-
x += parent.position.x;
|
|
432
|
-
y += parent.position.y;
|
|
433
|
-
parentId = parent.parentId;
|
|
434
|
-
}
|
|
435
|
-
return { x, y };
|
|
436
|
-
}
|
|
437
|
-
function nodeDepth(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
|
|
438
|
-
let depth = 0;
|
|
439
|
-
let parentId = nodes.get(id)?.parentId;
|
|
440
|
-
while (parentId && depth < maxDepth) {
|
|
441
|
-
depth++;
|
|
442
|
-
parentId = nodes.get(parentId)?.parentId;
|
|
443
|
-
}
|
|
444
|
-
return depth;
|
|
445
|
-
}
|
|
446
|
-
function hasAncestorIn(nodes, id, set, maxDepth = MAX_PARENT_DEPTH) {
|
|
447
|
-
let parentId = nodes.get(id)?.parentId;
|
|
448
|
-
let guard = 0;
|
|
449
|
-
while (parentId && guard++ < maxDepth) {
|
|
450
|
-
if (set.has(parentId)) return true;
|
|
451
|
-
parentId = nodes.get(parentId)?.parentId;
|
|
452
|
-
}
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
function buildChildIndex(nodes) {
|
|
456
|
-
const childrenOf = /* @__PURE__ */ new Map();
|
|
457
|
-
for (const node of nodes.values()) {
|
|
458
|
-
if (!node.parentId) continue;
|
|
459
|
-
const list = childrenOf.get(node.parentId);
|
|
460
|
-
if (list) list.push(node.id);
|
|
461
|
-
else childrenOf.set(node.parentId, [node.id]);
|
|
462
|
-
}
|
|
463
|
-
return childrenOf;
|
|
464
|
-
}
|
|
465
|
-
function collectDescendants(id, childIndex, out) {
|
|
466
|
-
for (const child of childIndex.get(id) ?? []) {
|
|
467
|
-
out.push(child);
|
|
468
|
-
collectDescendants(child, childIndex, out);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
function descendantIds(nodes, id) {
|
|
472
|
-
const out = [];
|
|
473
|
-
collectDescendants(id, buildChildIndex(nodes), out);
|
|
474
|
-
return out;
|
|
475
|
-
}
|
|
476
|
-
function serializeGraph(nodes, edges, viewport) {
|
|
477
|
-
const cleanNodes = [];
|
|
478
|
-
for (const node of nodes) {
|
|
479
|
-
const copy = { ...node, position: { ...node.position } };
|
|
480
|
-
if (node.size) copy.size = { ...node.size };
|
|
481
|
-
copy.hovered = void 0;
|
|
482
|
-
copy.dragging = void 0;
|
|
483
|
-
cleanNodes.push(copy);
|
|
484
|
-
}
|
|
485
|
-
return {
|
|
486
|
-
nodes: cleanNodes,
|
|
487
|
-
edges: [...edges].map((edge) => ({ ...edge })),
|
|
488
|
-
viewport: { ...viewport }
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
function nodeControlledEqual(a, b) {
|
|
492
|
-
return a.position.x === b.position.x && a.position.y === b.position.y && Boolean(a.selected) === Boolean(b.selected) && a.data === b.data && a.type === b.type && a.laneId === b.laneId && a.ariaLabel === b.ariaLabel && a.parentId === b.parentId && a.extent === b.extent && a.width === b.width && a.height === b.height && Boolean(a.dragging) === Boolean(b.dragging) && a.size?.width === b.size?.width && a.size?.height === b.size?.height;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
391
|
// src/history.ts
|
|
496
392
|
function hasRecordableChange(changes) {
|
|
497
393
|
return changes.some((change) => {
|
|
@@ -997,6 +893,30 @@ function resolveEnd(edge, role, node, peer, frame) {
|
|
|
997
893
|
extras?.displayedSides?.set(key, side);
|
|
998
894
|
return endAt(side);
|
|
999
895
|
}
|
|
896
|
+
function polylineMidpoint(points) {
|
|
897
|
+
const first = points[0];
|
|
898
|
+
if (!first) return { x: 0, y: 0 };
|
|
899
|
+
let total = 0;
|
|
900
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
901
|
+
const a = points[i];
|
|
902
|
+
const b = points[i + 1];
|
|
903
|
+
if (a && b) total += Math.hypot(b.x - a.x, b.y - a.y);
|
|
904
|
+
}
|
|
905
|
+
let travelled = 0;
|
|
906
|
+
const half = total / 2;
|
|
907
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
908
|
+
const a = points[i];
|
|
909
|
+
const b = points[i + 1];
|
|
910
|
+
if (!a || !b) continue;
|
|
911
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
912
|
+
if (travelled + length >= half) {
|
|
913
|
+
const t = length === 0 ? 0 : (half - travelled) / length;
|
|
914
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
915
|
+
}
|
|
916
|
+
travelled += length;
|
|
917
|
+
}
|
|
918
|
+
return points[points.length - 1] ?? first;
|
|
919
|
+
}
|
|
1000
920
|
function distanceToPolyline(point, points) {
|
|
1001
921
|
let best = Number.POSITIVE_INFINITY;
|
|
1002
922
|
for (let i = 0; i < points.length - 1; i++) {
|
|
@@ -1025,8 +945,14 @@ var RivetGraphStore = class {
|
|
|
1025
945
|
anchors = /* @__PURE__ */ new Map();
|
|
1026
946
|
// Arrow-wrapped so the (later-initialized) requestRender field is looked up
|
|
1027
947
|
// at call time, not while this field is being constructed.
|
|
1028
|
-
presence = createPresenceRegistry(
|
|
1029
|
-
|
|
948
|
+
presence = createPresenceRegistry({
|
|
949
|
+
requestRender: () => this.requestRender(),
|
|
950
|
+
requestCursorRender: () => this.requestCursorRender()
|
|
951
|
+
});
|
|
952
|
+
locks = createLockRegistry(
|
|
953
|
+
() => this.requestRender(),
|
|
954
|
+
(id) => this.nodes.get(id)?.parentId
|
|
955
|
+
);
|
|
1030
956
|
viewport;
|
|
1031
957
|
viewportClamp = null;
|
|
1032
958
|
pending = null;
|
|
@@ -1039,6 +965,8 @@ var RivetGraphStore = class {
|
|
|
1039
965
|
nodeElements = /* @__PURE__ */ new Map();
|
|
1040
966
|
viewportListeners = /* @__PURE__ */ new Set();
|
|
1041
967
|
frameListeners = /* @__PURE__ */ new Set();
|
|
968
|
+
cursorFrameListeners = /* @__PURE__ */ new Set();
|
|
969
|
+
connectionListeners = /* @__PURE__ */ new Set();
|
|
1042
970
|
selectionListeners = /* @__PURE__ */ new Set();
|
|
1043
971
|
selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1044
972
|
selectionBoxActive = false;
|
|
@@ -1048,6 +976,11 @@ var RivetGraphStore = class {
|
|
|
1048
976
|
focusedNodeId = null;
|
|
1049
977
|
focusListeners = /* @__PURE__ */ new Set();
|
|
1050
978
|
selectedEdgeId = null;
|
|
979
|
+
/** Selected anchors, keyed like the registry so lookups are one hash. */
|
|
980
|
+
selectedAnchors = /* @__PURE__ */ new Map();
|
|
981
|
+
selectedAnchorSnapshot = [];
|
|
982
|
+
selectedAnchorsStale = false;
|
|
983
|
+
anchorSelectionListeners = /* @__PURE__ */ new Set();
|
|
1051
984
|
nodeDragging = false;
|
|
1052
985
|
nodeResizing = false;
|
|
1053
986
|
edgeAlignment = "manual";
|
|
@@ -1059,6 +992,8 @@ var RivetGraphStore = class {
|
|
|
1059
992
|
lockConflictHandler = null;
|
|
1060
993
|
displayedSides = /* @__PURE__ */ new Map();
|
|
1061
994
|
edgeLabelAnchors = /* @__PURE__ */ new Map();
|
|
995
|
+
/** Screen-space points on claimed edges, published only while DOM outlines are on. */
|
|
996
|
+
peerEdgeAnchors = /* @__PURE__ */ new Map();
|
|
1062
997
|
alignmentGuides = [];
|
|
1063
998
|
nodeChangeHandler = null;
|
|
1064
999
|
edgeChangeHandler = null;
|
|
@@ -1072,6 +1007,12 @@ var RivetGraphStore = class {
|
|
|
1072
1007
|
peerWorldVersion = -1;
|
|
1073
1008
|
renderRequester = () => {
|
|
1074
1009
|
};
|
|
1010
|
+
/**
|
|
1011
|
+
* Null until a runtime claims it, and then `requestCursorRender` narrows to
|
|
1012
|
+
* the cursor layer. Falling back to a full frame is the safe default: a
|
|
1013
|
+
* consumer without the layer mounted still gets its paint.
|
|
1014
|
+
*/
|
|
1015
|
+
cursorRenderRequester = null;
|
|
1075
1016
|
constructor(init) {
|
|
1076
1017
|
this.viewport = init.viewport;
|
|
1077
1018
|
for (const node of init.nodes) this.writeNode(node.id, node);
|
|
@@ -1246,6 +1187,7 @@ var RivetGraphStore = class {
|
|
|
1246
1187
|
for (const key of this.anchors.keys()) {
|
|
1247
1188
|
if (key.startsWith(prefix)) this.anchors.delete(key);
|
|
1248
1189
|
}
|
|
1190
|
+
this.dropSelectedAnchors((ref) => ref.nodeId === id);
|
|
1249
1191
|
}
|
|
1250
1192
|
removeNodeInternal(id, removedEdges) {
|
|
1251
1193
|
if (!this.nodes.has(id)) return false;
|
|
@@ -1373,6 +1315,15 @@ var RivetGraphStore = class {
|
|
|
1373
1315
|
notifyFrame = () => {
|
|
1374
1316
|
for (const listener of this.frameListeners) listener();
|
|
1375
1317
|
};
|
|
1318
|
+
subscribeCursorFrame = (listener) => {
|
|
1319
|
+
this.cursorFrameListeners.add(listener);
|
|
1320
|
+
return () => {
|
|
1321
|
+
this.cursorFrameListeners.delete(listener);
|
|
1322
|
+
};
|
|
1323
|
+
};
|
|
1324
|
+
notifyCursorFrame = () => {
|
|
1325
|
+
for (const listener of this.cursorFrameListeners) listener();
|
|
1326
|
+
};
|
|
1376
1327
|
// --- node geometry -------------------------------------------------------
|
|
1377
1328
|
getWorldPositions = () => {
|
|
1378
1329
|
if (!this.worldCache) {
|
|
@@ -1419,8 +1370,14 @@ var RivetGraphStore = class {
|
|
|
1419
1370
|
getNodeRect = (id) => {
|
|
1420
1371
|
const origin = this.getNodeWorldPosition(id);
|
|
1421
1372
|
const live = this.heldNodeIds.has(id) ? void 0 : this.presence.getNodeTransforms().get(id);
|
|
1422
|
-
|
|
1423
|
-
|
|
1373
|
+
if (live) return { x: origin.x, y: origin.y, width: live.width, height: live.height };
|
|
1374
|
+
const node = this.nodes.get(id);
|
|
1375
|
+
return {
|
|
1376
|
+
x: origin.x,
|
|
1377
|
+
y: origin.y,
|
|
1378
|
+
width: node?.width ?? node?.size?.width ?? DEFAULT_NODE_SIZE.width,
|
|
1379
|
+
height: node?.height ?? node?.size?.height ?? DEFAULT_NODE_SIZE.height
|
|
1380
|
+
};
|
|
1424
1381
|
};
|
|
1425
1382
|
getNodeDepth = (id) => nodeDepth(this.nodes, id);
|
|
1426
1383
|
getDescendantIds = (id) => descendantIds(this.nodes, id);
|
|
@@ -1618,8 +1575,10 @@ var RivetGraphStore = class {
|
|
|
1618
1575
|
const opened = this.locks.setLocks(locks);
|
|
1619
1576
|
if (opened.length === 0 || this.heldNodeIds.size === 0) return;
|
|
1620
1577
|
const held = [...this.heldNodeIds];
|
|
1621
|
-
|
|
1622
|
-
|
|
1578
|
+
const openedSet = new Set(opened);
|
|
1579
|
+
for (const nodeId of held) {
|
|
1580
|
+
const source = this.locks.getLockSource(nodeId);
|
|
1581
|
+
if (source === null || !openedSet.has(source)) continue;
|
|
1623
1582
|
const holderId = this.locks.getHolder(nodeId);
|
|
1624
1583
|
if (holderId) this.lockConflictHandler?.({ nodeId, holderId, ids: held });
|
|
1625
1584
|
}
|
|
@@ -1647,6 +1606,7 @@ var RivetGraphStore = class {
|
|
|
1647
1606
|
};
|
|
1648
1607
|
selectNode = (id, additive = false) => {
|
|
1649
1608
|
this.setSelectionBoxActive(false);
|
|
1609
|
+
this.dropSelectedAnchors(() => true);
|
|
1650
1610
|
if (id === null) {
|
|
1651
1611
|
this.applyNodeSelection(/* @__PURE__ */ new Set());
|
|
1652
1612
|
this.notifySelection();
|
|
@@ -1666,6 +1626,7 @@ var RivetGraphStore = class {
|
|
|
1666
1626
|
};
|
|
1667
1627
|
selectNodes = (ids, additive = false) => {
|
|
1668
1628
|
this.setSelectionBoxActive(false);
|
|
1629
|
+
this.dropSelectedAnchors(() => true);
|
|
1669
1630
|
const target = additive ? new Set(this.selectedNodeIds) : /* @__PURE__ */ new Set();
|
|
1670
1631
|
for (const id of ids) target.add(id);
|
|
1671
1632
|
this.applyNodeSelection(target);
|
|
@@ -1674,6 +1635,7 @@ var RivetGraphStore = class {
|
|
|
1674
1635
|
};
|
|
1675
1636
|
getSelectedNodes = () => [...this.selectedNodeIds];
|
|
1676
1637
|
isNodeSelected = (id) => this.selectedNodeIds.has(id);
|
|
1638
|
+
getSelectedEdgeId = () => this.selectedEdgeId;
|
|
1677
1639
|
getSelection = () => this.buildSelection();
|
|
1678
1640
|
subscribeSelection = (listener) => {
|
|
1679
1641
|
this.selectionListeners.add(listener);
|
|
@@ -1713,6 +1675,7 @@ var RivetGraphStore = class {
|
|
|
1713
1675
|
// --- edges ---------------------------------------------------------------
|
|
1714
1676
|
selectEdge = (id) => {
|
|
1715
1677
|
this.setSelectionBoxActive(false);
|
|
1678
|
+
this.dropSelectedAnchors(() => true);
|
|
1716
1679
|
this.setEdgeSelection(id);
|
|
1717
1680
|
this.applyNodeSelection(/* @__PURE__ */ new Set());
|
|
1718
1681
|
this.notifySelection();
|
|
@@ -1758,7 +1721,7 @@ var RivetGraphStore = class {
|
|
|
1758
1721
|
const removedNodes = [];
|
|
1759
1722
|
const removedEdges = [];
|
|
1760
1723
|
const selectedEdge = this.selectedEdgeId;
|
|
1761
|
-
if (selectedEdge && this.removeEdgeInternal(selectedEdge)) {
|
|
1724
|
+
if (selectedEdge && this.locks.allows(edgeTargetKey(selectedEdge), "delete") && this.removeEdgeInternal(selectedEdge)) {
|
|
1762
1725
|
removedEdges.push(selectedEdge);
|
|
1763
1726
|
}
|
|
1764
1727
|
if (this.selectedNodeIds.size > 0) {
|
|
@@ -1883,6 +1846,10 @@ var RivetGraphStore = class {
|
|
|
1883
1846
|
setEdgeLabelAnchors = (anchors) => {
|
|
1884
1847
|
this.edgeLabelAnchors = anchors;
|
|
1885
1848
|
};
|
|
1849
|
+
getPeerEdgeAnchors = () => this.peerEdgeAnchors;
|
|
1850
|
+
setPeerEdgeAnchors = (anchors) => {
|
|
1851
|
+
this.peerEdgeAnchors = anchors;
|
|
1852
|
+
};
|
|
1886
1853
|
getAlignmentGuides = () => this.alignmentGuides;
|
|
1887
1854
|
setAlignmentGuides = (guides) => {
|
|
1888
1855
|
this.alignmentGuides = guides;
|
|
@@ -1929,6 +1896,8 @@ var RivetGraphStore = class {
|
|
|
1929
1896
|
anchorId,
|
|
1930
1897
|
color: options?.color ?? prev?.color,
|
|
1931
1898
|
strays: options?.strays ?? prev?.strays,
|
|
1899
|
+
selectable: options?.selectable ?? prev?.selectable,
|
|
1900
|
+
data: options?.data ?? prev?.data,
|
|
1932
1901
|
element,
|
|
1933
1902
|
// Keep the last-known geometry until the next successful measurement, so
|
|
1934
1903
|
// a re-bind (display remount, editor DOM swap) never blanks the chrome.
|
|
@@ -1947,6 +1916,7 @@ var RivetGraphStore = class {
|
|
|
1947
1916
|
};
|
|
1948
1917
|
unregisterAnchor = (nodeId, anchorId) => {
|
|
1949
1918
|
if (!this.anchors.delete(handleKey(nodeId, anchorId))) return;
|
|
1919
|
+
this.dropSelectedAnchors((ref) => ref.nodeId === nodeId && ref.anchorId === anchorId);
|
|
1950
1920
|
this.bumpAnchors(nodeId);
|
|
1951
1921
|
this.requestRender();
|
|
1952
1922
|
};
|
|
@@ -1968,6 +1938,51 @@ var RivetGraphStore = class {
|
|
|
1968
1938
|
return records;
|
|
1969
1939
|
};
|
|
1970
1940
|
getNodeAnchorsVersion = (nodeId) => this.anchorVersions.get(nodeId) ?? 0;
|
|
1941
|
+
getSelectedAnchors = () => {
|
|
1942
|
+
if (this.selectedAnchorsStale) {
|
|
1943
|
+
this.selectedAnchorsStale = false;
|
|
1944
|
+
this.selectedAnchorSnapshot = [...this.selectedAnchors.values()];
|
|
1945
|
+
}
|
|
1946
|
+
return this.selectedAnchorSnapshot;
|
|
1947
|
+
};
|
|
1948
|
+
setSelectedAnchors = (refs) => {
|
|
1949
|
+
const next = /* @__PURE__ */ new Map();
|
|
1950
|
+
for (const ref of refs) next.set(handleKey(ref.nodeId, ref.anchorId), ref);
|
|
1951
|
+
if (next.size === this.selectedAnchors.size && [...next.keys()].every((key) => this.selectedAnchors.has(key))) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const clearsGraph = next.size > 0 && (this.selectedNodeIds.size > 0 || this.selectedEdgeId !== null);
|
|
1955
|
+
this.selectedAnchors = /* @__PURE__ */ new Map();
|
|
1956
|
+
if (clearsGraph) {
|
|
1957
|
+
this.selectNode(null);
|
|
1958
|
+
this.selectEdge(null);
|
|
1959
|
+
}
|
|
1960
|
+
this.selectedAnchors = next;
|
|
1961
|
+
this.selectedAnchorsStale = true;
|
|
1962
|
+
for (const listener of this.anchorSelectionListeners) listener();
|
|
1963
|
+
this.requestRender();
|
|
1964
|
+
};
|
|
1965
|
+
isAnchorSelected = (nodeId, anchorId) => this.selectedAnchors.has(handleKey(nodeId, anchorId));
|
|
1966
|
+
subscribeAnchorSelection = (listener) => {
|
|
1967
|
+
this.anchorSelectionListeners.add(listener);
|
|
1968
|
+
return () => {
|
|
1969
|
+
this.anchorSelectionListeners.delete(listener);
|
|
1970
|
+
};
|
|
1971
|
+
};
|
|
1972
|
+
/** Drop every selected anchor the predicate claims. No-op when none match. */
|
|
1973
|
+
dropSelectedAnchors(matches) {
|
|
1974
|
+
if (this.selectedAnchors.size === 0) return;
|
|
1975
|
+
let changed = false;
|
|
1976
|
+
for (const [key, ref] of this.selectedAnchors) {
|
|
1977
|
+
if (!matches(ref)) continue;
|
|
1978
|
+
this.selectedAnchors.delete(key);
|
|
1979
|
+
changed = true;
|
|
1980
|
+
}
|
|
1981
|
+
if (!changed) return;
|
|
1982
|
+
this.selectedAnchorsStale = true;
|
|
1983
|
+
for (const listener of this.anchorSelectionListeners) listener();
|
|
1984
|
+
this.requestRender();
|
|
1985
|
+
}
|
|
1971
1986
|
subscribeNodeAnchors = (nodeId, listener) => {
|
|
1972
1987
|
let set = this.anchorListeners.get(nodeId);
|
|
1973
1988
|
if (!set) {
|
|
@@ -1992,17 +2007,29 @@ var RivetGraphStore = class {
|
|
|
1992
2007
|
beginConnection = (next) => {
|
|
1993
2008
|
this.pending = next;
|
|
1994
2009
|
this.requestRender();
|
|
2010
|
+
this.notifyConnection();
|
|
1995
2011
|
};
|
|
1996
2012
|
updateConnection = (to, toPosition) => {
|
|
1997
2013
|
if (!this.pending) return;
|
|
1998
2014
|
this.pending = { ...this.pending, to, toPosition };
|
|
1999
2015
|
this.requestRender();
|
|
2016
|
+
this.notifyConnection();
|
|
2000
2017
|
};
|
|
2001
2018
|
endConnection = () => {
|
|
2002
2019
|
if (!this.pending) return;
|
|
2003
2020
|
this.pending = null;
|
|
2004
2021
|
this.requestRender();
|
|
2022
|
+
this.notifyConnection();
|
|
2023
|
+
};
|
|
2024
|
+
subscribeConnection = (listener) => {
|
|
2025
|
+
this.connectionListeners.add(listener);
|
|
2026
|
+
return () => {
|
|
2027
|
+
this.connectionListeners.delete(listener);
|
|
2028
|
+
};
|
|
2005
2029
|
};
|
|
2030
|
+
notifyConnection() {
|
|
2031
|
+
for (const listener of this.connectionListeners) listener();
|
|
2032
|
+
}
|
|
2006
2033
|
// --- render loop hook ----------------------------------------------------
|
|
2007
2034
|
requestRender = () => {
|
|
2008
2035
|
this.renderRequester();
|
|
@@ -2010,6 +2037,13 @@ var RivetGraphStore = class {
|
|
|
2010
2037
|
bindRenderRequester = (fn) => {
|
|
2011
2038
|
this.renderRequester = fn;
|
|
2012
2039
|
};
|
|
2040
|
+
requestCursorRender = () => {
|
|
2041
|
+
if (this.cursorRenderRequester) this.cursorRenderRequester();
|
|
2042
|
+
else this.renderRequester();
|
|
2043
|
+
};
|
|
2044
|
+
bindCursorRenderRequester = (fn) => {
|
|
2045
|
+
this.cursorRenderRequester = fn;
|
|
2046
|
+
};
|
|
2013
2047
|
// --- reconnect gateway (runtime-bound; consulted by <Handle>) ------------
|
|
2014
2048
|
reconnectDelegate = null;
|
|
2015
2049
|
getSelectedEdge = () => this.selectedEdgeId;
|
|
@@ -2674,7 +2708,12 @@ function NodeResizer({
|
|
|
2674
2708
|
|
|
2675
2709
|
// src/presence/local.ts
|
|
2676
2710
|
var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
2677
|
-
var
|
|
2711
|
+
var sameConnection = (a, b) => {
|
|
2712
|
+
if (a === b) return true;
|
|
2713
|
+
if (!a || !b) return false;
|
|
2714
|
+
return a.source === b.source && a.sourceHandle === b.sourceHandle && a.toPosition === b.toPosition && a.to.x === b.to.x && a.to.y === b.to.y;
|
|
2715
|
+
};
|
|
2716
|
+
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) && sameConnection(a.pending, b.pending);
|
|
2678
2717
|
function trackLocalPresence({
|
|
2679
2718
|
store,
|
|
2680
2719
|
pane,
|
|
@@ -2686,6 +2725,21 @@ function trackLocalPresence({
|
|
|
2686
2725
|
let last = null;
|
|
2687
2726
|
let timer = null;
|
|
2688
2727
|
let lastEmit = Number.NEGATIVE_INFINITY;
|
|
2728
|
+
const selectionTargets = () => {
|
|
2729
|
+
const targets = store.getSelectedNodes();
|
|
2730
|
+
const edgeId = store.getSelectedEdgeId();
|
|
2731
|
+
if (edgeId) targets.push(presenceTargetKey({ kind: "edge", id: edgeId }));
|
|
2732
|
+
for (const ref of store.getSelectedAnchors()) {
|
|
2733
|
+
targets.push(presenceTargetKey({ kind: "anchor", ...ref }));
|
|
2734
|
+
}
|
|
2735
|
+
return targets;
|
|
2736
|
+
};
|
|
2737
|
+
const localConnection = () => {
|
|
2738
|
+
const pending = store.getPending();
|
|
2739
|
+
if (!pending) return null;
|
|
2740
|
+
const { source, sourceHandle, sourceType, from, to, toPosition } = pending;
|
|
2741
|
+
return toPosition === void 0 ? { source, sourceHandle, sourceType, from, to } : { source, sourceHandle, sourceType, from, to, toPosition };
|
|
2742
|
+
};
|
|
2689
2743
|
const publish = () => {
|
|
2690
2744
|
if (timer !== null) {
|
|
2691
2745
|
clearTimeout(timer);
|
|
@@ -2694,8 +2748,9 @@ function trackLocalPresence({
|
|
|
2694
2748
|
lastEmit = performance.now();
|
|
2695
2749
|
const next = {
|
|
2696
2750
|
cursor,
|
|
2697
|
-
selection:
|
|
2698
|
-
holding: [...store.getHeldNodeIds()]
|
|
2751
|
+
selection: selectionTargets(),
|
|
2752
|
+
holding: [...store.getHeldNodeIds()],
|
|
2753
|
+
pending: localConnection()
|
|
2699
2754
|
};
|
|
2700
2755
|
if (last && samePresence(last, next)) return;
|
|
2701
2756
|
last = next;
|
|
@@ -2737,7 +2792,18 @@ function trackLocalPresence({
|
|
|
2737
2792
|
schedule();
|
|
2738
2793
|
});
|
|
2739
2794
|
const unsubscribeSelection = store.subscribeSelection(() => publish());
|
|
2795
|
+
const unsubscribeAnchors = store.subscribeAnchorSelection(() => publish());
|
|
2740
2796
|
const unsubscribeHeld = store.subscribeHeldNodes(() => publish());
|
|
2797
|
+
let hadPending = store.getPending() !== null;
|
|
2798
|
+
const unsubscribeConnection = store.subscribeConnection(() => {
|
|
2799
|
+
const has = store.getPending() !== null;
|
|
2800
|
+
if (has === hadPending) {
|
|
2801
|
+
schedule();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
hadPending = has;
|
|
2805
|
+
publish();
|
|
2806
|
+
});
|
|
2741
2807
|
pane.addEventListener("pointermove", onPointerMove);
|
|
2742
2808
|
pane.addEventListener("pointerleave", onPointerLeave);
|
|
2743
2809
|
return () => {
|
|
@@ -2746,7 +2812,9 @@ function trackLocalPresence({
|
|
|
2746
2812
|
pane.removeEventListener("pointerleave", onPointerLeave);
|
|
2747
2813
|
unsubscribeViewport();
|
|
2748
2814
|
unsubscribeSelection();
|
|
2815
|
+
unsubscribeAnchors();
|
|
2749
2816
|
unsubscribeHeld();
|
|
2817
|
+
unsubscribeConnection();
|
|
2750
2818
|
};
|
|
2751
2819
|
}
|
|
2752
2820
|
|
|
@@ -3043,7 +3111,7 @@ function createPanController(deps) {
|
|
|
3043
3111
|
|
|
3044
3112
|
// src/input/resize-controller.ts
|
|
3045
3113
|
function createResizeController(deps) {
|
|
3046
|
-
const { pane, bgCanvas, size, edgeRenderer, foregroundRenderer, schedule } = deps;
|
|
3114
|
+
const { pane, bgCanvas, size, edgeRenderer, foregroundRenderer, onResize, schedule } = deps;
|
|
3047
3115
|
const resize = () => {
|
|
3048
3116
|
const rect = pane.getBoundingClientRect();
|
|
3049
3117
|
size.width = rect.width;
|
|
@@ -3055,6 +3123,7 @@ function createResizeController(deps) {
|
|
|
3055
3123
|
bgCanvas.style.height = `${rect.height}px`;
|
|
3056
3124
|
edgeRenderer.resize(rect.width, rect.height, size.dpr);
|
|
3057
3125
|
foregroundRenderer.resize(rect.width, rect.height, size.dpr);
|
|
3126
|
+
onResize?.();
|
|
3058
3127
|
schedule();
|
|
3059
3128
|
};
|
|
3060
3129
|
const observer = new ResizeObserver(resize);
|
|
@@ -3147,6 +3216,9 @@ var LABEL_PADDING_X = 6;
|
|
|
3147
3216
|
var LABEL_HEIGHT = 17;
|
|
3148
3217
|
var LABEL_RADIUS = 4;
|
|
3149
3218
|
var CULL_MARGIN_PX = 64;
|
|
3219
|
+
var PENDING_DASH = [6, 4];
|
|
3220
|
+
var PENDING_LINE_WIDTH = 2;
|
|
3221
|
+
var PENDING_TIP_RADIUS = 3.5;
|
|
3150
3222
|
function traceRect(ctx, rect, radius) {
|
|
3151
3223
|
ctx.beginPath();
|
|
3152
3224
|
if (typeof ctx.roundRect === "function") {
|
|
@@ -3199,15 +3271,18 @@ function drawCursor(ctx, point, color, name) {
|
|
|
3199
3271
|
ctx.fillStyle = "#ffffff";
|
|
3200
3272
|
ctx.fillText(name, x + LABEL_PADDING_X, y + LABEL_HEIGHT / 2);
|
|
3201
3273
|
}
|
|
3202
|
-
function
|
|
3274
|
+
function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
|
|
3203
3275
|
if (peers.length === 0 && locks.size === 0) return;
|
|
3204
3276
|
ctx.save();
|
|
3205
3277
|
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3206
3278
|
const painted = /* @__PURE__ */ new Set();
|
|
3207
3279
|
for (const peer of peers) {
|
|
3208
|
-
const held = /* @__PURE__ */ new Set([
|
|
3280
|
+
const held = /* @__PURE__ */ new Set([
|
|
3281
|
+
...peer.holding.filter((key) => isNodeTarget(key)),
|
|
3282
|
+
...peer.transforms.keys()
|
|
3283
|
+
]);
|
|
3209
3284
|
for (const id of peer.selection) {
|
|
3210
|
-
if (held.has(id)) continue;
|
|
3285
|
+
if (!isNodeTarget(id) || held.has(id)) continue;
|
|
3211
3286
|
const rect = getNodeRect(id);
|
|
3212
3287
|
if (!rect) continue;
|
|
3213
3288
|
const screen = projectRect(rect, viewport);
|
|
@@ -3224,7 +3299,7 @@ function drawPresence(ctx, peers, { viewport, size, getNodeRect, renderCursors,
|
|
|
3224
3299
|
if (locks.size > 0) {
|
|
3225
3300
|
const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
|
|
3226
3301
|
for (const [id, holderId] of locks) {
|
|
3227
|
-
if (painted.has(id)) continue;
|
|
3302
|
+
if (!isNodeTarget(id) || painted.has(id)) continue;
|
|
3228
3303
|
const rect = getNodeRect(id);
|
|
3229
3304
|
if (!rect) continue;
|
|
3230
3305
|
const screen = projectRect(rect, viewport);
|
|
@@ -3232,15 +3307,80 @@ function drawPresence(ctx, peers, { viewport, size, getNodeRect, renderCursors,
|
|
|
3232
3307
|
drawNodeOutline(ctx, screen, declared.get(holderId) ?? peerColor(holderId), "held");
|
|
3233
3308
|
}
|
|
3234
3309
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3310
|
+
ctx.restore();
|
|
3311
|
+
}
|
|
3312
|
+
function collectPeerEdges(peers, lockedEdges) {
|
|
3313
|
+
if (peers.length === 0 && lockedEdges.size === 0) return void 0;
|
|
3314
|
+
let claimed = null;
|
|
3315
|
+
const claim = (key, color, override) => {
|
|
3316
|
+
const edgeId = presenceTargetEdgeId(key);
|
|
3317
|
+
if (edgeId === null) return;
|
|
3318
|
+
claimed ??= /* @__PURE__ */ new Map();
|
|
3319
|
+
if (override || !claimed.has(edgeId)) claimed.set(edgeId, color);
|
|
3320
|
+
};
|
|
3321
|
+
for (const peer of peers) {
|
|
3322
|
+
for (const key of peer.selection) claim(key, peer.color, false);
|
|
3323
|
+
}
|
|
3324
|
+
for (const peer of peers) {
|
|
3325
|
+
for (const key of peer.holding) claim(key, peer.color, true);
|
|
3326
|
+
}
|
|
3327
|
+
if (lockedEdges.size > 0) {
|
|
3328
|
+
const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
|
|
3329
|
+
for (const [edgeId, holderId] of lockedEdges) {
|
|
3330
|
+
claimed ??= /* @__PURE__ */ new Map();
|
|
3331
|
+
if (!claimed.has(edgeId)) {
|
|
3332
|
+
claimed.set(edgeId, declared.get(holderId) ?? peerColor(holderId));
|
|
3333
|
+
}
|
|
3241
3334
|
}
|
|
3242
3335
|
}
|
|
3243
|
-
|
|
3336
|
+
return claimed ?? void 0;
|
|
3337
|
+
}
|
|
3338
|
+
function drawPeerPending(ctx, peers, { viewport, size, handles }) {
|
|
3339
|
+
let saved = false;
|
|
3340
|
+
for (const peer of peers) {
|
|
3341
|
+
if (!peer.pending) continue;
|
|
3342
|
+
const from = worldToScreen(peer.pending.from, viewport);
|
|
3343
|
+
const to = worldToScreen(peer.pending.to, viewport);
|
|
3344
|
+
const bounds = {
|
|
3345
|
+
x: Math.min(from.x, to.x),
|
|
3346
|
+
y: Math.min(from.y, to.y),
|
|
3347
|
+
width: Math.abs(to.x - from.x),
|
|
3348
|
+
height: Math.abs(to.y - from.y)
|
|
3349
|
+
};
|
|
3350
|
+
if (!onScreen(bounds, size)) continue;
|
|
3351
|
+
if (!saved) {
|
|
3352
|
+
saved = true;
|
|
3353
|
+
ctx.save();
|
|
3354
|
+
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3355
|
+
ctx.lineWidth = PENDING_LINE_WIDTH;
|
|
3356
|
+
ctx.setLineDash(PENDING_DASH);
|
|
3357
|
+
}
|
|
3358
|
+
const points = buildPendingPath(peer.pending, handles, viewport);
|
|
3359
|
+
const [first, ...rest] = points;
|
|
3360
|
+
if (!first) continue;
|
|
3361
|
+
ctx.strokeStyle = peer.color;
|
|
3362
|
+
ctx.beginPath();
|
|
3363
|
+
ctx.moveTo(first.x, first.y);
|
|
3364
|
+
for (const point of rest) ctx.lineTo(point.x, point.y);
|
|
3365
|
+
ctx.stroke();
|
|
3366
|
+
ctx.setLineDash([]);
|
|
3367
|
+
ctx.fillStyle = peer.color;
|
|
3368
|
+
ctx.beginPath();
|
|
3369
|
+
ctx.arc(to.x, to.y, PENDING_TIP_RADIUS, 0, Math.PI * 2);
|
|
3370
|
+
ctx.fill();
|
|
3371
|
+
ctx.setLineDash(PENDING_DASH);
|
|
3372
|
+
}
|
|
3373
|
+
if (saved) ctx.restore();
|
|
3374
|
+
}
|
|
3375
|
+
function drawPeerCursors(ctx, peers, { viewport, size }) {
|
|
3376
|
+
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3377
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
3378
|
+
for (const peer of peers) {
|
|
3379
|
+
if (!peer.cursor) continue;
|
|
3380
|
+
const point = worldToScreen(peer.cursor, viewport);
|
|
3381
|
+
if (!onScreen({ ...point, width: 0, height: 0 }, size)) continue;
|
|
3382
|
+
drawCursor(ctx, point, peer.color, peer.name);
|
|
3383
|
+
}
|
|
3244
3384
|
}
|
|
3245
3385
|
|
|
3246
3386
|
// src/renderer/swimlane.ts
|
|
@@ -3302,6 +3442,7 @@ function drawAlignmentGuides(ctx, guides, viewport, size) {
|
|
|
3302
3442
|
}
|
|
3303
3443
|
var ENDPOINT_BUBBLE_RADIUS = 6;
|
|
3304
3444
|
var HOVER_KEEP_TOLERANCE = 16;
|
|
3445
|
+
var EMPTY_EDGE_ANCHORS = /* @__PURE__ */ new Map();
|
|
3305
3446
|
function drawEndpointBubbles(ctx, ends, size) {
|
|
3306
3447
|
ctx.save();
|
|
3307
3448
|
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
@@ -3318,7 +3459,7 @@ function drawEndpointBubbles(ctx, ends, size) {
|
|
|
3318
3459
|
}
|
|
3319
3460
|
function useRivetRuntime(params) {
|
|
3320
3461
|
const { store, paneRef, nodeContainerRef } = params;
|
|
3321
|
-
const { backgroundCanvasRef, edgeCanvasRef, foregroundCanvasRef } = params;
|
|
3462
|
+
const { backgroundCanvasRef, edgeCanvasRef, foregroundCanvasRef, cursorCanvasRef } = params;
|
|
3322
3463
|
const { swimlaneLanes, scrollToPan, zoomSpeed } = params;
|
|
3323
3464
|
const { minZoom, maxZoom, gridGap, edgeTypes, defaultEdgeOptions, anchorOptions } = params;
|
|
3324
3465
|
const { edgeRenderer: createEdgeRenderer } = params;
|
|
@@ -3326,6 +3467,8 @@ function useRivetRuntime(params) {
|
|
|
3326
3467
|
const [visibleIds, setVisibleIds] = useState([]);
|
|
3327
3468
|
const reconnectRef = useRef(params);
|
|
3328
3469
|
reconnectRef.current = params;
|
|
3470
|
+
const outlineModeRef = useRef(params.outlineMode);
|
|
3471
|
+
outlineModeRef.current = params.outlineMode;
|
|
3329
3472
|
const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
|
|
3330
3473
|
const visibleKeyRef = useRef("");
|
|
3331
3474
|
const swimlaneLanesRef = useRef(swimlaneLanes);
|
|
@@ -3357,6 +3500,7 @@ function useRivetRuntime(params) {
|
|
|
3357
3500
|
const size = sizeRef.current;
|
|
3358
3501
|
let frame = 0;
|
|
3359
3502
|
let dirty = true;
|
|
3503
|
+
let cursorsDirty = true;
|
|
3360
3504
|
let hoveredEdgeId = null;
|
|
3361
3505
|
let peerPositioned = /* @__PURE__ */ new Set();
|
|
3362
3506
|
const positionPeerNodes = (worldPositions) => {
|
|
@@ -3383,6 +3527,39 @@ function useRivetRuntime(params) {
|
|
|
3383
3527
|
}
|
|
3384
3528
|
peerPositioned = current;
|
|
3385
3529
|
};
|
|
3530
|
+
let cursorCanvas = null;
|
|
3531
|
+
let cursorCtx = null;
|
|
3532
|
+
let cursorAllocated = false;
|
|
3533
|
+
const sizeCursorLayer = () => {
|
|
3534
|
+
if (!cursorCanvas || !cursorAllocated) return;
|
|
3535
|
+
cursorCanvas.width = Math.max(1, Math.round(size.width * size.dpr));
|
|
3536
|
+
cursorCanvas.height = Math.max(1, Math.round(size.height * size.dpr));
|
|
3537
|
+
cursorCanvas.style.width = `${size.width}px`;
|
|
3538
|
+
cursorCanvas.style.height = `${size.height}px`;
|
|
3539
|
+
};
|
|
3540
|
+
const drawCursorLayer = () => {
|
|
3541
|
+
const canvas = cursorCanvasRef.current;
|
|
3542
|
+
if (canvas !== cursorCanvas) {
|
|
3543
|
+
cursorCanvas = canvas;
|
|
3544
|
+
cursorCtx = canvas?.getContext("2d") ?? null;
|
|
3545
|
+
cursorAllocated = false;
|
|
3546
|
+
}
|
|
3547
|
+
if (!cursorCtx) return;
|
|
3548
|
+
const peers = store.presence.getPeers();
|
|
3549
|
+
const wanted = peers.some((peer) => peer.cursor);
|
|
3550
|
+
if (!wanted && !cursorAllocated) return;
|
|
3551
|
+
if (!cursorAllocated) {
|
|
3552
|
+
cursorAllocated = true;
|
|
3553
|
+
sizeCursorLayer();
|
|
3554
|
+
}
|
|
3555
|
+
drawPeerCursors(cursorCtx, peers, { viewport: store.getViewport(), size });
|
|
3556
|
+
};
|
|
3557
|
+
const stepPresence = () => {
|
|
3558
|
+
const version = store.presence.getTransformVersion();
|
|
3559
|
+
if (!store.presence.step(performance.now())) return;
|
|
3560
|
+
if (store.presence.getTransformVersion() !== version) schedule();
|
|
3561
|
+
else scheduleCursors();
|
|
3562
|
+
};
|
|
3386
3563
|
const anchorPlacement = (nodeId, anchorId, side) => {
|
|
3387
3564
|
const record = store.anchors.get(handleKey(nodeId, anchorId));
|
|
3388
3565
|
return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
|
|
@@ -3398,9 +3575,10 @@ function useRivetRuntime(params) {
|
|
|
3398
3575
|
scrollToPan,
|
|
3399
3576
|
schedule: () => schedule()
|
|
3400
3577
|
});
|
|
3578
|
+
const canReconnect = (edgeId) => store.locks.size() === 0 || store.locks.allows(edgeTargetKey(edgeId), "reconnect");
|
|
3401
3579
|
const render = () => {
|
|
3402
3580
|
zoom.step();
|
|
3403
|
-
|
|
3581
|
+
stepPresence();
|
|
3404
3582
|
const viewport = store.getViewport();
|
|
3405
3583
|
layerHint.apply(viewportToCss(viewport));
|
|
3406
3584
|
bgCtx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
@@ -3412,6 +3590,8 @@ function useRivetRuntime(params) {
|
|
|
3412
3590
|
const edgeList = [...store.edges.values()];
|
|
3413
3591
|
const pending = store.getPending();
|
|
3414
3592
|
const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
|
|
3593
|
+
const outlineMode = outlineModeRef.current;
|
|
3594
|
+
const peerEdges = outlineMode === "none" ? void 0 : collectPeerEdges(store.presence.getPeers(), store.locks.getLockedEdges());
|
|
3415
3595
|
edgeRenderer.draw(edgeList, store.nodes, viewport, {
|
|
3416
3596
|
handles: store.handles,
|
|
3417
3597
|
pending: null,
|
|
@@ -3420,32 +3600,43 @@ function useRivetRuntime(params) {
|
|
|
3420
3600
|
worldPositions,
|
|
3421
3601
|
displayedSides: store.getDisplayedSides(),
|
|
3422
3602
|
liveAlignNodes,
|
|
3423
|
-
anchorPlacement
|
|
3603
|
+
anchorPlacement,
|
|
3604
|
+
peerEdges
|
|
3424
3605
|
});
|
|
3425
3606
|
store.setEdgeLabelAnchors(edgeRenderer.getLabels());
|
|
3607
|
+
if (outlineMode === "dom") {
|
|
3608
|
+
store.setPeerEdgeAnchors(edgeRenderer.getPeerEdgeAnchors?.() ?? EMPTY_EDGE_ANCHORS);
|
|
3609
|
+
}
|
|
3426
3610
|
foregroundRenderer.draw([], store.nodes, viewport, {
|
|
3427
3611
|
handles: store.handles,
|
|
3428
3612
|
pending
|
|
3429
3613
|
});
|
|
3614
|
+
if (fgCtx && !store.presence.isEmpty()) {
|
|
3615
|
+
drawPeerPending(fgCtx, store.presence.getPeers(), {
|
|
3616
|
+
viewport,
|
|
3617
|
+
size,
|
|
3618
|
+
handles: store.handles
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3430
3621
|
if (fgCtx) drawAlignmentGuides(fgCtx, store.getAlignmentGuides(), viewport, size);
|
|
3431
3622
|
if (fgCtx) {
|
|
3432
3623
|
const affordanceId = hoveredEdgeId ?? store.getSelectedEdge();
|
|
3433
3624
|
const edge = affordanceId ? store.edges.get(affordanceId) : void 0;
|
|
3434
3625
|
const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
|
|
3435
|
-
if (edge && reconnectable && edge.id !== pending?.reconnecting) {
|
|
3626
|
+
if (edge && reconnectable && edge.id !== pending?.reconnecting && canReconnect(edge.id)) {
|
|
3436
3627
|
const ends = edgeRenderer.getEndpoints?.().get(edge.id);
|
|
3437
3628
|
if (ends) drawEndpointBubbles(fgCtx, ends, size);
|
|
3438
3629
|
}
|
|
3439
3630
|
}
|
|
3440
|
-
if (fgCtx && (!store.presence.isEmpty() || store.locks.size() > 0)) {
|
|
3441
|
-
|
|
3631
|
+
if (fgCtx && outlineMode === "canvas" && (!store.presence.isEmpty() || store.locks.size() > 0)) {
|
|
3632
|
+
drawPeerOutlines(fgCtx, store.presence.getPeers(), {
|
|
3442
3633
|
viewport,
|
|
3443
3634
|
size,
|
|
3444
3635
|
getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
|
|
3445
|
-
renderCursors: reconnectRef.current.presenceOptions.renderCursors,
|
|
3446
3636
|
locks: store.locks.getLocks()
|
|
3447
3637
|
});
|
|
3448
3638
|
}
|
|
3639
|
+
drawCursorLayer();
|
|
3449
3640
|
if (edgeList.some((edge) => edge.animated ?? defaultEdgeOptions?.animated)) schedule();
|
|
3450
3641
|
const rect = visibleWorldRect(viewport, size.width, size.height);
|
|
3451
3642
|
const margin = CULL_MARGIN_PX2 / viewport.zoom;
|
|
@@ -3474,19 +3665,34 @@ function useRivetRuntime(params) {
|
|
|
3474
3665
|
setVisibleIds(ids);
|
|
3475
3666
|
}
|
|
3476
3667
|
store.notifyFrame();
|
|
3668
|
+
store.notifyCursorFrame();
|
|
3477
3669
|
};
|
|
3478
3670
|
const tick = () => {
|
|
3479
3671
|
frame = 0;
|
|
3480
|
-
if (
|
|
3481
|
-
|
|
3482
|
-
|
|
3672
|
+
if (dirty) {
|
|
3673
|
+
dirty = false;
|
|
3674
|
+
cursorsDirty = false;
|
|
3675
|
+
render();
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
if (!cursorsDirty) return;
|
|
3679
|
+
cursorsDirty = false;
|
|
3680
|
+
stepPresence();
|
|
3681
|
+
drawCursorLayer();
|
|
3682
|
+
store.notifyCursorFrame();
|
|
3483
3683
|
};
|
|
3484
3684
|
const schedule = () => {
|
|
3485
3685
|
dirty = true;
|
|
3486
3686
|
if (frame) return;
|
|
3487
3687
|
frame = requestAnimationFrame(tick);
|
|
3488
3688
|
};
|
|
3689
|
+
const scheduleCursors = () => {
|
|
3690
|
+
cursorsDirty = true;
|
|
3691
|
+
if (frame) return;
|
|
3692
|
+
frame = requestAnimationFrame(tick);
|
|
3693
|
+
};
|
|
3489
3694
|
store.bindRenderRequester(schedule);
|
|
3695
|
+
store.bindCursorRenderRequester(scheduleCursors);
|
|
3490
3696
|
const setEdgeHover = (id) => {
|
|
3491
3697
|
if (hoveredEdgeId === id) return;
|
|
3492
3698
|
hoveredEdgeId = id;
|
|
@@ -3522,6 +3728,7 @@ function useRivetRuntime(params) {
|
|
|
3522
3728
|
return { x, y: base.y + size2.height / 2 };
|
|
3523
3729
|
};
|
|
3524
3730
|
const beginReconnect = (edge, end) => {
|
|
3731
|
+
if (!canReconnect(edge.id)) return;
|
|
3525
3732
|
zoom.cancel();
|
|
3526
3733
|
const fixedIsSource = end === "target";
|
|
3527
3734
|
const fixedNodeId = fixedIsSource ? edge.source : edge.target;
|
|
@@ -3578,7 +3785,7 @@ function useRivetRuntime(params) {
|
|
|
3578
3785
|
if (hit.edgeId !== hoveredEdgeId && hit.edgeId !== store.getSelectedEdge()) return null;
|
|
3579
3786
|
const edge = store.edges.get(hit.edgeId);
|
|
3580
3787
|
const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
|
|
3581
|
-
return reconnectable ? hit : null;
|
|
3788
|
+
return reconnectable && canReconnect(hit.edgeId) ? hit : null;
|
|
3582
3789
|
},
|
|
3583
3790
|
begin: (edgeId, end) => {
|
|
3584
3791
|
const edge = store.edges.get(edgeId);
|
|
@@ -3617,7 +3824,7 @@ function useRivetRuntime(params) {
|
|
|
3617
3824
|
}
|
|
3618
3825
|
const hitEdge = edgeRenderer.pick(px, py);
|
|
3619
3826
|
if (hitEdge && store.edges.get(hitEdge)?.selectable !== false) {
|
|
3620
|
-
store.selectEdge(hitEdge);
|
|
3827
|
+
if (store.locks.allows(edgeTargetKey(hitEdge), "select")) store.selectEdge(hitEdge);
|
|
3621
3828
|
return;
|
|
3622
3829
|
}
|
|
3623
3830
|
store.selectNode(null);
|
|
@@ -3696,6 +3903,7 @@ function useRivetRuntime(params) {
|
|
|
3696
3903
|
size,
|
|
3697
3904
|
edgeRenderer,
|
|
3698
3905
|
foregroundRenderer,
|
|
3906
|
+
onResize: sizeCursorLayer,
|
|
3699
3907
|
schedule
|
|
3700
3908
|
});
|
|
3701
3909
|
pane.addEventListener("wheel", zoom.onWheel, { passive: false });
|
|
@@ -3712,6 +3920,7 @@ function useRivetRuntime(params) {
|
|
|
3712
3920
|
resizer.dispose();
|
|
3713
3921
|
store.bindRenderRequester(() => {
|
|
3714
3922
|
});
|
|
3923
|
+
store.bindCursorRenderRequester(null);
|
|
3715
3924
|
store.bindReconnectDelegate(null);
|
|
3716
3925
|
unsubscribeFocus();
|
|
3717
3926
|
if (focusRetryFrame) cancelAnimationFrame(focusRetryFrame);
|
|
@@ -3739,6 +3948,7 @@ function useRivetRuntime(params) {
|
|
|
3739
3948
|
backgroundCanvasRef,
|
|
3740
3949
|
edgeCanvasRef,
|
|
3741
3950
|
foregroundCanvasRef,
|
|
3951
|
+
cursorCanvasRef,
|
|
3742
3952
|
scrollToPan,
|
|
3743
3953
|
zoomSpeed,
|
|
3744
3954
|
minZoom,
|
|
@@ -3958,6 +4168,8 @@ var PICK_TOLERANCE = 6;
|
|
|
3958
4168
|
var ENDPOINT_RADIUS = 12;
|
|
3959
4169
|
var ARROW_SIZE = 9;
|
|
3960
4170
|
var DASH_SPEED = 40;
|
|
4171
|
+
var PEER_HALO_EXTRA = 6;
|
|
4172
|
+
var PEER_HALO_ALPHA = 0.45;
|
|
3961
4173
|
var Canvas2DEdgeRenderer = class {
|
|
3962
4174
|
constructor(canvas, options = {}) {
|
|
3963
4175
|
this.canvas = canvas;
|
|
@@ -3980,6 +4192,8 @@ var Canvas2DEdgeRenderer = class {
|
|
|
3980
4192
|
endpoints = /* @__PURE__ */ new Map();
|
|
3981
4193
|
/** Screen-space label anchor per edge that has a label. */
|
|
3982
4194
|
labels = /* @__PURE__ */ new Map();
|
|
4195
|
+
/** Screen-space midpoint per edge somebody has claimed, for DOM claim chrome. */
|
|
4196
|
+
peerAnchors = /* @__PURE__ */ new Map();
|
|
3983
4197
|
resize(width, height, dpr) {
|
|
3984
4198
|
this.width = width;
|
|
3985
4199
|
this.height = height;
|
|
@@ -3993,6 +4207,10 @@ var Canvas2DEdgeRenderer = class {
|
|
|
3993
4207
|
getLabels() {
|
|
3994
4208
|
return this.labels;
|
|
3995
4209
|
}
|
|
4210
|
+
/** Screen-space midpoints for the edges a peer had claimed last frame. */
|
|
4211
|
+
getPeerEdgeAnchors() {
|
|
4212
|
+
return this.peerAnchors;
|
|
4213
|
+
}
|
|
3996
4214
|
/** Screen-space endpoints per edge from the last frame (reconnect affordance). */
|
|
3997
4215
|
getEndpoints() {
|
|
3998
4216
|
return this.endpoints;
|
|
@@ -4007,11 +4225,23 @@ var Canvas2DEdgeRenderer = class {
|
|
|
4007
4225
|
this.geometry.clear();
|
|
4008
4226
|
this.endpoints.clear();
|
|
4009
4227
|
this.labels.clear();
|
|
4228
|
+
this.peerAnchors.clear();
|
|
4010
4229
|
const resolved = buildEdges(edges, nodes, viewport, this.edgeTypes, this.defaults, extras);
|
|
4230
|
+
const peerEdges = extras?.peerEdges;
|
|
4011
4231
|
for (const edge of resolved) {
|
|
4012
4232
|
this.geometry.set(edge.id, edge.points);
|
|
4013
4233
|
this.endpoints.set(edge.id, { source: edge.source, target: edge.target });
|
|
4014
4234
|
if (edge.label) this.labels.set(edge.id, edge.label);
|
|
4235
|
+
const peerColor2 = peerEdges?.get(edge.id);
|
|
4236
|
+
if (peerColor2) {
|
|
4237
|
+
this.peerAnchors.set(edge.id, polylineMidpoint(edge.points));
|
|
4238
|
+
ctx.strokeStyle = peerColor2;
|
|
4239
|
+
ctx.lineWidth = edge.width + PEER_HALO_EXTRA * 2;
|
|
4240
|
+
ctx.globalAlpha = PEER_HALO_ALPHA;
|
|
4241
|
+
ctx.setLineDash([]);
|
|
4242
|
+
ctx.lineDashOffset = 0;
|
|
4243
|
+
strokePolyline(ctx, edge.points);
|
|
4244
|
+
}
|
|
4015
4245
|
ctx.strokeStyle = edge.stroke;
|
|
4016
4246
|
ctx.lineWidth = edge.width;
|
|
4017
4247
|
ctx.globalAlpha = edge.opacity;
|
|
@@ -4072,6 +4302,7 @@ var Canvas2DEdgeRenderer = class {
|
|
|
4072
4302
|
this.geometry.clear();
|
|
4073
4303
|
this.endpoints.clear();
|
|
4074
4304
|
this.labels.clear();
|
|
4305
|
+
this.peerAnchors.clear();
|
|
4075
4306
|
}
|
|
4076
4307
|
};
|
|
4077
4308
|
var canvas2DEdgeRenderer = (canvas, options) => new Canvas2DEdgeRenderer(canvas, options);
|
|
@@ -4181,7 +4412,6 @@ function EdgeLabelLayer() {
|
|
|
4181
4412
|
}
|
|
4182
4413
|
|
|
4183
4414
|
// src/input/group-drag.ts
|
|
4184
|
-
var DRAG_THRESHOLD = 4;
|
|
4185
4415
|
var ALIGN_THRESHOLD = 6;
|
|
4186
4416
|
function startGroupDrag(params) {
|
|
4187
4417
|
const { store, el, snapGrid, alignmentGuides, onLaneChange, onFrame } = params;
|
|
@@ -4206,6 +4436,7 @@ function startGroupDrag(params) {
|
|
|
4206
4436
|
onFrame?.();
|
|
4207
4437
|
};
|
|
4208
4438
|
const beginDrag = () => {
|
|
4439
|
+
params.selectOnDrag?.();
|
|
4209
4440
|
const movers = store.getMovers(store.getSelectedNodes()).filter((moverId) => store.locks.allows(moverId, "drag"));
|
|
4210
4441
|
if (movers.length === 0) return false;
|
|
4211
4442
|
started = true;
|
|
@@ -4334,6 +4565,37 @@ function clampToParent(store, parentId, position, size) {
|
|
|
4334
4565
|
};
|
|
4335
4566
|
return clampChildToParent(position, size, parentSize);
|
|
4336
4567
|
}
|
|
4568
|
+
|
|
4569
|
+
// src/anchor/select-gesture.ts
|
|
4570
|
+
function attachAnchorSelection(store, record) {
|
|
4571
|
+
const element = record.element;
|
|
4572
|
+
if (!record.selectable || !(element instanceof HTMLElement || element instanceof SVGElement)) {
|
|
4573
|
+
return null;
|
|
4574
|
+
}
|
|
4575
|
+
const { nodeId, anchorId } = record;
|
|
4576
|
+
const previous = element.style.pointerEvents;
|
|
4577
|
+
element.style.pointerEvents = "auto";
|
|
4578
|
+
const onPointerDown = (event) => {
|
|
4579
|
+
if (!(event instanceof MouseEvent)) return;
|
|
4580
|
+
if (event.button !== 0) return;
|
|
4581
|
+
const originX = event.clientX;
|
|
4582
|
+
const originY = event.clientY;
|
|
4583
|
+
const onUp = (upEvent) => {
|
|
4584
|
+
window.removeEventListener("pointerup", onUp);
|
|
4585
|
+
if (!(upEvent instanceof MouseEvent)) return;
|
|
4586
|
+
const moved = Math.hypot(upEvent.clientX - originX, upEvent.clientY - originY);
|
|
4587
|
+
if (moved >= DRAG_THRESHOLD) return;
|
|
4588
|
+
if (!store.locks.allows(anchorTargetKey(nodeId, anchorId), "select")) return;
|
|
4589
|
+
store.setSelectedAnchors([{ nodeId, anchorId }]);
|
|
4590
|
+
};
|
|
4591
|
+
window.addEventListener("pointerup", onUp);
|
|
4592
|
+
};
|
|
4593
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
4594
|
+
return () => {
|
|
4595
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
4596
|
+
element.style.pointerEvents = previous;
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4337
4599
|
var DEFAULT_ANCHOR_COLOR = "#6366f1";
|
|
4338
4600
|
function NodeAnchors({ nodeId }) {
|
|
4339
4601
|
const { store, anchorOptions } = useRivetContext();
|
|
@@ -4342,8 +4604,27 @@ function NodeAnchors({ nodeId }) {
|
|
|
4342
4604
|
[store, nodeId]
|
|
4343
4605
|
);
|
|
4344
4606
|
const getAnchorsVersion = useCallback(() => store.getNodeAnchorsVersion(nodeId), [store, nodeId]);
|
|
4345
|
-
useSyncExternalStore(
|
|
4607
|
+
const anchorsVersion = useSyncExternalStore(
|
|
4608
|
+
subscribeAnchors,
|
|
4609
|
+
getAnchorsVersion,
|
|
4610
|
+
getAnchorsVersion
|
|
4611
|
+
);
|
|
4346
4612
|
useSyncExternalStore(store.subscribeEdges, store.getEdgesVersion, store.getEdgesVersion);
|
|
4613
|
+
useSyncExternalStore(
|
|
4614
|
+
store.subscribeAnchorSelection,
|
|
4615
|
+
store.getSelectedAnchors,
|
|
4616
|
+
store.getSelectedAnchors
|
|
4617
|
+
);
|
|
4618
|
+
useEffect(() => {
|
|
4619
|
+
const cleanups = [];
|
|
4620
|
+
for (const record of store.getNodeAnchors(nodeId)) {
|
|
4621
|
+
const cleanup = attachAnchorSelection(store, record);
|
|
4622
|
+
if (cleanup) cleanups.push(cleanup);
|
|
4623
|
+
}
|
|
4624
|
+
return () => {
|
|
4625
|
+
for (const cleanup of cleanups) cleanup();
|
|
4626
|
+
};
|
|
4627
|
+
}, [store, nodeId, anchorsVersion]);
|
|
4347
4628
|
const records = store.getNodeAnchors(nodeId);
|
|
4348
4629
|
if (records.length === 0) return null;
|
|
4349
4630
|
const dotsVisible = Boolean(store.nodes.get(nodeId)?.hovered);
|
|
@@ -4361,6 +4642,7 @@ function NodeAnchors({ nodeId }) {
|
|
|
4361
4642
|
}
|
|
4362
4643
|
}
|
|
4363
4644
|
return /* @__PURE__ */ jsx(Fragment, { children: records.map((record) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4645
|
+
store.isAnchorSelected(nodeId, record.anchorId) && /* @__PURE__ */ jsx(AnchorSelectionOutline, { record }),
|
|
4364
4646
|
/* @__PURE__ */ jsx(AnchorDots, { record, options: anchorOptions, visible: dotsVisible }),
|
|
4365
4647
|
record.strays !== "none" && /* @__PURE__ */ jsx(
|
|
4366
4648
|
AnchorStrayHandles,
|
|
@@ -4372,6 +4654,31 @@ function NodeAnchors({ nodeId }) {
|
|
|
4372
4654
|
)
|
|
4373
4655
|
] }, record.anchorId)) });
|
|
4374
4656
|
}
|
|
4657
|
+
function AnchorSelectionOutline({ record }) {
|
|
4658
|
+
const geometry = record.geometry;
|
|
4659
|
+
if (!geometry) return null;
|
|
4660
|
+
const { dots } = geometry;
|
|
4661
|
+
const left = dots.left.xPct;
|
|
4662
|
+
const top = dots.top.yPct;
|
|
4663
|
+
return /* @__PURE__ */ jsx(
|
|
4664
|
+
"div",
|
|
4665
|
+
{
|
|
4666
|
+
"data-rivet-anchor-selected": record.anchorId,
|
|
4667
|
+
style: {
|
|
4668
|
+
position: "absolute",
|
|
4669
|
+
left: `${left}%`,
|
|
4670
|
+
top: `${top}%`,
|
|
4671
|
+
width: `${dots.right.xPct - left}%`,
|
|
4672
|
+
height: `${dots.bottom.yPct - top}%`,
|
|
4673
|
+
border: `1px solid ${record.color ?? DEFAULT_ANCHOR_COLOR}`,
|
|
4674
|
+
borderRadius: 2,
|
|
4675
|
+
// The box is a readout, never a target: presses belong to the element
|
|
4676
|
+
// underneath it, which is what put the selection here.
|
|
4677
|
+
pointerEvents: "none"
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4375
4682
|
function AnchorDots(props) {
|
|
4376
4683
|
const { record, options } = props;
|
|
4377
4684
|
if (!record.geometry || !record.element?.isConnected) return null;
|
|
@@ -4590,7 +4897,11 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4590
4897
|
if (!noSelect) store.selectNode(id, true);
|
|
4591
4898
|
return;
|
|
4592
4899
|
}
|
|
4593
|
-
|
|
4900
|
+
const onAnchor = target !== null && findSelectableAnchor(store.getNodeAnchors(id), target) !== null;
|
|
4901
|
+
const selectNode = () => {
|
|
4902
|
+
if (!noSelect && !store.isNodeSelected(id)) store.selectNode(id);
|
|
4903
|
+
};
|
|
4904
|
+
if (!onAnchor) selectNode();
|
|
4594
4905
|
if (noDrag) return;
|
|
4595
4906
|
startGroupDrag({
|
|
4596
4907
|
store,
|
|
@@ -4603,7 +4914,8 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4603
4914
|
clampToSwimlane,
|
|
4604
4915
|
swimlaneMargin,
|
|
4605
4916
|
swimlaneLabelWidth,
|
|
4606
|
-
onLaneChange
|
|
4917
|
+
onLaneChange,
|
|
4918
|
+
selectOnDrag: onAnchor ? selectNode : void 0
|
|
4607
4919
|
});
|
|
4608
4920
|
};
|
|
4609
4921
|
const node = store.nodes.get(id);
|
|
@@ -4760,6 +5072,176 @@ function NodeLayer({ containerRef, visibleIds }) {
|
|
|
4760
5072
|
/* @__PURE__ */ jsx(NodesSelection, {})
|
|
4761
5073
|
] });
|
|
4762
5074
|
}
|
|
5075
|
+
var overlayStyle2 = {
|
|
5076
|
+
position: "absolute",
|
|
5077
|
+
inset: 0,
|
|
5078
|
+
// Cursor chrome belongs to the canvas: clipped to the pane rather than
|
|
5079
|
+
// escaping into whatever app UI surrounds it.
|
|
5080
|
+
overflow: "hidden",
|
|
5081
|
+
pointerEvents: "none"
|
|
5082
|
+
};
|
|
5083
|
+
var cursorStyle = {
|
|
5084
|
+
position: "absolute",
|
|
5085
|
+
top: 0,
|
|
5086
|
+
left: 0,
|
|
5087
|
+
// The wrapper is a point, not a box: it sits at the pointer tip and whatever
|
|
5088
|
+
// the consumer renders lays itself out from there.
|
|
5089
|
+
width: 0,
|
|
5090
|
+
height: 0,
|
|
5091
|
+
pointerEvents: "none"
|
|
5092
|
+
};
|
|
5093
|
+
function PeerCursorLayer({
|
|
5094
|
+
component: Cursor
|
|
5095
|
+
}) {
|
|
5096
|
+
const { store, paneRef } = useRivetContext();
|
|
5097
|
+
const peers = usePeerIdentities();
|
|
5098
|
+
const refs = useRef(/* @__PURE__ */ new Map());
|
|
5099
|
+
const zoomRef = useRef(null);
|
|
5100
|
+
const zoomWritten = useRef(/* @__PURE__ */ new WeakSet());
|
|
5101
|
+
const position = useCallback(() => {
|
|
5102
|
+
const pane = paneRef.current;
|
|
5103
|
+
if (!pane) return;
|
|
5104
|
+
const viewport = store.getViewport();
|
|
5105
|
+
const zoomChanged = zoomRef.current !== viewport.zoom;
|
|
5106
|
+
zoomRef.current = viewport.zoom;
|
|
5107
|
+
const { width, height } = pane.getBoundingClientRect();
|
|
5108
|
+
const live = new Map(store.presence.getPeers().map((peer) => [peer.id, peer.cursor]));
|
|
5109
|
+
for (const [id, el] of refs.current) {
|
|
5110
|
+
if (!el) continue;
|
|
5111
|
+
const cursor = live.get(id);
|
|
5112
|
+
if (!cursor) {
|
|
5113
|
+
el.style.display = "none";
|
|
5114
|
+
continue;
|
|
5115
|
+
}
|
|
5116
|
+
el.style.display = "";
|
|
5117
|
+
const point = worldToScreen(cursor, viewport);
|
|
5118
|
+
const x = Math.min(Math.max(point.x, 0), width);
|
|
5119
|
+
const y = Math.min(Math.max(point.y, 0), height);
|
|
5120
|
+
const offscreen = x !== point.x || y !== point.y;
|
|
5121
|
+
el.style.transform = `translate(${x}px, ${y}px)`;
|
|
5122
|
+
el.style.visibility = offscreen ? "hidden" : "";
|
|
5123
|
+
if (offscreen) el.setAttribute("data-rivet-offscreen", "");
|
|
5124
|
+
else el.removeAttribute("data-rivet-offscreen");
|
|
5125
|
+
if (zoomChanged || !zoomWritten.current.has(el)) {
|
|
5126
|
+
el.style.setProperty("--rivet-zoom", String(viewport.zoom));
|
|
5127
|
+
zoomWritten.current.add(el);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}, [store, paneRef]);
|
|
5131
|
+
useEffect(() => {
|
|
5132
|
+
return store.subscribeCursorFrame(position);
|
|
5133
|
+
}, [store, position]);
|
|
5134
|
+
useLayoutEffect(position);
|
|
5135
|
+
return /* @__PURE__ */ jsx("div", { style: overlayStyle2, children: peers.map((peer) => /* @__PURE__ */ jsx(
|
|
5136
|
+
"div",
|
|
5137
|
+
{
|
|
5138
|
+
ref: (el) => {
|
|
5139
|
+
if (el) refs.current.set(peer.id, el);
|
|
5140
|
+
else refs.current.delete(peer.id);
|
|
5141
|
+
},
|
|
5142
|
+
style: { ...cursorStyle, display: "none" },
|
|
5143
|
+
"data-rivet-peer-cursor": peer.id,
|
|
5144
|
+
children: /* @__PURE__ */ jsx(Cursor, { peer })
|
|
5145
|
+
},
|
|
5146
|
+
peer.id
|
|
5147
|
+
)) });
|
|
5148
|
+
}
|
|
5149
|
+
var overlayStyle3 = {
|
|
5150
|
+
position: "absolute",
|
|
5151
|
+
inset: 0,
|
|
5152
|
+
// Claim chrome belongs to the canvas, so a box that has been panned off the
|
|
5153
|
+
// pane is clipped rather than left hanging over the app around it.
|
|
5154
|
+
overflow: "hidden",
|
|
5155
|
+
pointerEvents: "none"
|
|
5156
|
+
};
|
|
5157
|
+
var wrapperStyle2 = {
|
|
5158
|
+
position: "absolute",
|
|
5159
|
+
top: 0,
|
|
5160
|
+
left: 0,
|
|
5161
|
+
pointerEvents: "none"
|
|
5162
|
+
};
|
|
5163
|
+
function projectRect2(rect, viewport) {
|
|
5164
|
+
const origin = worldToScreen({ x: rect.x, y: rect.y }, viewport);
|
|
5165
|
+
return {
|
|
5166
|
+
x: origin.x,
|
|
5167
|
+
y: origin.y,
|
|
5168
|
+
width: rect.width * viewport.zoom,
|
|
5169
|
+
height: rect.height * viewport.zoom
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
function PeerOutlineLayer({
|
|
5173
|
+
component: Outline
|
|
5174
|
+
}) {
|
|
5175
|
+
const { store } = useRivetContext();
|
|
5176
|
+
const [entries, setEntries] = useState([]);
|
|
5177
|
+
const refs = useRef(/* @__PURE__ */ new Map());
|
|
5178
|
+
const zoomRef = useRef(null);
|
|
5179
|
+
const zoomWritten = useRef(/* @__PURE__ */ new WeakSet());
|
|
5180
|
+
const rectOf = useCallback(
|
|
5181
|
+
(target, viewport) => {
|
|
5182
|
+
if (target.kind === "edge") {
|
|
5183
|
+
const point = store.getPeerEdgeAnchors().get(target.id);
|
|
5184
|
+
return point ? { x: point.x, y: point.y, width: 0, height: 0 } : null;
|
|
5185
|
+
}
|
|
5186
|
+
const nodeId = target.kind === "node" ? target.id : target.nodeId;
|
|
5187
|
+
if (!store.nodes.has(nodeId)) return null;
|
|
5188
|
+
const node = store.getNodeRect(nodeId);
|
|
5189
|
+
if (target.kind === "node") return projectRect2(node, viewport);
|
|
5190
|
+
const record = store.anchors.get(handleKey(nodeId, target.anchorId));
|
|
5191
|
+
if (!record?.geometry) return null;
|
|
5192
|
+
return projectRect2(anchorRect(node, record.geometry), viewport);
|
|
5193
|
+
},
|
|
5194
|
+
[store]
|
|
5195
|
+
);
|
|
5196
|
+
const position = useCallback(() => {
|
|
5197
|
+
const viewport = store.getViewport();
|
|
5198
|
+
const zoomChanged = zoomRef.current !== viewport.zoom;
|
|
5199
|
+
zoomRef.current = viewport.zoom;
|
|
5200
|
+
for (const entry of entries) {
|
|
5201
|
+
const el = refs.current.get(entry.key);
|
|
5202
|
+
if (!el) continue;
|
|
5203
|
+
const rect = rectOf(entry.target, viewport);
|
|
5204
|
+
if (!rect) {
|
|
5205
|
+
el.style.display = "none";
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
el.style.display = "";
|
|
5209
|
+
el.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
|
|
5210
|
+
el.style.width = `${rect.width}px`;
|
|
5211
|
+
el.style.height = `${rect.height}px`;
|
|
5212
|
+
if (zoomChanged || !zoomWritten.current.has(el)) {
|
|
5213
|
+
el.style.setProperty("--rivet-zoom", String(viewport.zoom));
|
|
5214
|
+
zoomWritten.current.add(el);
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
}, [store, entries, rectOf]);
|
|
5218
|
+
useEffect(() => {
|
|
5219
|
+
return store.subscribeFrame(() => {
|
|
5220
|
+
const next = collectPeerOutlines(
|
|
5221
|
+
store.presence.getPeers(),
|
|
5222
|
+
store.presence.getPeerIdentities(),
|
|
5223
|
+
store.locks.getLocks()
|
|
5224
|
+
);
|
|
5225
|
+
setEntries((prev) => peerOutlinesEqual(prev, next) ? prev : next);
|
|
5226
|
+
position();
|
|
5227
|
+
});
|
|
5228
|
+
}, [store, position]);
|
|
5229
|
+
useLayoutEffect(position);
|
|
5230
|
+
return /* @__PURE__ */ jsx("div", { style: overlayStyle3, children: entries.map((entry) => /* @__PURE__ */ jsx(
|
|
5231
|
+
"div",
|
|
5232
|
+
{
|
|
5233
|
+
ref: (el) => {
|
|
5234
|
+
if (el) refs.current.set(entry.key, el);
|
|
5235
|
+
else refs.current.delete(entry.key);
|
|
5236
|
+
},
|
|
5237
|
+
style: { ...wrapperStyle2, display: "none" },
|
|
5238
|
+
"data-rivet-peer-outline": entry.key,
|
|
5239
|
+
"data-rivet-outline-kind": entry.target.kind,
|
|
5240
|
+
children: /* @__PURE__ */ jsx(Outline, { target: entry.target, claims: entry.claims, lock: entry.lock })
|
|
5241
|
+
},
|
|
5242
|
+
entry.key
|
|
5243
|
+
)) });
|
|
5244
|
+
}
|
|
4763
5245
|
var containerStyle2 = {
|
|
4764
5246
|
position: "absolute",
|
|
4765
5247
|
inset: 0,
|
|
@@ -5189,6 +5671,7 @@ function Rivet({
|
|
|
5189
5671
|
const backgroundCanvasRef = useRef(null);
|
|
5190
5672
|
const edgeCanvasRef = useRef(null);
|
|
5191
5673
|
const foregroundCanvasRef = useRef(null);
|
|
5674
|
+
const cursorCanvasRef = useRef(null);
|
|
5192
5675
|
useEffect(() => {
|
|
5193
5676
|
if (peers === void 0) return;
|
|
5194
5677
|
store.presence.setPeers(peers);
|
|
@@ -5206,6 +5689,7 @@ function Rivet({
|
|
|
5206
5689
|
store.setLockedNodes(lockedNodes);
|
|
5207
5690
|
}, [store, lockedNodes]);
|
|
5208
5691
|
useLocalPresence({ store, paneRef, onLocalPresence, throttleMs: cfg.presenceOptions.throttleMs });
|
|
5692
|
+
const outlineMode = !cfg.presenceOptions.renderOutlines ? "none" : cfg.presenceOptions.outlineComponent ? "dom" : "canvas";
|
|
5209
5693
|
const { visibleIds, controls, getViewportElements } = useRivetRuntime({
|
|
5210
5694
|
store,
|
|
5211
5695
|
paneRef,
|
|
@@ -5213,11 +5697,12 @@ function Rivet({
|
|
|
5213
5697
|
backgroundCanvasRef,
|
|
5214
5698
|
edgeCanvasRef,
|
|
5215
5699
|
foregroundCanvasRef,
|
|
5700
|
+
cursorCanvasRef,
|
|
5701
|
+
outlineMode,
|
|
5216
5702
|
swimlaneLanes: swimlanes.lanes,
|
|
5217
5703
|
edgeTypes: cfg.edgeTypes,
|
|
5218
5704
|
defaultEdgeOptions: cfg.defaultEdgeOptions,
|
|
5219
5705
|
anchorOptions: cfg.anchorOptions,
|
|
5220
|
-
presenceOptions: cfg.presenceOptions,
|
|
5221
5706
|
edgeRenderer: renderer,
|
|
5222
5707
|
edgesReconnectable,
|
|
5223
5708
|
isValidConnection: cfg.isValidConnection,
|
|
@@ -5306,6 +5791,8 @@ function Rivet({
|
|
|
5306
5791
|
/* @__PURE__ */ jsx("canvas", { ref: edgeCanvasRef, style: canvasStyle }),
|
|
5307
5792
|
/* @__PURE__ */ jsx(NodeLayer, { containerRef: nodeContainerRef, visibleIds }),
|
|
5308
5793
|
/* @__PURE__ */ jsx("canvas", { ref: foregroundCanvasRef, style: canvasStyle }),
|
|
5794
|
+
outlineMode === "dom" && cfg.presenceOptions.outlineComponent && /* @__PURE__ */ jsx(PeerOutlineLayer, { component: cfg.presenceOptions.outlineComponent }),
|
|
5795
|
+
cfg.presenceOptions.renderCursors && (cfg.presenceOptions.cursorComponent ? /* @__PURE__ */ jsx(PeerCursorLayer, { component: cfg.presenceOptions.cursorComponent }) : /* @__PURE__ */ jsx("canvas", { ref: cursorCanvasRef, style: canvasStyle })),
|
|
5309
5796
|
/* @__PURE__ */ jsx(EdgeLabelLayer, {}),
|
|
5310
5797
|
/* @__PURE__ */ jsx(SwimlaneOverlay, {}),
|
|
5311
5798
|
children,
|
|
@@ -5365,6 +5852,10 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
5365
5852
|
),
|
|
5366
5853
|
getNodes,
|
|
5367
5854
|
getNode: (id) => store.nodes.get(id),
|
|
5855
|
+
// The store's own `getNodeRect` answers for any id, falling back to the
|
|
5856
|
+
// origin and the default size; a public caller needs to tell "it's there,
|
|
5857
|
+
// at 0,0" from "it isn't there".
|
|
5858
|
+
getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
|
|
5368
5859
|
setNodes,
|
|
5369
5860
|
addNodes: (nodes) => {
|
|
5370
5861
|
for (const node of Array.isArray(nodes) ? nodes : [nodes]) store.addNode(node);
|
|
@@ -5399,8 +5890,12 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
5399
5890
|
},
|
|
5400
5891
|
unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
|
|
5401
5892
|
remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
|
|
5893
|
+
getAnchor: (nodeId, anchorId) => store.getNodeAnchors(nodeId).find((record) => record.anchorId === anchorId) ?? null,
|
|
5894
|
+
setSelectedAnchors: (refs) => store.setSelectedAnchors(refs),
|
|
5895
|
+
getSelectedAnchors: () => store.getSelectedAnchors(),
|
|
5402
5896
|
setPeerCursor: (peerId, point) => store.presence.setPeerCursor(peerId, point),
|
|
5403
5897
|
setPeerNodeTransform: (peerId, nodeId, rect) => store.presence.setPeerNodeTransform(peerId, nodeId, rect),
|
|
5898
|
+
setPeerConnection: (peerId, connection) => store.presence.setPeerConnection(peerId, connection),
|
|
5404
5899
|
removePeer: (peerId) => store.presence.removePeer(peerId),
|
|
5405
5900
|
releaseNodeGesture: (id, ids) => store.endNodeGesture(id, ids),
|
|
5406
5901
|
copy: (ids) => {
|
|
@@ -5442,6 +5937,6 @@ function useRivet() {
|
|
|
5442
5937
|
// src/index.ts
|
|
5443
5938
|
var VERSION = "0.0.0";
|
|
5444
5939
|
|
|
5445
|
-
export { BUILTIN_EDGE_TYPES, BUILTIN_NODE_TYPES, Canvas2DEdgeRenderer, Controls, DEFAULT_ALIGNMENT_HYSTERESIS, DefaultNode, GroupNode, Handle, MiniMap, NodeResizer, Rivet, VERSION, alignRect, applyEdgeChanges, applyNodeChanges,
|
|
5940
|
+
export { BUILTIN_EDGE_TYPES, BUILTIN_NODE_TYPES, Canvas2DEdgeRenderer, Controls, DEFAULT_ALIGNMENT_HYSTERESIS, DefaultNode, GroupNode, Handle, MiniMap, NodeResizer, Rivet, VERSION, alignRect, applyEdgeChanges, applyNodeChanges, canvas2DEdgeRenderer, cloneElements, createRivetStore, edgeEndpointHandle, facingSide, getBezierPath, getSmoothStepPath, getStepPath, getStraightPath, handleKey, parseEdgeEndpoint, parseSideHandleId, rectsIntersect, resolveFacingSide, resolveFacingSideAmong, screenToWorld, useRivet, viewportToCss, visibleWorldRect, worldToScreen, zoomAt };
|
|
5446
5941
|
//# sourceMappingURL=index.js.map
|
|
5447
5942
|
//# sourceMappingURL=index.js.map
|