@kolosal-ai/rivet 0.4.0 → 0.6.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 +3 -3
- package/dist/anchor/index.js +1 -1
- package/dist/{chunk-W5HBJ4VF.js → chunk-76GHEXDP.js} +37 -5
- package/dist/chunk-76GHEXDP.js.map +1 -0
- package/dist/index.d.ts +52 -8
- package/dist/index.js +333 -61
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +3 -3
- package/dist/{registry-Z_oiPyjm.d.ts → registry-DPfWNdkC.d.ts} +58 -11
- package/dist/svg/index.d.ts +2 -2
- package/dist/swimlane/index.d.ts +2 -2
- package/dist/{types-D3aldmsO.d.ts → types-tkMbskgC.d.ts} +7 -0
- package/dist/{use-node-lock-CbS59CRE.d.ts → use-node-lock-CKtOwBrI.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-W5HBJ4VF.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { findSelectableAnchor, parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId,
|
|
2
|
-
import { snapToGrid, boundingRect, clampChildToParent,
|
|
1
|
+
import { findSelectableAnchor, parseAnchorHandleId, orderedSelectableAnchors, ANCHOR_SIDES, anchorDotHandleId, strayPoint, anchorHandleId, parseAnchorAutoHandleId, anchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS, anchorRect, strayPlacementPct } from './chunk-76GHEXDP.js';
|
|
2
|
+
import { snapToGrid, anchorTargetKey, boundingRect, clampChildToParent, createPresenceRegistry, createLockRegistry, nodeControlledEqual, buildChildIndex, collectDescendants, worldPosition, nodeDepth, descendantIds, hasAncestorIn, edgeTargetKey, DEFAULT_PRESENCE_OPTIONS, collectPeerOutlines, peerOutlinesEqual, usePeerIdentities, serializeGraph, presenceTargetKey, peerColor, isNodeTarget, presenceTargetEdgeId, parsePresenceTarget } from './chunk-TT2N7KWU.js';
|
|
3
3
|
export { LOCK_DEFAULT_REFUSED, boundingRect, clampChildToParent, serializeGraph, snapToGrid, useNodeLock, useNodeLockAllows } from './chunk-TT2N7KWU.js';
|
|
4
4
|
import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
5
5
|
export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
|
|
@@ -888,7 +888,8 @@ function resolveEnd(edge, role, node, peer, frame) {
|
|
|
888
888
|
rect,
|
|
889
889
|
nodeRect(peer, worldPositions),
|
|
890
890
|
anchorId ? [] : reachableSides(node.id, role, handles),
|
|
891
|
-
extras?.displayedSides?.get(key)
|
|
891
|
+
extras?.displayedSides?.get(key),
|
|
892
|
+
extras?.alignmentHysteresis
|
|
892
893
|
);
|
|
893
894
|
extras?.displayedSides?.set(key, side);
|
|
894
895
|
return endAt(side);
|
|
@@ -938,6 +939,20 @@ function distanceToSegment(p, a, b) {
|
|
|
938
939
|
}
|
|
939
940
|
|
|
940
941
|
// src/store.ts
|
|
942
|
+
function unionRect(rects) {
|
|
943
|
+
let left = Number.POSITIVE_INFINITY;
|
|
944
|
+
let top = Number.POSITIVE_INFINITY;
|
|
945
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
946
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
947
|
+
for (const rect of rects) {
|
|
948
|
+
left = Math.min(left, rect.left);
|
|
949
|
+
top = Math.min(top, rect.top);
|
|
950
|
+
right = Math.max(right, rect.right);
|
|
951
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
952
|
+
}
|
|
953
|
+
if (!Number.isFinite(left)) return { left: 0, top: 0, width: 0, height: 0 };
|
|
954
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
955
|
+
}
|
|
941
956
|
var RivetGraphStore = class {
|
|
942
957
|
nodes = /* @__PURE__ */ new Map();
|
|
943
958
|
edges = /* @__PURE__ */ new Map();
|
|
@@ -984,6 +999,7 @@ var RivetGraphStore = class {
|
|
|
984
999
|
nodeDragging = false;
|
|
985
1000
|
nodeResizing = false;
|
|
986
1001
|
edgeAlignment = "manual";
|
|
1002
|
+
alignmentHysteresis = DEFAULT_ALIGNMENT_HYSTERESIS;
|
|
987
1003
|
draggingNodeIds = /* @__PURE__ */ new Set();
|
|
988
1004
|
heldNodeIds = /* @__PURE__ */ new Set();
|
|
989
1005
|
heldListeners = /* @__PURE__ */ new Set();
|
|
@@ -1467,6 +1483,13 @@ var RivetGraphStore = class {
|
|
|
1467
1483
|
this.edgeAlignment = mode;
|
|
1468
1484
|
};
|
|
1469
1485
|
getEdgeAlignment = () => this.edgeAlignment;
|
|
1486
|
+
setAlignmentHysteresis = (ratio) => {
|
|
1487
|
+
const next = Number.isFinite(ratio) ? Math.max(1, ratio) : DEFAULT_ALIGNMENT_HYSTERESIS;
|
|
1488
|
+
if (next === this.alignmentHysteresis) return;
|
|
1489
|
+
this.alignmentHysteresis = next;
|
|
1490
|
+
this.requestRender();
|
|
1491
|
+
};
|
|
1492
|
+
getAlignmentHysteresis = () => this.alignmentHysteresis;
|
|
1470
1493
|
getDisplayedSides = () => this.displayedSides;
|
|
1471
1494
|
getDraggingNodeIds = () => this.draggingNodeIds;
|
|
1472
1495
|
/**
|
|
@@ -1487,22 +1510,77 @@ var RivetGraphStore = class {
|
|
|
1487
1510
|
*/
|
|
1488
1511
|
reachableFacingSide(nodeId, handleId, rect, peerRect, end) {
|
|
1489
1512
|
const allowed = parseAnchorHandleId(handleId) || parseAnchorAutoHandleId(handleId) ? [] : reachableSides(nodeId, end, this.handles);
|
|
1490
|
-
return resolveFacingSideAmong(rect, peerRect, allowed);
|
|
1513
|
+
return resolveFacingSideAmong(rect, peerRect, allowed, void 0, this.alignmentHysteresis);
|
|
1491
1514
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1515
|
+
/**
|
|
1516
|
+
* What an end does about its new side, decided without choosing a handle yet.
|
|
1517
|
+
*
|
|
1518
|
+
* The choice is held back because it isn't an independent one: several ends
|
|
1519
|
+
* can want the same side of the same node, and which handle each takes is
|
|
1520
|
+
* only right relative to the others (see {@link assignSideHandles}).
|
|
1521
|
+
*/
|
|
1522
|
+
planEndHandle(nodeId, handleId, side, end) {
|
|
1523
|
+
if (!handleId || parseAnchorAutoHandleId(handleId)) return { kind: "fixed", handleId };
|
|
1494
1524
|
const anchor = parseAnchorHandleId(handleId);
|
|
1495
|
-
if (anchor)
|
|
1525
|
+
if (anchor) {
|
|
1526
|
+
return {
|
|
1527
|
+
kind: "fixed",
|
|
1528
|
+
handleId: anchor.side === side ? handleId : anchorHandleId(anchor.anchorId, side)
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1496
1531
|
const record = this.handles.get(handleKey(nodeId, handleId));
|
|
1497
|
-
if (!record)
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1532
|
+
if (!record) {
|
|
1533
|
+
return { kind: "fixed", handleId: parseSideHandleId(handleId) ? side : handleId };
|
|
1534
|
+
}
|
|
1535
|
+
if (record.position === side) return { kind: "fixed", handleId };
|
|
1536
|
+
return { kind: "assign", nodeId, side, role: end };
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Hand out one node side's handles to the ends that want it, **in the order
|
|
1540
|
+
* their peers sit along that side**.
|
|
1541
|
+
*
|
|
1542
|
+
* Occupancy alone spreads edges across a side but says nothing about which
|
|
1543
|
+
* gets which, so the assignment fell out of registry order — and an edge from
|
|
1544
|
+
* a node above taking the lower handle while one from below takes the upper
|
|
1545
|
+
* is two lines crossing for no reason. Sorting both sides of the assignment
|
|
1546
|
+
* by the same axis removes exactly those crossings: peers in order, handles
|
|
1547
|
+
* in order, paired off.
|
|
1548
|
+
*
|
|
1549
|
+
* This is local, not global. It can't uncross edges that don't share a side,
|
|
1550
|
+
* which needs a pass over pairs rather than over one node's occupancy.
|
|
1551
|
+
*/
|
|
1552
|
+
assignSideHandles(requests, occupied) {
|
|
1553
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
1554
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const request of requests) {
|
|
1556
|
+
const key = `${request.nodeId}\0${request.side}`;
|
|
1557
|
+
const group = groups.get(key);
|
|
1558
|
+
if (group) group.push(request);
|
|
1559
|
+
else groups.set(key, [request]);
|
|
1560
|
+
}
|
|
1561
|
+
for (const group of groups.values()) {
|
|
1562
|
+
const first = group[0];
|
|
1563
|
+
if (!first) continue;
|
|
1564
|
+
const { nodeId, side } = first;
|
|
1565
|
+
const vertical = side === "left" || side === "right";
|
|
1566
|
+
const candidates = [...this.handles.values()].filter((handle) => handle.nodeId === nodeId && handle.position === side).map((handle, index) => ({ handle, index })).sort((a, b) => {
|
|
1567
|
+
const delta = vertical ? a.handle.offset.y - b.handle.offset.y : a.handle.offset.x - b.handle.offset.x;
|
|
1568
|
+
return delta !== 0 ? delta : a.index - b.index;
|
|
1569
|
+
}).map(({ handle }) => handle);
|
|
1570
|
+
const ordered = [...group].sort(
|
|
1571
|
+
(a, b) => a.along !== b.along ? a.along - b.along : a.seq - b.seq
|
|
1572
|
+
);
|
|
1573
|
+
for (const request of ordered) {
|
|
1574
|
+
const wanted = request.role === "source" ? ["source", "either"] : ["target", "either"];
|
|
1575
|
+
const usable = candidates.filter((handle) => wanted.includes(handle.type));
|
|
1576
|
+
const free = usable.find((handle) => !occupied.has(handleKey(nodeId, handle.handleId)));
|
|
1577
|
+
const picked = free ?? usable[0];
|
|
1578
|
+
if (!picked) continue;
|
|
1579
|
+
occupied.add(handleKey(nodeId, picked.handleId));
|
|
1580
|
+
assigned.set(request.id, picked.handleId);
|
|
1503
1581
|
}
|
|
1504
1582
|
}
|
|
1505
|
-
return
|
|
1583
|
+
return assigned;
|
|
1506
1584
|
}
|
|
1507
1585
|
/**
|
|
1508
1586
|
* Under `"on-move"` and `"live"`: re-side both ends of the moved node's
|
|
@@ -1522,6 +1600,15 @@ var RivetGraphStore = class {
|
|
|
1522
1600
|
if (this.edgeAlignment === "manual") return;
|
|
1523
1601
|
const live = this.edgeAlignment === "live";
|
|
1524
1602
|
const changes = [];
|
|
1603
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
1604
|
+
for (const edge of this.edges.values()) {
|
|
1605
|
+
const moved = edge.source === nodeId || edge.target === nodeId;
|
|
1606
|
+
if (moved) continue;
|
|
1607
|
+
if (edge.sourceHandle) occupied.add(handleKey(edge.source, edge.sourceHandle));
|
|
1608
|
+
if (edge.targetHandle) occupied.add(handleKey(edge.target, edge.targetHandle));
|
|
1609
|
+
}
|
|
1610
|
+
const planned = [];
|
|
1611
|
+
const requests = [];
|
|
1525
1612
|
for (const [edgeId, edge] of this.edges) {
|
|
1526
1613
|
if (edge.source !== nodeId && edge.target !== nodeId) continue;
|
|
1527
1614
|
if (edge.source === edge.target) continue;
|
|
@@ -1532,18 +1619,42 @@ var RivetGraphStore = class {
|
|
|
1532
1619
|
const targetKey = displayedSideKey(edgeId, "target");
|
|
1533
1620
|
const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? this.reachableFacingSide(edge.source, edge.sourceHandle, sourceRect, targetRect, "source");
|
|
1534
1621
|
const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ?? this.reachableFacingSide(edge.target, edge.targetHandle, targetRect, sourceRect, "target");
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
)
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1622
|
+
const source = this.planEndHandle(edge.source, edge.sourceHandle, sourceSide, "source");
|
|
1623
|
+
const target = this.planEndHandle(edge.target, edge.targetHandle, targetSide, "target");
|
|
1624
|
+
if (source.kind === "fixed" && source.handleId) {
|
|
1625
|
+
occupied.add(handleKey(edge.source, source.handleId));
|
|
1626
|
+
}
|
|
1627
|
+
if (target.kind === "fixed" && target.handleId) {
|
|
1628
|
+
occupied.add(handleKey(edge.target, target.handleId));
|
|
1629
|
+
}
|
|
1630
|
+
const along = (side, peer) => side === "left" || side === "right" ? peer.y + peer.height / 2 : peer.x + peer.width / 2;
|
|
1631
|
+
if (source.kind === "assign") {
|
|
1632
|
+
requests.push({
|
|
1633
|
+
id: `${edgeId}:source`,
|
|
1634
|
+
nodeId: source.nodeId,
|
|
1635
|
+
side: source.side,
|
|
1636
|
+
role: source.role,
|
|
1637
|
+
along: along(source.side, targetRect),
|
|
1638
|
+
seq: requests.length
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
if (target.kind === "assign") {
|
|
1642
|
+
requests.push({
|
|
1643
|
+
id: `${edgeId}:target`,
|
|
1644
|
+
nodeId: target.nodeId,
|
|
1645
|
+
side: target.side,
|
|
1646
|
+
role: target.role,
|
|
1647
|
+
along: along(target.side, sourceRect),
|
|
1648
|
+
seq: requests.length
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
planned.push({ edgeId, edge, source, target, sourceKey, targetKey });
|
|
1652
|
+
}
|
|
1653
|
+
const assigned = this.assignSideHandles(requests, occupied);
|
|
1654
|
+
const resolve = (plan, id, fallback) => plan.kind === "fixed" ? plan.handleId : assigned.get(id) ?? fallback;
|
|
1655
|
+
for (const { edgeId, edge, source, target, sourceKey, targetKey } of planned) {
|
|
1656
|
+
const sourceHandle = resolve(source, `${edgeId}:source`, edge.sourceHandle);
|
|
1657
|
+
const targetHandle = resolve(target, `${edgeId}:target`, edge.targetHandle);
|
|
1547
1658
|
if (live) {
|
|
1548
1659
|
const pinned = (h) => Boolean(h) && !parseAnchorAutoHandleId(h);
|
|
1549
1660
|
if (pinned(edge.sourceHandle)) this.displayedSides.delete(sourceKey);
|
|
@@ -1873,16 +1984,20 @@ var RivetGraphStore = class {
|
|
|
1873
1984
|
*/
|
|
1874
1985
|
measureAnchor(key) {
|
|
1875
1986
|
const record = this.anchors.get(key);
|
|
1876
|
-
if (!record
|
|
1877
|
-
const
|
|
1987
|
+
if (!record) return false;
|
|
1988
|
+
const live = record.elements.filter((element) => element.isConnected);
|
|
1989
|
+
const firstElement = live[0];
|
|
1990
|
+
const lastElement = live[live.length - 1];
|
|
1991
|
+
if (!firstElement || !lastElement) return false;
|
|
1878
1992
|
const nodeEl = this.nodeElements.get(record.nodeId);
|
|
1879
1993
|
if (!nodeEl) return false;
|
|
1880
1994
|
const root = nodeEl.getBoundingClientRect();
|
|
1881
|
-
const box = element.getBoundingClientRect();
|
|
1995
|
+
const box = unionRect(live.map((element) => element.getBoundingClientRect()));
|
|
1882
1996
|
if (root.width <= 0 || root.height <= 0 || box.width <= 0 && box.height <= 0) return false;
|
|
1883
|
-
const
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1997
|
+
const firstRects = firstElement.getClientRects();
|
|
1998
|
+
const lastRects = lastElement.getClientRects();
|
|
1999
|
+
const first = firstRects.item(0) ?? firstElement.getBoundingClientRect();
|
|
2000
|
+
const last = lastRects.item(lastRects.length - 1) ?? lastElement.getBoundingClientRect();
|
|
1886
2001
|
const geometry = computeAnchorGeometry(root, first, last, box);
|
|
1887
2002
|
if (anchorGeometryEqual(record.geometry, geometry)) return false;
|
|
1888
2003
|
this.anchors.set(key, { ...record, geometry });
|
|
@@ -1891,6 +2006,7 @@ var RivetGraphStore = class {
|
|
|
1891
2006
|
registerAnchor = (nodeId, anchorId, element, options) => {
|
|
1892
2007
|
const key = handleKey(nodeId, anchorId);
|
|
1893
2008
|
const prev = this.anchors.get(key);
|
|
2009
|
+
const elements = Array.isArray(element) ? [...element] : [element];
|
|
1894
2010
|
this.anchors.set(key, {
|
|
1895
2011
|
nodeId,
|
|
1896
2012
|
anchorId,
|
|
@@ -1898,7 +2014,8 @@ var RivetGraphStore = class {
|
|
|
1898
2014
|
strays: options?.strays ?? prev?.strays,
|
|
1899
2015
|
selectable: options?.selectable ?? prev?.selectable,
|
|
1900
2016
|
data: options?.data ?? prev?.data,
|
|
1901
|
-
|
|
2017
|
+
elements,
|
|
2018
|
+
element: elements[0] ?? null,
|
|
1902
2019
|
// Keep the last-known geometry until the next successful measurement, so
|
|
1903
2020
|
// a re-bind (display remount, editor DOM swap) never blanks the chrome.
|
|
1904
2021
|
geometry: prev?.geometry ?? null
|
|
@@ -1910,8 +2027,8 @@ var RivetGraphStore = class {
|
|
|
1910
2027
|
detachAnchor = (nodeId, anchorId) => {
|
|
1911
2028
|
const key = handleKey(nodeId, anchorId);
|
|
1912
2029
|
const record = this.anchors.get(key);
|
|
1913
|
-
if (!record || record.
|
|
1914
|
-
this.anchors.set(key, { ...record, element: null });
|
|
2030
|
+
if (!record || record.elements.length === 0) return;
|
|
2031
|
+
this.anchors.set(key, { ...record, elements: [], element: null });
|
|
1915
2032
|
this.bumpAnchors(nodeId);
|
|
1916
2033
|
};
|
|
1917
2034
|
unregisterAnchor = (nodeId, anchorId) => {
|
|
@@ -3201,6 +3318,9 @@ var HELD_LINE_WIDTH = 2;
|
|
|
3201
3318
|
var SELECTED_LINE_WIDTH = 1.5;
|
|
3202
3319
|
var SELECTED_DASH = [4, 4];
|
|
3203
3320
|
var OUTLINE_RADIUS = 6;
|
|
3321
|
+
var ANCHOR_CLAIM_ALPHA = 0.28;
|
|
3322
|
+
var ANCHOR_CLAIM_LINE_WIDTH = 1;
|
|
3323
|
+
var ANCHOR_CLAIM_RADIUS = 3;
|
|
3204
3324
|
var CURSOR_POINTS = [
|
|
3205
3325
|
{ x: 0, y: 0 },
|
|
3206
3326
|
{ x: 0, y: 16 },
|
|
@@ -3239,6 +3359,17 @@ function projectRect(rect, viewport) {
|
|
|
3239
3359
|
function onScreen(rect, size) {
|
|
3240
3360
|
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;
|
|
3241
3361
|
}
|
|
3362
|
+
function drawAnchorClaim(ctx, rect, color) {
|
|
3363
|
+
traceRect(ctx, rect, ANCHOR_CLAIM_RADIUS);
|
|
3364
|
+
ctx.globalAlpha = ANCHOR_CLAIM_ALPHA;
|
|
3365
|
+
ctx.fillStyle = color;
|
|
3366
|
+
ctx.fill();
|
|
3367
|
+
ctx.globalAlpha = 1;
|
|
3368
|
+
ctx.strokeStyle = color;
|
|
3369
|
+
ctx.lineWidth = ANCHOR_CLAIM_LINE_WIDTH;
|
|
3370
|
+
ctx.setLineDash([]);
|
|
3371
|
+
ctx.stroke();
|
|
3372
|
+
}
|
|
3242
3373
|
function drawNodeOutline(ctx, rect, color, mode) {
|
|
3243
3374
|
ctx.strokeStyle = color;
|
|
3244
3375
|
ctx.lineWidth = mode === "selected" ? SELECTED_LINE_WIDTH : HELD_LINE_WIDTH;
|
|
@@ -3271,18 +3402,31 @@ function drawCursor(ctx, point, color, name) {
|
|
|
3271
3402
|
ctx.fillStyle = "#ffffff";
|
|
3272
3403
|
ctx.fillText(name, x + LABEL_PADDING_X, y + LABEL_HEIGHT / 2);
|
|
3273
3404
|
}
|
|
3274
|
-
function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
|
|
3405
|
+
function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, getAnchorRect, locks }) {
|
|
3275
3406
|
if (peers.length === 0 && locks.size === 0) return;
|
|
3276
3407
|
ctx.save();
|
|
3277
3408
|
ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
|
|
3278
3409
|
const painted = /* @__PURE__ */ new Set();
|
|
3410
|
+
const paintAnchor = (key, color) => {
|
|
3411
|
+
const target = parsePresenceTarget(key);
|
|
3412
|
+
if (target.kind !== "anchor") return;
|
|
3413
|
+
const rect = getAnchorRect(target.nodeId, target.anchorId);
|
|
3414
|
+
if (!rect) return;
|
|
3415
|
+
const screen = projectRect(rect, viewport);
|
|
3416
|
+
if (onScreen(screen, size)) drawAnchorClaim(ctx, screen, color);
|
|
3417
|
+
};
|
|
3279
3418
|
for (const peer of peers) {
|
|
3280
3419
|
const held = /* @__PURE__ */ new Set([
|
|
3281
3420
|
...peer.holding.filter((key) => isNodeTarget(key)),
|
|
3282
3421
|
...peer.transforms.keys()
|
|
3283
3422
|
]);
|
|
3284
3423
|
for (const id of peer.selection) {
|
|
3285
|
-
if (!isNodeTarget(id)
|
|
3424
|
+
if (!isNodeTarget(id)) {
|
|
3425
|
+
painted.add(id);
|
|
3426
|
+
paintAnchor(id, peer.color);
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
if (held.has(id)) continue;
|
|
3286
3430
|
const rect = getNodeRect(id);
|
|
3287
3431
|
if (!rect) continue;
|
|
3288
3432
|
const screen = projectRect(rect, viewport);
|
|
@@ -3299,7 +3443,11 @@ function drawPeerOutlines(ctx, peers, { viewport, size, getNodeRect, locks }) {
|
|
|
3299
3443
|
if (locks.size > 0) {
|
|
3300
3444
|
const declared = new Map(peers.map((peer) => [peer.id, peer.color]));
|
|
3301
3445
|
for (const [id, holderId] of locks) {
|
|
3302
|
-
if (
|
|
3446
|
+
if (painted.has(id)) continue;
|
|
3447
|
+
if (!isNodeTarget(id)) {
|
|
3448
|
+
paintAnchor(id, declared.get(holderId) ?? peerColor(holderId));
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3303
3451
|
const rect = getNodeRect(id);
|
|
3304
3452
|
if (!rect) continue;
|
|
3305
3453
|
const screen = projectRect(rect, viewport);
|
|
@@ -3560,6 +3708,12 @@ function useRivetRuntime(params) {
|
|
|
3560
3708
|
if (store.presence.getTransformVersion() !== version) schedule();
|
|
3561
3709
|
else scheduleCursors();
|
|
3562
3710
|
};
|
|
3711
|
+
const peerAnchorRect = (nodeId, anchorId) => {
|
|
3712
|
+
if (!store.nodes.has(nodeId)) return null;
|
|
3713
|
+
const record = store.anchors.get(handleKey(nodeId, anchorId));
|
|
3714
|
+
if (!record?.geometry) return null;
|
|
3715
|
+
return anchorRect(store.getNodeRect(nodeId), record.geometry);
|
|
3716
|
+
};
|
|
3563
3717
|
const anchorPlacement = (nodeId, anchorId, side) => {
|
|
3564
3718
|
const record = store.anchors.get(handleKey(nodeId, anchorId));
|
|
3565
3719
|
return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
|
|
@@ -3600,6 +3754,7 @@ function useRivetRuntime(params) {
|
|
|
3600
3754
|
worldPositions,
|
|
3601
3755
|
displayedSides: store.getDisplayedSides(),
|
|
3602
3756
|
liveAlignNodes,
|
|
3757
|
+
alignmentHysteresis: store.getAlignmentHysteresis(),
|
|
3603
3758
|
anchorPlacement,
|
|
3604
3759
|
peerEdges
|
|
3605
3760
|
});
|
|
@@ -3633,6 +3788,7 @@ function useRivetRuntime(params) {
|
|
|
3633
3788
|
viewport,
|
|
3634
3789
|
size,
|
|
3635
3790
|
getNodeRect: (id) => store.nodes.has(id) ? store.getNodeRect(id) : null,
|
|
3791
|
+
getAnchorRect: peerAnchorRect,
|
|
3636
3792
|
locks: store.locks.getLocks()
|
|
3637
3793
|
});
|
|
3638
3794
|
}
|
|
@@ -4567,38 +4723,49 @@ function clampToParent(store, parentId, position, size) {
|
|
|
4567
4723
|
}
|
|
4568
4724
|
|
|
4569
4725
|
// src/anchor/select-gesture.ts
|
|
4570
|
-
function attachAnchorSelection(store, record) {
|
|
4571
|
-
const
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4726
|
+
function attachAnchorSelection(store, record, multiSelectionKeys = []) {
|
|
4727
|
+
const elements = record.elements.filter(
|
|
4728
|
+
(candidate) => candidate instanceof HTMLElement || candidate instanceof SVGElement
|
|
4729
|
+
);
|
|
4730
|
+
if (!record.selectable || elements.length === 0) return null;
|
|
4575
4731
|
const { nodeId, anchorId } = record;
|
|
4576
|
-
const previous = element.style.pointerEvents;
|
|
4577
|
-
element.style.pointerEvents = "auto";
|
|
4732
|
+
const previous = elements.map((element) => element.style.pointerEvents);
|
|
4733
|
+
for (const element of elements) element.style.pointerEvents = "auto";
|
|
4578
4734
|
const onPointerDown = (event) => {
|
|
4579
4735
|
if (!(event instanceof MouseEvent)) return;
|
|
4580
4736
|
if (event.button !== 0) return;
|
|
4581
4737
|
const originX = event.clientX;
|
|
4582
4738
|
const originY = event.clientY;
|
|
4739
|
+
const additive = keyHeld(event, multiSelectionKeys);
|
|
4583
4740
|
const onUp = (upEvent) => {
|
|
4584
4741
|
window.removeEventListener("pointerup", onUp);
|
|
4585
4742
|
if (!(upEvent instanceof MouseEvent)) return;
|
|
4586
4743
|
const moved = Math.hypot(upEvent.clientX - originX, upEvent.clientY - originY);
|
|
4587
4744
|
if (moved >= DRAG_THRESHOLD) return;
|
|
4588
4745
|
if (!store.locks.allows(anchorTargetKey(nodeId, anchorId), "select")) return;
|
|
4589
|
-
|
|
4746
|
+
if (!additive) {
|
|
4747
|
+
store.setSelectedAnchors([{ nodeId, anchorId }]);
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
const selected = store.getSelectedAnchors();
|
|
4751
|
+
const without = selected.filter((ref) => ref.nodeId !== nodeId || ref.anchorId !== anchorId);
|
|
4752
|
+
store.setSelectedAnchors(
|
|
4753
|
+
without.length === selected.length ? [...selected, { nodeId, anchorId }] : without
|
|
4754
|
+
);
|
|
4590
4755
|
};
|
|
4591
4756
|
window.addEventListener("pointerup", onUp);
|
|
4592
4757
|
};
|
|
4593
|
-
element.addEventListener("pointerdown", onPointerDown);
|
|
4758
|
+
for (const element of elements) element.addEventListener("pointerdown", onPointerDown);
|
|
4594
4759
|
return () => {
|
|
4595
|
-
|
|
4596
|
-
|
|
4760
|
+
elements.forEach((element, index) => {
|
|
4761
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
4762
|
+
element.style.pointerEvents = previous[index] ?? "";
|
|
4763
|
+
});
|
|
4597
4764
|
};
|
|
4598
4765
|
}
|
|
4599
4766
|
var DEFAULT_ANCHOR_COLOR = "#6366f1";
|
|
4600
4767
|
function NodeAnchors({ nodeId }) {
|
|
4601
|
-
const { store, anchorOptions } = useRivetContext();
|
|
4768
|
+
const { store, anchorOptions, multiSelectionKeys } = useRivetContext();
|
|
4602
4769
|
const subscribeAnchors = useCallback(
|
|
4603
4770
|
(listener) => store.subscribeNodeAnchors(nodeId, listener),
|
|
4604
4771
|
[store, nodeId]
|
|
@@ -4618,13 +4785,13 @@ function NodeAnchors({ nodeId }) {
|
|
|
4618
4785
|
useEffect(() => {
|
|
4619
4786
|
const cleanups = [];
|
|
4620
4787
|
for (const record of store.getNodeAnchors(nodeId)) {
|
|
4621
|
-
const cleanup = attachAnchorSelection(store, record);
|
|
4788
|
+
const cleanup = attachAnchorSelection(store, record, multiSelectionKeys);
|
|
4622
4789
|
if (cleanup) cleanups.push(cleanup);
|
|
4623
4790
|
}
|
|
4624
4791
|
return () => {
|
|
4625
4792
|
for (const cleanup of cleanups) cleanup();
|
|
4626
4793
|
};
|
|
4627
|
-
}, [store, nodeId, anchorsVersion]);
|
|
4794
|
+
}, [store, nodeId, anchorsVersion, multiSelectionKeys]);
|
|
4628
4795
|
const records = store.getNodeAnchors(nodeId);
|
|
4629
4796
|
if (records.length === 0) return null;
|
|
4630
4797
|
const dotsVisible = Boolean(store.nodes.get(nodeId)?.hovered);
|
|
@@ -4681,7 +4848,7 @@ function AnchorSelectionOutline({ record }) {
|
|
|
4681
4848
|
}
|
|
4682
4849
|
function AnchorDots(props) {
|
|
4683
4850
|
const { record, options } = props;
|
|
4684
|
-
if (!record.geometry || !record.element
|
|
4851
|
+
if (!record.geometry || !record.elements.some((element) => element.isConnected)) return null;
|
|
4685
4852
|
const dots = record.geometry.dots;
|
|
4686
4853
|
return /* @__PURE__ */ jsx(Fragment, { children: ANCHOR_SIDES.map((side) => {
|
|
4687
4854
|
const dot = dots[side];
|
|
@@ -4712,11 +4879,11 @@ function AnchorDots(props) {
|
|
|
4712
4879
|
function AnchorStrayHandles(props) {
|
|
4713
4880
|
const { record, options } = props;
|
|
4714
4881
|
const geometry = record.geometry;
|
|
4715
|
-
if (!geometry) return null;
|
|
4716
4882
|
const sides = props.connectedSides && props.connectedSides.length > 0 ? props.connectedSides : [options.defaultSide];
|
|
4883
|
+
useLiveStrayPlacement(record, sides, options);
|
|
4884
|
+
if (!geometry) return null;
|
|
4717
4885
|
return /* @__PURE__ */ jsx(Fragment, { children: sides.map((side) => {
|
|
4718
|
-
const
|
|
4719
|
-
const axis = side === "top" || side === "bottom" ? { left: `${placed}%` } : { top: `${placed}%` };
|
|
4886
|
+
const point = strayPoint(geometry, side, options);
|
|
4720
4887
|
return /* @__PURE__ */ jsx(
|
|
4721
4888
|
Handle,
|
|
4722
4889
|
{
|
|
@@ -4724,17 +4891,89 @@ function AnchorStrayHandles(props) {
|
|
|
4724
4891
|
type: "either",
|
|
4725
4892
|
position: side,
|
|
4726
4893
|
style: {
|
|
4894
|
+
left: `${point.xPct}%`,
|
|
4895
|
+
top: `${point.yPct}%`,
|
|
4896
|
+
right: "auto",
|
|
4897
|
+
bottom: "auto",
|
|
4898
|
+
transform: "translate(-50%, -50%)",
|
|
4727
4899
|
width: options.strayHandleSize,
|
|
4728
4900
|
height: options.strayHandleSize,
|
|
4729
4901
|
background: record.color ?? DEFAULT_ANCHOR_COLOR,
|
|
4730
|
-
border: "none"
|
|
4731
|
-
...axis
|
|
4902
|
+
border: "none"
|
|
4732
4903
|
}
|
|
4733
4904
|
},
|
|
4734
|
-
`${side}:${
|
|
4905
|
+
`${side}:${point.xPct.toFixed(1)},${point.yPct.toFixed(1)}`
|
|
4735
4906
|
);
|
|
4736
4907
|
}) });
|
|
4737
4908
|
}
|
|
4909
|
+
function useLiveStrayPlacement(record, sides, options) {
|
|
4910
|
+
const { store } = useRivetContext();
|
|
4911
|
+
const { wrapperRef } = useRivetNodeContext();
|
|
4912
|
+
const sideKey = sides.join(",");
|
|
4913
|
+
useEffect(() => {
|
|
4914
|
+
const wrapper = wrapperRef.current;
|
|
4915
|
+
const geometry = record.geometry;
|
|
4916
|
+
if (!wrapper || !geometry) return;
|
|
4917
|
+
let displaced = false;
|
|
4918
|
+
const element = (side) => wrapper.querySelector(
|
|
4919
|
+
`[data-rivet-handle-id="${anchorHandleId(record.anchorId, side)}"]`
|
|
4920
|
+
);
|
|
4921
|
+
const place = (side, at) => {
|
|
4922
|
+
const el = element(side);
|
|
4923
|
+
if (!el) return;
|
|
4924
|
+
const point = strayPoint(geometry, at, options);
|
|
4925
|
+
el.style.left = `${point.xPct}%`;
|
|
4926
|
+
el.style.top = `${point.yPct}%`;
|
|
4927
|
+
};
|
|
4928
|
+
return store.subscribeFrame(() => {
|
|
4929
|
+
if (!store.isNodeDragging()) {
|
|
4930
|
+
if (displaced) {
|
|
4931
|
+
displaced = false;
|
|
4932
|
+
for (const side of sides) place(side, side);
|
|
4933
|
+
}
|
|
4934
|
+
return;
|
|
4935
|
+
}
|
|
4936
|
+
const autoSide = sides.length === 1 ? sides[0] ?? null : null;
|
|
4937
|
+
const shown = displayedAnchorSides(store, record, autoSide);
|
|
4938
|
+
for (const side of sides) {
|
|
4939
|
+
const at = shown.get(side);
|
|
4940
|
+
if (at && at !== side) {
|
|
4941
|
+
displaced = true;
|
|
4942
|
+
place(side, at);
|
|
4943
|
+
} else if (displaced) {
|
|
4944
|
+
place(side, side);
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
});
|
|
4948
|
+
}, [store, wrapperRef, record, sideKey, options]);
|
|
4949
|
+
}
|
|
4950
|
+
function displayedAnchorSides(store, record, autoSide) {
|
|
4951
|
+
const shown = /* @__PURE__ */ new Map();
|
|
4952
|
+
const displayed = store.getDisplayedSides();
|
|
4953
|
+
if (displayed.size === 0) return shown;
|
|
4954
|
+
for (const [edgeId, edge] of store.edges) {
|
|
4955
|
+
for (const end of ["source", "target"]) {
|
|
4956
|
+
const nodeId = end === "source" ? edge.source : edge.target;
|
|
4957
|
+
if (nodeId !== record.nodeId) continue;
|
|
4958
|
+
const handleId = end === "source" ? edge.sourceHandle : edge.targetHandle;
|
|
4959
|
+
const at = displayed.get(displayedSideKey(edgeId, end));
|
|
4960
|
+
if (!at) continue;
|
|
4961
|
+
const anchor = parseAnchorHandleId(handleId);
|
|
4962
|
+
if (anchor?.anchorId === record.anchorId) {
|
|
4963
|
+
if (!shown.has(anchor.side)) shown.set(anchor.side, at);
|
|
4964
|
+
continue;
|
|
4965
|
+
}
|
|
4966
|
+
if (autoSide && parseAnchorAutoHandleId(handleId) === record.anchorId && !shown.has(autoSide)) {
|
|
4967
|
+
shown.set(autoSide, at);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
return shown;
|
|
4972
|
+
}
|
|
4973
|
+
var ANCHOR_STEP_KEYS = {
|
|
4974
|
+
ArrowRight: 1,
|
|
4975
|
+
ArrowLeft: -1
|
|
4976
|
+
};
|
|
4738
4977
|
var wrapperStyle = {
|
|
4739
4978
|
position: "absolute",
|
|
4740
4979
|
top: 0,
|
|
@@ -4827,6 +5066,21 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4827
5066
|
if (next instanceof Element && next.closest(SELECTOR.node)) return;
|
|
4828
5067
|
if (store.getFocusedNode() === id) store.setFocusedNode(null);
|
|
4829
5068
|
};
|
|
5069
|
+
const stepAnchor = (step) => {
|
|
5070
|
+
const anchors = orderedSelectableAnchors(store.getNodeAnchors(id)).filter(
|
|
5071
|
+
(record2) => store.locks.allows(anchorTargetKey(id, record2.anchorId), "select")
|
|
5072
|
+
);
|
|
5073
|
+
if (anchors.length === 0) return;
|
|
5074
|
+
const selected = store.getSelectedAnchors();
|
|
5075
|
+
const current = anchors.findLastIndex(
|
|
5076
|
+
(record2) => selected.some((ref2) => ref2.nodeId === id && ref2.anchorId === record2.anchorId)
|
|
5077
|
+
);
|
|
5078
|
+
const next = current < 0 ? step > 0 ? 0 : anchors.length - 1 : current + step;
|
|
5079
|
+
const record = anchors[(next + anchors.length) % anchors.length];
|
|
5080
|
+
if (!record) return;
|
|
5081
|
+
store.setSelectedAnchors([{ nodeId: id, anchorId: record.anchorId }]);
|
|
5082
|
+
announce(`${record.anchorId} selected in ${nodeName()}.`);
|
|
5083
|
+
};
|
|
4830
5084
|
const onKeyDown = (event) => {
|
|
4831
5085
|
if (event.target !== event.currentTarget) return;
|
|
4832
5086
|
const node2 = store.nodes.get(id);
|
|
@@ -4858,9 +5112,21 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4858
5112
|
releaseGrab(true);
|
|
4859
5113
|
return;
|
|
4860
5114
|
}
|
|
5115
|
+
if (store.getSelectedAnchors().some((ref2) => ref2.nodeId === id)) {
|
|
5116
|
+
event.preventDefault();
|
|
5117
|
+
store.setSelectedAnchors([]);
|
|
5118
|
+
store.selectNode(id);
|
|
5119
|
+
announce(`${nodeName()} selected.`);
|
|
5120
|
+
return;
|
|
5121
|
+
}
|
|
4861
5122
|
store.selectNode(null);
|
|
4862
5123
|
return;
|
|
4863
5124
|
}
|
|
5125
|
+
if (event.altKey && !grabOrigins.current && ANCHOR_STEP_KEYS[event.key]) {
|
|
5126
|
+
event.preventDefault();
|
|
5127
|
+
stepAnchor(ANCHOR_STEP_KEYS[event.key] ?? 1);
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
4864
5130
|
const dir = ARROW_DIRS[event.key];
|
|
4865
5131
|
if (!dir || !grabOrigins.current) return;
|
|
4866
5132
|
event.preventDefault();
|
|
@@ -4892,12 +5158,13 @@ var NodeWrapper = memo(function NodeWrapper2({ id }) {
|
|
|
4892
5158
|
const target = event.target instanceof Element ? event.target : null;
|
|
4893
5159
|
const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect)) || !store.locks.allows(id, "select");
|
|
4894
5160
|
const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag)) || !store.locks.allows(id, "drag");
|
|
5161
|
+
const onAnchor = target !== null && findSelectableAnchor(store.getNodeAnchors(id), target) !== null;
|
|
4895
5162
|
const additive = keyHeld(event, multiSelectionKeys);
|
|
4896
5163
|
if (additive) {
|
|
5164
|
+
if (onAnchor) return;
|
|
4897
5165
|
if (!noSelect) store.selectNode(id, true);
|
|
4898
5166
|
return;
|
|
4899
5167
|
}
|
|
4900
|
-
const onAnchor = target !== null && findSelectableAnchor(store.getNodeAnchors(id), target) !== null;
|
|
4901
5168
|
const selectNode = () => {
|
|
4902
5169
|
if (!noSelect && !store.isNodeSelected(id)) store.selectNode(id);
|
|
4903
5170
|
};
|
|
@@ -5474,7 +5741,7 @@ var srOnlyStyle = {
|
|
|
5474
5741
|
whiteSpace: "nowrap",
|
|
5475
5742
|
border: 0
|
|
5476
5743
|
};
|
|
5477
|
-
var NODE_KEYBOARD_INSTRUCTIONS = "Press Enter to select and grab the node. While grabbed, use the arrow keys to move it, Enter to drop, Escape to cancel. Press Delete to remove the selection.";
|
|
5744
|
+
var NODE_KEYBOARD_INSTRUCTIONS = "Press Enter to select and grab the node. While grabbed, use the arrow keys to move it, Enter to drop, Escape to cancel. Hold Alt with Left or Right to step through the node's anchors, Escape to leave them. Press Delete to remove the selection.";
|
|
5478
5745
|
var DEFAULT_MULTI_SELECTION_KEY_CODE = ["Meta", "Control", "Shift"];
|
|
5479
5746
|
var EMPTY_NODE_TYPES = {};
|
|
5480
5747
|
var EMPTY_NODES = [];
|
|
@@ -5529,6 +5796,7 @@ function Rivet({
|
|
|
5529
5796
|
onConnect,
|
|
5530
5797
|
edgesReconnectable,
|
|
5531
5798
|
edgeAlignment = "manual",
|
|
5799
|
+
edgeAlignmentHysteresis = DEFAULT_ALIGNMENT_HYSTERESIS,
|
|
5532
5800
|
onReconnect,
|
|
5533
5801
|
onReconnectStart,
|
|
5534
5802
|
onReconnectEnd,
|
|
@@ -5577,6 +5845,9 @@ function Rivet({
|
|
|
5577
5845
|
useEffect(() => {
|
|
5578
5846
|
store.setEdgeAlignment(edgeAlignment);
|
|
5579
5847
|
}, [store, edgeAlignment]);
|
|
5848
|
+
useEffect(() => {
|
|
5849
|
+
store.setAlignmentHysteresis(edgeAlignmentHysteresis);
|
|
5850
|
+
}, [store, edgeAlignmentHysteresis]);
|
|
5580
5851
|
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
5581
5852
|
onSelectionChangeRef.current = onSelectionChange;
|
|
5582
5853
|
useEffect(
|
|
@@ -5888,6 +6159,7 @@ function buildInstance(store, controls, getViewportElements) {
|
|
|
5888
6159
|
unregister: () => store.unregisterAnchor(nodeId, anchorId)
|
|
5889
6160
|
};
|
|
5890
6161
|
},
|
|
6162
|
+
detachAnchor: (nodeId, anchorId) => store.detachAnchor(nodeId, anchorId),
|
|
5891
6163
|
unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
|
|
5892
6164
|
remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
|
|
5893
6165
|
getAnchor: (nodeId, anchorId) => store.getNodeAnchors(nodeId).find((record) => record.anchorId === anchorId) ?? null,
|