@kolosal-ai/rivet 0.1.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/index.js ADDED
@@ -0,0 +1,4849 @@
1
+ import { parseAnchorHandleId, ANCHOR_SIDES, anchorDotHandleId, strayPlacementPct, anchorHandleId, anchorAutoHandleId, parseAnchorAutoHandleId, normalizeAnchorConnection, computeAnchorGeometry, anchorGeometryEqual, DEFAULT_ANCHOR_OPTIONS } from './chunk-AXT35ZJV.js';
2
+ import { useRivetContext, RivetNodeContext, useRivetNodeContext, useRivetControls, RivetContext, useRivetViewport } from './chunk-VQYN27OK.js';
3
+ export { useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetViewport } from './chunk-VQYN27OK.js';
4
+ import { clampNodeToLanes, clamp, swimlaneChromeAt, SWIMLANE_LABEL_WIDTH, resolveSwimlanes, flattenLanes, resolveMargin, requiredLaneHeight, withAlpha } from './chunk-6XRQSAQT.js';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+ import { memo, useRef, useState, useCallback, useSyncExternalStore, useMemo, useEffect, Fragment as Fragment$1, useId, useLayoutEffect } from 'react';
7
+
8
+ // src/align.ts
9
+ var edgesX = (r) => [r.x, r.x + r.width / 2, r.x + r.width];
10
+ var edgesY = (r) => [r.y, r.y + r.height / 2, r.y + r.height];
11
+ function alignRect(moving, statics, threshold) {
12
+ const mX = edgesX(moving);
13
+ const mY = edgesY(moving);
14
+ let bestX = null;
15
+ let bestY = null;
16
+ for (const other of statics) {
17
+ const oX = edgesX(other);
18
+ const oY = edgesY(other);
19
+ for (const me of mX) {
20
+ for (const oe of oX) {
21
+ const delta = oe - me;
22
+ if (Math.abs(delta) <= threshold && (!bestX || Math.abs(delta) < Math.abs(bestX.delta))) {
23
+ bestX = { delta, position: oe, other };
24
+ }
25
+ }
26
+ }
27
+ for (const me of mY) {
28
+ for (const oe of oY) {
29
+ const delta = oe - me;
30
+ if (Math.abs(delta) <= threshold && (!bestY || Math.abs(delta) < Math.abs(bestY.delta))) {
31
+ bestY = { delta, position: oe, other };
32
+ }
33
+ }
34
+ }
35
+ }
36
+ const offset = { x: bestX?.delta ?? 0, y: bestY?.delta ?? 0 };
37
+ const guides = [];
38
+ if (bestX) {
39
+ const o = bestX.other;
40
+ guides.push({
41
+ axis: "x",
42
+ position: bestX.position,
43
+ start: Math.min(moving.y + offset.y, o.y),
44
+ end: Math.max(moving.y + moving.height + offset.y, o.y + o.height)
45
+ });
46
+ }
47
+ if (bestY) {
48
+ const o = bestY.other;
49
+ guides.push({
50
+ axis: "y",
51
+ position: bestY.position,
52
+ start: Math.min(moving.x + offset.x, o.x),
53
+ end: Math.max(moving.x + moving.width + offset.x, o.x + o.width)
54
+ });
55
+ }
56
+ return { offset, guides };
57
+ }
58
+
59
+ // src/changes.ts
60
+ function applyNodeChanges(changes, nodes) {
61
+ if (changes.length === 0) return nodes;
62
+ const byId = new Map(nodes.map((node) => [node.id, node]));
63
+ const order = nodes.map((node) => node.id);
64
+ for (const change of changes) {
65
+ switch (change.type) {
66
+ case "add": {
67
+ if (!byId.has(change.item.id)) order.push(change.item.id);
68
+ byId.set(change.item.id, change.item);
69
+ break;
70
+ }
71
+ case "replace": {
72
+ byId.set(change.id, change.item);
73
+ break;
74
+ }
75
+ case "remove": {
76
+ byId.delete(change.id);
77
+ break;
78
+ }
79
+ case "position": {
80
+ const node = byId.get(change.id);
81
+ if (node) {
82
+ byId.set(change.id, {
83
+ ...node,
84
+ position: change.position ?? node.position,
85
+ dragging: change.dragging
86
+ });
87
+ }
88
+ break;
89
+ }
90
+ case "dimensions": {
91
+ const node = byId.get(change.id);
92
+ if (node) {
93
+ byId.set(change.id, {
94
+ ...node,
95
+ size: change.dimensions,
96
+ // A resize (not a measurement) sets explicit width/height.
97
+ ...change.resizing !== void 0 ? { width: change.dimensions.width, height: change.dimensions.height } : {}
98
+ });
99
+ }
100
+ break;
101
+ }
102
+ case "select": {
103
+ const node = byId.get(change.id);
104
+ if (node) byId.set(change.id, { ...node, selected: change.selected });
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ const result = [];
110
+ for (const id of order) {
111
+ const node = byId.get(id);
112
+ if (node) result.push(node);
113
+ }
114
+ return result;
115
+ }
116
+ function applyEdgeChanges(changes, edges) {
117
+ if (changes.length === 0) return edges;
118
+ const byId = new Map(edges.map((edge) => [edge.id, edge]));
119
+ const order = edges.map((edge) => edge.id);
120
+ for (const change of changes) {
121
+ switch (change.type) {
122
+ case "add": {
123
+ if (!byId.has(change.item.id)) order.push(change.item.id);
124
+ byId.set(change.item.id, change.item);
125
+ break;
126
+ }
127
+ case "replace": {
128
+ byId.set(change.id, change.item);
129
+ break;
130
+ }
131
+ case "remove": {
132
+ byId.delete(change.id);
133
+ break;
134
+ }
135
+ case "select": {
136
+ const edge = byId.get(change.id);
137
+ if (edge) byId.set(change.id, { ...edge, selected: change.selected });
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ const result = [];
143
+ for (const id of order) {
144
+ const edge = byId.get(id);
145
+ if (edge) result.push(edge);
146
+ }
147
+ return result;
148
+ }
149
+
150
+ // src/clipboard.ts
151
+ var ORIGIN = { x: 0, y: 0 };
152
+ var defaultId = () => crypto.randomUUID();
153
+ function cloneElements(nodes, edges, options = {}) {
154
+ const offset = options.offset ?? ORIGIN;
155
+ const makeId = options.makeId ?? defaultId;
156
+ const idMap = /* @__PURE__ */ new Map();
157
+ for (const node of nodes) idMap.set(node.id, makeId());
158
+ const remap = (id) => idMap.get(id) ?? id;
159
+ const clonedNodes = nodes.map((node) => {
160
+ const parentCopied = node.parentId !== void 0 && idMap.has(node.parentId);
161
+ const next = {
162
+ ...node,
163
+ id: remap(node.id),
164
+ position: parentCopied ? { ...node.position } : { x: node.position.x + offset.x, y: node.position.y + offset.y }
165
+ };
166
+ next.hovered = void 0;
167
+ next.dragging = void 0;
168
+ next.selected = void 0;
169
+ if (node.parentId) next.parentId = idMap.has(node.parentId) ? remap(node.parentId) : void 0;
170
+ return next;
171
+ });
172
+ const clonedEdges = edges.filter((edge) => idMap.has(edge.source) && idMap.has(edge.target)).map((edge) => ({
173
+ ...edge,
174
+ id: makeId(),
175
+ source: remap(edge.source),
176
+ target: remap(edge.target),
177
+ selected: void 0
178
+ }));
179
+ return { nodes: clonedNodes, edges: clonedEdges };
180
+ }
181
+ var panelStyle = {
182
+ position: "absolute",
183
+ bottom: 16,
184
+ left: 16,
185
+ display: "flex",
186
+ flexDirection: "column",
187
+ borderRadius: 8,
188
+ overflow: "hidden",
189
+ border: "1px solid rgba(100, 116, 139, 0.4)",
190
+ background: "#ffffff",
191
+ boxShadow: "0 1px 3px rgba(15, 23, 42, 0.12)"
192
+ };
193
+ var buttonStyle = {
194
+ display: "flex",
195
+ alignItems: "center",
196
+ justifyContent: "center",
197
+ width: 32,
198
+ height: 32,
199
+ padding: 0,
200
+ border: "none",
201
+ borderBottom: "1px solid rgba(100, 116, 139, 0.25)",
202
+ background: "transparent",
203
+ color: "#0f172a",
204
+ cursor: "pointer"
205
+ };
206
+ function Controls({
207
+ showZoom = true,
208
+ showFitView = true,
209
+ children,
210
+ className,
211
+ style
212
+ }) {
213
+ const controls = useRivetControls();
214
+ return /* @__PURE__ */ jsxs("div", { "data-nopan": true, className, style: { ...panelStyle, ...style }, children: [
215
+ showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [
216
+ /* @__PURE__ */ jsx(
217
+ "button",
218
+ {
219
+ type: "button",
220
+ style: buttonStyle,
221
+ onClick: () => controls.zoomIn(),
222
+ "aria-label": "Zoom in",
223
+ children: /* @__PURE__ */ jsx(Icon, { title: "Zoom in", path: "M12 5v14M5 12h14" })
224
+ }
225
+ ),
226
+ /* @__PURE__ */ jsx(
227
+ "button",
228
+ {
229
+ type: "button",
230
+ style: buttonStyle,
231
+ onClick: () => controls.zoomOut(),
232
+ "aria-label": "Zoom out",
233
+ children: /* @__PURE__ */ jsx(Icon, { title: "Zoom out", path: "M5 12h14" })
234
+ }
235
+ )
236
+ ] }),
237
+ showFitView && /* @__PURE__ */ jsx(
238
+ "button",
239
+ {
240
+ type: "button",
241
+ style: buttonStyle,
242
+ onClick: () => controls.fitView(),
243
+ "aria-label": "Fit view",
244
+ children: /* @__PURE__ */ jsx(Icon, { title: "Fit view", path: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" })
245
+ }
246
+ ),
247
+ children
248
+ ] });
249
+ }
250
+ function Icon({ title, path }) {
251
+ return /* @__PURE__ */ jsxs(
252
+ "svg",
253
+ {
254
+ width: 16,
255
+ height: 16,
256
+ viewBox: "0 0 24 24",
257
+ fill: "none",
258
+ stroke: "currentColor",
259
+ strokeWidth: 2,
260
+ strokeLinecap: "round",
261
+ strokeLinejoin: "round",
262
+ role: "img",
263
+ children: [
264
+ /* @__PURE__ */ jsx("title", { children: title }),
265
+ /* @__PURE__ */ jsx("path", { d: path })
266
+ ]
267
+ }
268
+ );
269
+ }
270
+
271
+ // ../utils/src/id.ts
272
+ function createId(prefix = "") {
273
+ const cryptoObj = globalThis.crypto;
274
+ const base = cryptoObj?.randomUUID ? cryptoObj.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
275
+ return prefix ? `${prefix}-${base}` : base;
276
+ }
277
+
278
+ // src/constants.ts
279
+ var DEFAULT_NODE_SIZE = { width: 160, height: 44 };
280
+ function handleKey(nodeId, handleId) {
281
+ return `${nodeId} ${handleId}`;
282
+ }
283
+ var SELECTOR = {
284
+ node: "[data-rivet-node]",
285
+ handle: "[data-rivet-handle]",
286
+ noPan: "[data-nopan]",
287
+ /** Press here starts no node drag (but the node can still be selected/clicked). */
288
+ noDrag: "[data-nodrag]",
289
+ /** Press here never changes the selection. */
290
+ noSelect: "[data-noselect]",
291
+ /** The draggable box a marquee leaves over its selection (owns its own drag). */
292
+ nodesSelection: "[data-rivet-nodes-selection]",
293
+ /** Elements that own their own pointer — a pane pointerdown here starts no gesture. */
294
+ paneGestureExempt: "[data-rivet-node], [data-rivet-swimlane-handle], [data-nopan], [data-rivet-nodes-selection]"
295
+ };
296
+
297
+ // src/edge-alignment.ts
298
+ function facingSide(node, peer) {
299
+ const dx = peer.x + peer.width / 2 - (node.x + node.width / 2);
300
+ const dy = peer.y + peer.height / 2 - (node.y + node.height / 2);
301
+ if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? "right" : "left";
302
+ return dy > 0 ? "bottom" : "top";
303
+ }
304
+ var DEFAULT_ALIGNMENT_HYSTERESIS = 1.2;
305
+ function resolveFacingSide(node, peer, previous, hysteresis = DEFAULT_ALIGNMENT_HYSTERESIS) {
306
+ if (!previous) return facingSide(node, peer);
307
+ const dx = peer.x + peer.width / 2 - (node.x + node.width / 2);
308
+ const dy = peer.y + peer.height / 2 - (node.y + node.height / 2);
309
+ if (previous === "left" || previous === "right") {
310
+ if (Math.abs(dy) > Math.abs(dx) * hysteresis) return dy > 0 ? "bottom" : "top";
311
+ return dx > 0 ? "right" : "left";
312
+ }
313
+ if (Math.abs(dx) > Math.abs(dy) * hysteresis) return dx > 0 ? "right" : "left";
314
+ return dy > 0 ? "bottom" : "top";
315
+ }
316
+
317
+ // src/edge-ends.ts
318
+ function displayedSideKey(edgeId, end) {
319
+ return `${edgeId}:${end}`;
320
+ }
321
+ var SIDES = ["left", "right", "top", "bottom"];
322
+ function parseSideHandleId(handleId) {
323
+ return handleId && SIDES.includes(handleId) ? handleId : null;
324
+ }
325
+ function edgeEndpointHandle(end) {
326
+ if (end.anchorId !== void 0) {
327
+ return end.side ? anchorHandleId(end.anchorId, end.side) : anchorAutoHandleId(end.anchorId);
328
+ }
329
+ return end.side;
330
+ }
331
+ function parseEdgeEndpoint(nodeId, handleId, handles) {
332
+ if (!handleId) return { nodeId };
333
+ const auto = parseAnchorAutoHandleId(handleId);
334
+ if (auto) return { nodeId, anchorId: auto };
335
+ const stray = parseAnchorHandleId(handleId);
336
+ if (stray) return { nodeId, anchorId: stray.anchorId, side: stray.side };
337
+ const registered = handles?.get(handleKey(nodeId, handleId));
338
+ if (registered) return { nodeId, side: registered.position };
339
+ const side = parseSideHandleId(handleId);
340
+ return side ? { nodeId, side } : { nodeId };
341
+ }
342
+
343
+ // src/graph.ts
344
+ function clampChildToParent(position, childSize, parentSize) {
345
+ const maxX = Math.max(0, parentSize.width - childSize.width);
346
+ const maxY = Math.max(0, parentSize.height - childSize.height);
347
+ return {
348
+ x: Math.min(maxX, Math.max(0, position.x)),
349
+ y: Math.min(maxY, Math.max(0, position.y))
350
+ };
351
+ }
352
+ function snapToGrid(position, grid) {
353
+ const [gx, gy] = grid;
354
+ return {
355
+ x: gx > 0 ? Math.round(position.x / gx) * gx : position.x,
356
+ y: gy > 0 ? Math.round(position.y / gy) * gy : position.y
357
+ };
358
+ }
359
+ function boundingRect(rects) {
360
+ if (rects.length === 0) return null;
361
+ let minX = Number.POSITIVE_INFINITY;
362
+ let minY = Number.POSITIVE_INFINITY;
363
+ let maxX = Number.NEGATIVE_INFINITY;
364
+ let maxY = Number.NEGATIVE_INFINITY;
365
+ for (const r of rects) {
366
+ minX = Math.min(minX, r.x);
367
+ minY = Math.min(minY, r.y);
368
+ maxX = Math.max(maxX, r.x + r.width);
369
+ maxY = Math.max(maxY, r.y + r.height);
370
+ }
371
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
372
+ }
373
+ var MAX_PARENT_DEPTH = 50;
374
+ function worldPosition(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
375
+ const node = nodes.get(id);
376
+ if (!node) return { x: 0, y: 0 };
377
+ let x = node.position.x;
378
+ let y = node.position.y;
379
+ let parentId = node.parentId;
380
+ let guard = 0;
381
+ while (parentId && guard++ < maxDepth) {
382
+ const parent = nodes.get(parentId);
383
+ if (!parent) break;
384
+ x += parent.position.x;
385
+ y += parent.position.y;
386
+ parentId = parent.parentId;
387
+ }
388
+ return { x, y };
389
+ }
390
+ function nodeDepth(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
391
+ let depth = 0;
392
+ let parentId = nodes.get(id)?.parentId;
393
+ while (parentId && depth < maxDepth) {
394
+ depth++;
395
+ parentId = nodes.get(parentId)?.parentId;
396
+ }
397
+ return depth;
398
+ }
399
+ function hasAncestorIn(nodes, id, set, maxDepth = MAX_PARENT_DEPTH) {
400
+ let parentId = nodes.get(id)?.parentId;
401
+ let guard = 0;
402
+ while (parentId && guard++ < maxDepth) {
403
+ if (set.has(parentId)) return true;
404
+ parentId = nodes.get(parentId)?.parentId;
405
+ }
406
+ return false;
407
+ }
408
+ function buildChildIndex(nodes) {
409
+ const childrenOf = /* @__PURE__ */ new Map();
410
+ for (const node of nodes.values()) {
411
+ if (!node.parentId) continue;
412
+ const list = childrenOf.get(node.parentId);
413
+ if (list) list.push(node.id);
414
+ else childrenOf.set(node.parentId, [node.id]);
415
+ }
416
+ return childrenOf;
417
+ }
418
+ function collectDescendants(id, childIndex, out) {
419
+ for (const child of childIndex.get(id) ?? []) {
420
+ out.push(child);
421
+ collectDescendants(child, childIndex, out);
422
+ }
423
+ }
424
+ function descendantIds(nodes, id) {
425
+ const out = [];
426
+ collectDescendants(id, buildChildIndex(nodes), out);
427
+ return out;
428
+ }
429
+ function serializeGraph(nodes, edges, viewport) {
430
+ const cleanNodes = [];
431
+ for (const node of nodes) {
432
+ const copy = { ...node, position: { ...node.position } };
433
+ if (node.size) copy.size = { ...node.size };
434
+ copy.hovered = void 0;
435
+ copy.dragging = void 0;
436
+ cleanNodes.push(copy);
437
+ }
438
+ return {
439
+ nodes: cleanNodes,
440
+ edges: [...edges].map((edge) => ({ ...edge })),
441
+ viewport: { ...viewport }
442
+ };
443
+ }
444
+ function nodeControlledEqual(a, b) {
445
+ 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;
446
+ }
447
+
448
+ // src/history.ts
449
+ function hasRecordableChange(changes) {
450
+ return changes.some((change) => {
451
+ switch (change.type) {
452
+ case "add":
453
+ case "remove":
454
+ case "replace":
455
+ return true;
456
+ case "position":
457
+ return !change.dragging;
458
+ case "dimensions":
459
+ return !change.resizing;
460
+ default:
461
+ return false;
462
+ }
463
+ });
464
+ }
465
+ var HistoryManager = class {
466
+ constructor(snapshot, limit = 100, schedule = (cb) => queueMicrotask(cb)) {
467
+ this.snapshot = snapshot;
468
+ this.limit = limit;
469
+ this.schedule = schedule;
470
+ this.last = snapshot();
471
+ }
472
+ snapshot;
473
+ limit;
474
+ schedule;
475
+ undoStack = [];
476
+ redoStack = [];
477
+ last;
478
+ scheduled = false;
479
+ rebaseScheduled = false;
480
+ listeners = /* @__PURE__ */ new Set();
481
+ /** Note that a recordable batch was applied; flushes one step next microtask. */
482
+ record(recordable) {
483
+ if (!recordable || this.scheduled) return;
484
+ this.scheduled = true;
485
+ this.schedule(() => {
486
+ if (this.scheduled) this.flush();
487
+ });
488
+ }
489
+ /**
490
+ * The graph changed *without* recording a step — a controlled reconcile
491
+ * applying incoming props, or a measurement echo. Refresh the rolling
492
+ * baseline so the next recorded step diffs against reality; a stale baseline
493
+ * makes undo revert the unrecorded changes too (e.g. a prop-added node that
494
+ * was then deleted through the store would undo to a graph without it).
495
+ * Coalesced per microtask like {@link record}. When a step is pending, its
496
+ * flush refreshes the baseline anyway, so this defers to it — with the
497
+ * documented limit that unrecorded and recorded mutations landing in the
498
+ * same microtask fold into that one step.
499
+ */
500
+ rebase() {
501
+ if (this.scheduled || this.rebaseScheduled) return;
502
+ this.rebaseScheduled = true;
503
+ this.schedule(() => {
504
+ this.rebaseScheduled = false;
505
+ if (this.scheduled) return;
506
+ this.last = this.snapshot();
507
+ });
508
+ }
509
+ flush() {
510
+ this.scheduled = false;
511
+ this.undoStack.push(this.last);
512
+ if (this.undoStack.length > this.limit) this.undoStack.shift();
513
+ this.redoStack = [];
514
+ this.last = this.snapshot();
515
+ this.notify();
516
+ }
517
+ undo(apply) {
518
+ if (this.scheduled) this.flush();
519
+ const prev = this.undoStack.pop();
520
+ if (!prev) return false;
521
+ this.redoStack.push(this.snapshot());
522
+ apply(prev);
523
+ this.last = prev;
524
+ this.notify();
525
+ return true;
526
+ }
527
+ redo(apply) {
528
+ const next = this.redoStack.pop();
529
+ if (!next) return false;
530
+ this.undoStack.push(this.last);
531
+ apply(next);
532
+ this.last = next;
533
+ this.notify();
534
+ return true;
535
+ }
536
+ canUndo() {
537
+ return this.undoStack.length > 0 || this.scheduled;
538
+ }
539
+ canRedo() {
540
+ return this.redoStack.length > 0;
541
+ }
542
+ /** Drop all history and reset the baseline to the current graph. */
543
+ clear() {
544
+ this.scheduled = false;
545
+ this.rebaseScheduled = false;
546
+ this.undoStack = [];
547
+ this.redoStack = [];
548
+ this.last = this.snapshot();
549
+ this.notify();
550
+ }
551
+ subscribe(listener) {
552
+ this.listeners.add(listener);
553
+ return () => {
554
+ this.listeners.delete(listener);
555
+ };
556
+ }
557
+ notify() {
558
+ for (const listener of this.listeners) listener();
559
+ }
560
+ };
561
+
562
+ // src/store.ts
563
+ var RivetGraphStore = class {
564
+ nodes = /* @__PURE__ */ new Map();
565
+ edges = /* @__PURE__ */ new Map();
566
+ handles = /* @__PURE__ */ new Map();
567
+ anchors = /* @__PURE__ */ new Map();
568
+ viewport;
569
+ viewportClamp = null;
570
+ pending = null;
571
+ nodeVersions = /* @__PURE__ */ new Map();
572
+ nodeListeners = /* @__PURE__ */ new Map();
573
+ anchorVersions = /* @__PURE__ */ new Map();
574
+ anchorListeners = /* @__PURE__ */ new Map();
575
+ edgesVersion = 0;
576
+ edgesListeners = /* @__PURE__ */ new Set();
577
+ nodeElements = /* @__PURE__ */ new Map();
578
+ viewportListeners = /* @__PURE__ */ new Set();
579
+ frameListeners = /* @__PURE__ */ new Set();
580
+ selectionListeners = /* @__PURE__ */ new Set();
581
+ selectedNodeIds = /* @__PURE__ */ new Set();
582
+ selectionBoxActive = false;
583
+ selectionBoxVersion = 0;
584
+ selectionBoxListeners = /* @__PURE__ */ new Set();
585
+ hoveredNodeId = null;
586
+ focusedNodeId = null;
587
+ focusListeners = /* @__PURE__ */ new Set();
588
+ selectedEdgeId = null;
589
+ nodeDragging = false;
590
+ nodeResizing = false;
591
+ edgeAlignment = "manual";
592
+ draggingNodeIds = /* @__PURE__ */ new Set();
593
+ displayedSides = /* @__PURE__ */ new Map();
594
+ edgeLabelAnchors = /* @__PURE__ */ new Map();
595
+ alignmentGuides = [];
596
+ nodeChangeHandler = null;
597
+ edgeChangeHandler = null;
598
+ reconciling = false;
599
+ history = new HistoryManager(() => this.historySnapshot());
600
+ lastSelectionKey;
601
+ worldCache = null;
602
+ renderRequester = () => {
603
+ };
604
+ constructor(init) {
605
+ this.viewport = init.viewport;
606
+ for (const node of init.nodes) this.writeNode(node.id, node);
607
+ for (const edge of init.edges) this.edges.set(edge.id, edge);
608
+ for (const node of init.nodes) if (node.selected) this.selectedNodeIds.add(node.id);
609
+ this.selectedEdgeId = init.edges.find((edge) => edge.selected)?.id ?? null;
610
+ this.lastSelectionKey = this.selectionKey();
611
+ this.history.clear();
612
+ }
613
+ // --- internal bookkeeping ------------------------------------------------
614
+ invalidateWorld() {
615
+ this.worldCache = null;
616
+ }
617
+ /**
618
+ * The single node-map write path. Invalidates the world-position cache exactly
619
+ * when a layout-affecting field changed (the node's position or its parent
620
+ * chain) or the node is new — so size/lane/selection/hover writes skip the
621
+ * O(n) rebuild, and no mutator can forget to invalidate.
622
+ */
623
+ writeNode(id, next) {
624
+ const prev = this.nodes.get(id);
625
+ this.nodes.set(id, next);
626
+ if (!prev || prev.parentId !== next.parentId || prev.position.x !== next.position.x || prev.position.y !== next.position.y) {
627
+ this.invalidateWorld();
628
+ }
629
+ }
630
+ /** The single node-map delete path. Always invalidates — a removal shifts descendants. */
631
+ deleteNode(id) {
632
+ this.nodes.delete(id);
633
+ this.invalidateWorld();
634
+ }
635
+ selectionKey() {
636
+ return `${[...this.selectedNodeIds].sort().join(",")}|${this.selectedEdgeId ?? ""}`;
637
+ }
638
+ buildSelection() {
639
+ return {
640
+ nodes: [...this.selectedNodeIds].map((id) => this.nodes.get(id)).filter((n) => n !== void 0),
641
+ edges: this.selectedEdgeId ? [this.edges.get(this.selectedEdgeId)].filter((e) => e !== void 0) : []
642
+ };
643
+ }
644
+ notifySelection() {
645
+ const key = this.selectionKey();
646
+ if (key === this.lastSelectionKey) return;
647
+ this.lastSelectionKey = key;
648
+ if (this.selectionBoxActive) this.bumpSelectionBox();
649
+ if (this.selectionListeners.size === 0) return;
650
+ const selection = this.buildSelection();
651
+ for (const listener of this.selectionListeners) listener(selection);
652
+ }
653
+ /**
654
+ * The one emission policy. Unless a `reconcile` is applying incoming props
655
+ * (which must never echo them back out), record the batch to history — when
656
+ * `record` is set — then hand it to the controlled consumer. Recording happens
657
+ * before the handler so history works in uncontrolled mode too.
658
+ */
659
+ emit(changes, handler, record) {
660
+ if (this.reconciling || changes.length === 0) return;
661
+ if (record) this.history.record(hasRecordableChange(changes));
662
+ else if (hasRecordableChange(changes)) this.history.rebase();
663
+ handler?.(changes);
664
+ }
665
+ emitNodeChanges(changes, record = true) {
666
+ this.emit(changes, this.nodeChangeHandler, record);
667
+ }
668
+ emitEdgeChanges(changes, record = true) {
669
+ this.emit(changes, this.edgeChangeHandler, record);
670
+ }
671
+ bumpNode(id) {
672
+ this.nodeVersions.set(id, (this.nodeVersions.get(id) ?? 0) + 1);
673
+ const listeners = this.nodeListeners.get(id);
674
+ if (listeners) for (const listener of listeners) listener();
675
+ if (this.selectionBoxActive && this.selectedNodeIds.has(id)) this.bumpSelectionBox();
676
+ }
677
+ bumpSelectionBox() {
678
+ this.selectionBoxVersion += 1;
679
+ for (const listener of this.selectionBoxListeners) listener();
680
+ }
681
+ bumpAnchors(id) {
682
+ this.anchorVersions.set(id, (this.anchorVersions.get(id) ?? 0) + 1);
683
+ const listeners = this.anchorListeners.get(id);
684
+ if (listeners) for (const listener of listeners) listener();
685
+ }
686
+ bumpEdges() {
687
+ this.edgesVersion += 1;
688
+ for (const listener of this.edgesListeners) listener();
689
+ }
690
+ // Node selection is React state (nodes are DOM), so bump only what flips.
691
+ // `target` is the exact set that should end up selected.
692
+ applyNodeSelection(target) {
693
+ const changes = [];
694
+ for (const [nodeId, node] of this.nodes) {
695
+ const selected = target.has(nodeId);
696
+ if (Boolean(node.selected) !== selected) {
697
+ this.writeNode(nodeId, { ...node, selected });
698
+ this.bumpNode(nodeId);
699
+ changes.push({ type: "select", id: nodeId, selected });
700
+ }
701
+ }
702
+ this.selectedNodeIds.clear();
703
+ for (const id of target) if (this.nodes.has(id)) this.selectedNodeIds.add(id);
704
+ this.emitNodeChanges(changes);
705
+ }
706
+ setNodeHover(id) {
707
+ if (this.hoveredNodeId === id) return;
708
+ const prev = this.hoveredNodeId;
709
+ this.hoveredNodeId = id;
710
+ if (prev !== null) {
711
+ const node = this.nodes.get(prev);
712
+ if (node) {
713
+ this.writeNode(prev, { ...node, hovered: false });
714
+ this.bumpNode(prev);
715
+ }
716
+ }
717
+ if (id !== null) {
718
+ const node = this.nodes.get(id);
719
+ if (node) {
720
+ this.writeNode(id, { ...node, hovered: true });
721
+ this.bumpNode(id);
722
+ }
723
+ }
724
+ }
725
+ // Edge selection is canvas-only, so it just invalidates the frame.
726
+ setEdgeSelection(id) {
727
+ if (this.selectedEdgeId === id) return;
728
+ const changes = [];
729
+ if (this.selectedEdgeId) {
730
+ const prev = this.edges.get(this.selectedEdgeId);
731
+ if (prev) {
732
+ this.edges.set(this.selectedEdgeId, { ...prev, selected: false });
733
+ changes.push({ type: "select", id: this.selectedEdgeId, selected: false });
734
+ }
735
+ }
736
+ if (id) {
737
+ const next = this.edges.get(id);
738
+ if (next) {
739
+ this.edges.set(id, { ...next, selected: true });
740
+ changes.push({ type: "select", id, selected: true });
741
+ }
742
+ }
743
+ this.selectedEdgeId = id;
744
+ this.emitEdgeChanges(changes);
745
+ this.requestRender();
746
+ }
747
+ removeEdgeInternal(id) {
748
+ if (!this.edges.delete(id)) return false;
749
+ if (this.selectedEdgeId === id) this.selectedEdgeId = null;
750
+ this.displayedSides.delete(displayedSideKey(id, "source"));
751
+ this.displayedSides.delete(displayedSideKey(id, "target"));
752
+ this.bumpEdges();
753
+ return true;
754
+ }
755
+ // Drop a node's bookkeeping (map entry, version, selection, hover, its
756
+ // handles and anchors). Edges are handled by the caller — reconcile owns
757
+ // edges separately from remove.
758
+ dropNodeState(id) {
759
+ this.deleteNode(id);
760
+ this.nodeVersions.delete(id);
761
+ this.anchorVersions.delete(id);
762
+ this.selectedNodeIds.delete(id);
763
+ if (this.hoveredNodeId === id) this.hoveredNodeId = null;
764
+ if (this.focusedNodeId === id) this.setFocusedNode(null);
765
+ const prefix = `${id} `;
766
+ for (const key of this.handles.keys()) {
767
+ if (key.startsWith(prefix)) this.handles.delete(key);
768
+ }
769
+ for (const key of this.anchors.keys()) {
770
+ if (key.startsWith(prefix)) this.anchors.delete(key);
771
+ }
772
+ }
773
+ removeNodeInternal(id, removedEdges) {
774
+ if (!this.nodes.has(id)) return false;
775
+ this.dropNodeState(id);
776
+ for (const [edgeId, edge] of this.edges) {
777
+ if (edge.source === id || edge.target === id) {
778
+ if (this.removeEdgeInternal(edgeId)) removedEdges.push(edgeId);
779
+ }
780
+ }
781
+ return true;
782
+ }
783
+ edgeRemoveChanges(ids) {
784
+ return ids.map((id) => ({ type: "remove", id }));
785
+ }
786
+ clampViewport(next) {
787
+ const bound = this.viewportClamp;
788
+ if (!bound) return next;
789
+ const maxViewportX = -bound.minX * next.zoom;
790
+ const maxViewportY = -bound.minY * next.zoom;
791
+ const minViewportY = bound.maxY === void 0 ? Number.NEGATIVE_INFINITY : -bound.maxY * next.zoom;
792
+ return {
793
+ ...next,
794
+ x: Math.min(next.x, maxViewportX),
795
+ y: clamp(next.y, minViewportY, maxViewportY)
796
+ };
797
+ }
798
+ reconcileNodes(nextNodes) {
799
+ const incoming = /* @__PURE__ */ new Set();
800
+ const moved = [];
801
+ for (const node of nextNodes) {
802
+ incoming.add(node.id);
803
+ const existing = this.nodes.get(node.id);
804
+ if (!existing) {
805
+ this.writeNode(node.id, node);
806
+ continue;
807
+ }
808
+ if (nodeControlledEqual(existing, node)) continue;
809
+ this.writeNode(node.id, existing.hovered ? { ...node, hovered: true } : node);
810
+ this.bumpNode(node.id);
811
+ if (existing.position.x !== node.position.x || existing.position.y !== node.position.y) {
812
+ moved.push(node.id);
813
+ }
814
+ }
815
+ for (const id of [...this.nodes.keys()]) {
816
+ if (!incoming.has(id)) this.dropNodeState(id);
817
+ }
818
+ if (moved.length > 0) {
819
+ const childIndex = buildChildIndex(this.nodes);
820
+ const descendants = [];
821
+ for (const id of moved) collectDescendants(id, childIndex, descendants);
822
+ for (const id of descendants) this.bumpNode(id);
823
+ }
824
+ this.selectedNodeIds.clear();
825
+ for (const node of nextNodes) if (node.selected) this.selectedNodeIds.add(node.id);
826
+ }
827
+ reconcileEdges(nextEdges) {
828
+ const incoming = /* @__PURE__ */ new Set();
829
+ let connectivityChanged = false;
830
+ for (const edge of nextEdges) {
831
+ incoming.add(edge.id);
832
+ const prev = this.edges.get(edge.id);
833
+ if (!prev || prev.source !== edge.source || prev.target !== edge.target || prev.sourceHandle !== edge.sourceHandle || prev.targetHandle !== edge.targetHandle) {
834
+ connectivityChanged = true;
835
+ }
836
+ this.edges.set(edge.id, edge);
837
+ }
838
+ for (const id of [...this.edges.keys()]) {
839
+ if (!incoming.has(id)) {
840
+ this.edges.delete(id);
841
+ connectivityChanged = true;
842
+ }
843
+ }
844
+ this.selectedEdgeId = nextEdges.find((edge) => edge.selected)?.id ?? null;
845
+ if (connectivityChanged) this.bumpEdges();
846
+ }
847
+ // --- per-node React channel ----------------------------------------------
848
+ getNodeVersion = (id) => this.nodeVersions.get(id) ?? 0;
849
+ subscribeNode = (id, listener) => {
850
+ let set = this.nodeListeners.get(id);
851
+ if (!set) {
852
+ set = /* @__PURE__ */ new Set();
853
+ this.nodeListeners.set(id, set);
854
+ }
855
+ set.add(listener);
856
+ return () => {
857
+ set.delete(listener);
858
+ if (set.size === 0) this.nodeListeners.delete(id);
859
+ };
860
+ };
861
+ registerNodeElement = (id, el) => {
862
+ this.nodeElements.set(id, el);
863
+ };
864
+ unregisterNodeElement = (id, el) => {
865
+ if (this.nodeElements.get(id) === el) this.nodeElements.delete(id);
866
+ };
867
+ getNodeElement = (id) => this.nodeElements.get(id);
868
+ // --- viewport ------------------------------------------------------------
869
+ getViewport = () => this.viewport;
870
+ setViewport = (next) => {
871
+ this.viewport = this.clampViewport(next);
872
+ for (const listener of this.viewportListeners) listener(this.viewport);
873
+ };
874
+ setViewportClamp = (clampBound) => {
875
+ this.viewportClamp = clampBound;
876
+ const clamped = this.clampViewport(this.viewport);
877
+ if (clamped.x !== this.viewport.x || clamped.y !== this.viewport.y) {
878
+ this.viewport = clamped;
879
+ for (const listener of this.viewportListeners) listener(this.viewport);
880
+ this.requestRender();
881
+ }
882
+ };
883
+ subscribeViewport = (listener) => {
884
+ this.viewportListeners.add(listener);
885
+ return () => {
886
+ this.viewportListeners.delete(listener);
887
+ };
888
+ };
889
+ subscribeFrame = (listener) => {
890
+ this.frameListeners.add(listener);
891
+ return () => {
892
+ this.frameListeners.delete(listener);
893
+ };
894
+ };
895
+ notifyFrame = () => {
896
+ for (const listener of this.frameListeners) listener();
897
+ };
898
+ // --- node geometry -------------------------------------------------------
899
+ getWorldPositions = () => {
900
+ if (!this.worldCache) {
901
+ this.worldCache = /* @__PURE__ */ new Map();
902
+ for (const id of this.nodes.keys()) this.worldCache.set(id, worldPosition(this.nodes, id));
903
+ }
904
+ return this.worldCache;
905
+ };
906
+ getNodeWorldPosition = (id) => this.getWorldPositions().get(id) ?? { x: 0, y: 0 };
907
+ getNodeRect = (id) => {
908
+ const origin = this.getNodeWorldPosition(id);
909
+ const size = this.nodes.get(id)?.size ?? DEFAULT_NODE_SIZE;
910
+ return { x: origin.x, y: origin.y, width: size.width, height: size.height };
911
+ };
912
+ getNodeDepth = (id) => nodeDepth(this.nodes, id);
913
+ getDescendantIds = (id) => descendantIds(this.nodes, id);
914
+ getMovers = (ids) => {
915
+ const set = new Set(ids);
916
+ return ids.filter((id) => !hasAncestorIn(this.nodes, id, set));
917
+ };
918
+ // --- node mutations ------------------------------------------------------
919
+ moveNode = (id, position, commit) => {
920
+ const node = this.nodes.get(id);
921
+ if (!node) return;
922
+ this.writeNode(id, { ...node, position });
923
+ if (commit) this.draggingNodeIds.delete(id);
924
+ else this.draggingNodeIds.add(id);
925
+ this.requestRender();
926
+ if (commit) {
927
+ this.bumpNode(id);
928
+ for (const descendant of descendantIds(this.nodes, id)) this.bumpNode(descendant);
929
+ }
930
+ this.emitNodeChanges([{ type: "position", id, position, dragging: !commit }]);
931
+ if (commit) this.realignNodeEdges(id);
932
+ };
933
+ setNodeSize = (id, size) => {
934
+ const node = this.nodes.get(id);
935
+ if (!node) return;
936
+ if (node.size && Math.abs(node.size.width - size.width) < 1 && Math.abs(node.size.height - size.height) < 1) {
937
+ return;
938
+ }
939
+ this.writeNode(id, { ...node, size });
940
+ this.requestRender();
941
+ this.bumpNode(id);
942
+ this.emitNodeChanges([{ type: "dimensions", id, dimensions: size }], false);
943
+ };
944
+ resizeNode = (id, size, position, commit = true) => {
945
+ const node = this.nodes.get(id);
946
+ if (!node) return;
947
+ this.nodeResizing = !commit;
948
+ const next = { ...node, width: size.width, height: size.height, size };
949
+ if (position) next.position = position;
950
+ this.writeNode(id, next);
951
+ this.requestRender();
952
+ this.bumpNode(id);
953
+ const changes = [{ type: "dimensions", id, dimensions: size, resizing: !commit }];
954
+ if (position) changes.push({ type: "position", id, position, dragging: !commit });
955
+ this.emitNodeChanges(changes);
956
+ if (commit && position) this.realignNodeEdges(id);
957
+ };
958
+ setNodeLane = (id, laneId) => {
959
+ const node = this.nodes.get(id);
960
+ if (!node) return false;
961
+ const next = laneId ?? void 0;
962
+ if (node.laneId === next) return false;
963
+ const previous = node.laneId ?? null;
964
+ const updated = { ...node, laneId: next };
965
+ this.writeNode(id, updated);
966
+ this.bumpNode(id);
967
+ this.emitNodeChanges([{ type: "replace", id, item: updated }]);
968
+ return previous;
969
+ };
970
+ addNode = (node) => {
971
+ if (this.nodes.has(node.id)) return;
972
+ this.writeNode(node.id, node);
973
+ if (node.selected) this.selectedNodeIds.add(node.id);
974
+ this.requestRender();
975
+ this.emitNodeChanges([{ type: "add", item: node }]);
976
+ };
977
+ replaceNode = (id, node) => {
978
+ if (!this.nodes.has(id)) return;
979
+ this.writeNode(id, node);
980
+ if (node.selected) this.selectedNodeIds.add(id);
981
+ else this.selectedNodeIds.delete(id);
982
+ this.bumpNode(id);
983
+ this.requestRender();
984
+ this.emitNodeChanges([{ type: "replace", id, item: node }]);
985
+ };
986
+ removeNode = (id) => {
987
+ const removedEdges = [];
988
+ if (this.removeNodeInternal(id, removedEdges)) {
989
+ this.emitNodeChanges([{ type: "remove", id }]);
990
+ this.emitEdgeChanges(this.edgeRemoveChanges(removedEdges));
991
+ this.requestRender();
992
+ this.notifySelection();
993
+ }
994
+ };
995
+ // --- edge alignment ------------------------------------------------------
996
+ setEdgeAlignment = (mode) => {
997
+ this.edgeAlignment = mode;
998
+ };
999
+ getEdgeAlignment = () => this.edgeAlignment;
1000
+ getDisplayedSides = () => this.displayedSides;
1001
+ getDraggingNodeIds = () => this.draggingNodeIds;
1002
+ /**
1003
+ * Re-side one edge end toward `side`. Auto ends (no handle, or an
1004
+ * `inode-…-auto` id) have no pin to rewrite and pass through. Anchor stray
1005
+ * and bare-side ids encode their side, so they rewrite as strings (the
1006
+ * anchor chrome follows connectivity, and the target stray may not be
1007
+ * rendered yet). A registered plain handle re-sides through the registry:
1008
+ * swap to a registered handle on the facing side with a compatible type. No
1009
+ * candidate (single-handle nodes, custom layouts, a culled peer whose
1010
+ * handles are unregistered) means the pin is kept.
1011
+ */
1012
+ realignEndHandle(nodeId, handleId, side, end) {
1013
+ if (!handleId || parseAnchorAutoHandleId(handleId)) return handleId;
1014
+ const anchor = parseAnchorHandleId(handleId);
1015
+ if (anchor) return anchor.side === side ? handleId : anchorHandleId(anchor.anchorId, side);
1016
+ const record = this.handles.get(handleKey(nodeId, handleId));
1017
+ if (!record) return parseSideHandleId(handleId) ? side : handleId;
1018
+ if (record.position === side) return handleId;
1019
+ const wanted = end === "source" ? ["source", "either"] : ["target", "either"];
1020
+ for (const candidate of this.handles.values()) {
1021
+ if (candidate.nodeId === nodeId && candidate.position === side && wanted.includes(candidate.type)) {
1022
+ return candidate.handleId;
1023
+ }
1024
+ }
1025
+ return handleId;
1026
+ }
1027
+ /**
1028
+ * Under `"on-move"` and `"live"`: re-side both ends of the moved node's
1029
+ * edges to face each other. Called only from committed position mutations —
1030
+ * never from replaces, dimension echoes, reconcile, or history restores,
1031
+ * which must not move an edge the user didn't touch or rewrite handles an
1032
+ * undo just restored. Emitting in the same synchronous batch as the move
1033
+ * keeps the rewrite inside the move's history step (microtask coalescing).
1034
+ *
1035
+ * `"live"` commits the *displayed* side when the renderer resolved one this
1036
+ * gesture — the hysteresis band means the raw facing side can differ near a
1037
+ * diagonal, and the drop must not flip what the user was just shown. The
1038
+ * consumed entries are dropped afterwards so a later commit without live
1039
+ * frames (an arrow-key move) can't re-side to a stale display.
1040
+ */
1041
+ realignNodeEdges(nodeId) {
1042
+ if (this.edgeAlignment === "manual") return;
1043
+ const live = this.edgeAlignment === "live";
1044
+ const changes = [];
1045
+ for (const [edgeId, edge] of this.edges) {
1046
+ if (edge.source !== nodeId && edge.target !== nodeId) continue;
1047
+ if (edge.source === edge.target) continue;
1048
+ if (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) continue;
1049
+ const sourceRect = this.getNodeRect(edge.source);
1050
+ const targetRect = this.getNodeRect(edge.target);
1051
+ const sourceKey = displayedSideKey(edgeId, "source");
1052
+ const targetKey = displayedSideKey(edgeId, "target");
1053
+ const sourceSide = (live ? this.displayedSides.get(sourceKey) : void 0) ?? facingSide(sourceRect, targetRect);
1054
+ const targetSide = (live ? this.displayedSides.get(targetKey) : void 0) ?? facingSide(targetRect, sourceRect);
1055
+ const sourceHandle = this.realignEndHandle(
1056
+ edge.source,
1057
+ edge.sourceHandle,
1058
+ sourceSide,
1059
+ "source"
1060
+ );
1061
+ const targetHandle = this.realignEndHandle(
1062
+ edge.target,
1063
+ edge.targetHandle,
1064
+ targetSide,
1065
+ "target"
1066
+ );
1067
+ if (live) {
1068
+ const pinned = (h) => Boolean(h) && !parseAnchorAutoHandleId(h);
1069
+ if (pinned(edge.sourceHandle)) this.displayedSides.delete(sourceKey);
1070
+ if (pinned(edge.targetHandle)) this.displayedSides.delete(targetKey);
1071
+ }
1072
+ if (sourceHandle === edge.sourceHandle && targetHandle === edge.targetHandle) continue;
1073
+ const updated = { ...edge, sourceHandle, targetHandle };
1074
+ this.edges.set(edgeId, updated);
1075
+ changes.push({ type: "replace", id: edgeId, item: updated });
1076
+ }
1077
+ if (changes.length === 0) return;
1078
+ this.bumpEdges();
1079
+ this.requestRender();
1080
+ this.emitEdgeChanges(changes);
1081
+ }
1082
+ // --- drag / selection state ----------------------------------------------
1083
+ isNodeDragging = () => this.nodeDragging;
1084
+ setNodeDragging = (dragging) => {
1085
+ this.nodeDragging = dragging;
1086
+ if (!dragging) this.draggingNodeIds.clear();
1087
+ };
1088
+ isNodeResizing = () => this.nodeResizing;
1089
+ selectNode = (id, additive = false) => {
1090
+ this.setSelectionBoxActive(false);
1091
+ if (id === null) {
1092
+ this.applyNodeSelection(/* @__PURE__ */ new Set());
1093
+ this.notifySelection();
1094
+ return;
1095
+ }
1096
+ let target;
1097
+ if (additive) {
1098
+ target = new Set(this.selectedNodeIds);
1099
+ if (target.has(id)) target.delete(id);
1100
+ else target.add(id);
1101
+ } else {
1102
+ target = /* @__PURE__ */ new Set([id]);
1103
+ }
1104
+ this.applyNodeSelection(target);
1105
+ this.setEdgeSelection(null);
1106
+ this.notifySelection();
1107
+ };
1108
+ selectNodes = (ids, additive = false) => {
1109
+ this.setSelectionBoxActive(false);
1110
+ const target = additive ? new Set(this.selectedNodeIds) : /* @__PURE__ */ new Set();
1111
+ for (const id of ids) target.add(id);
1112
+ this.applyNodeSelection(target);
1113
+ if (target.size > 0) this.setEdgeSelection(null);
1114
+ this.notifySelection();
1115
+ };
1116
+ getSelectedNodes = () => [...this.selectedNodeIds];
1117
+ isNodeSelected = (id) => this.selectedNodeIds.has(id);
1118
+ getSelection = () => this.buildSelection();
1119
+ subscribeSelection = (listener) => {
1120
+ this.selectionListeners.add(listener);
1121
+ return () => {
1122
+ this.selectionListeners.delete(listener);
1123
+ };
1124
+ };
1125
+ setSelectionBoxActive = (active) => {
1126
+ if (this.selectionBoxActive === active) return;
1127
+ this.selectionBoxActive = active;
1128
+ this.bumpSelectionBox();
1129
+ };
1130
+ isSelectionBoxActive = () => this.selectionBoxActive;
1131
+ getSelectionBoxVersion = () => this.selectionBoxVersion;
1132
+ subscribeSelectionBox = (listener) => {
1133
+ this.selectionBoxListeners.add(listener);
1134
+ return () => {
1135
+ this.selectionBoxListeners.delete(listener);
1136
+ };
1137
+ };
1138
+ hoverNode = (id) => {
1139
+ this.setNodeHover(id);
1140
+ };
1141
+ getFocusedNode = () => this.focusedNodeId;
1142
+ setFocusedNode = (id) => {
1143
+ if (id !== null && !this.nodes.has(id)) return;
1144
+ if (this.focusedNodeId === id) return;
1145
+ this.focusedNodeId = id;
1146
+ for (const listener of this.focusListeners) listener(id);
1147
+ };
1148
+ subscribeFocus = (listener) => {
1149
+ this.focusListeners.add(listener);
1150
+ return () => {
1151
+ this.focusListeners.delete(listener);
1152
+ };
1153
+ };
1154
+ // --- edges ---------------------------------------------------------------
1155
+ selectEdge = (id) => {
1156
+ this.setSelectionBoxActive(false);
1157
+ this.setEdgeSelection(id);
1158
+ this.applyNodeSelection(/* @__PURE__ */ new Set());
1159
+ this.notifySelection();
1160
+ };
1161
+ addEdge = (edge) => {
1162
+ if (this.edges.has(edge.id)) return;
1163
+ this.edges.set(edge.id, edge);
1164
+ this.bumpEdges();
1165
+ this.requestRender();
1166
+ this.emitEdgeChanges([{ type: "add", item: edge }]);
1167
+ };
1168
+ replaceEdge = (id, edge) => {
1169
+ if (!this.edges.has(id)) return;
1170
+ this.edges.set(id, edge);
1171
+ if (this.selectedEdgeId === id && !edge.selected) this.selectedEdgeId = null;
1172
+ this.bumpEdges();
1173
+ this.requestRender();
1174
+ this.emitEdgeChanges([{ type: "replace", id, item: edge }]);
1175
+ };
1176
+ reconnectEdge = (id, connection) => {
1177
+ const edge = this.edges.get(id);
1178
+ if (!edge) return;
1179
+ const updated = {
1180
+ ...edge,
1181
+ source: connection.source,
1182
+ target: connection.target,
1183
+ sourceHandle: connection.sourceHandle ?? void 0,
1184
+ targetHandle: connection.targetHandle ?? void 0
1185
+ };
1186
+ this.edges.set(id, updated);
1187
+ this.bumpEdges();
1188
+ this.requestRender();
1189
+ this.emitEdgeChanges([{ type: "replace", id, item: updated }]);
1190
+ };
1191
+ removeEdge = (id) => {
1192
+ if (this.removeEdgeInternal(id)) {
1193
+ this.emitEdgeChanges([{ type: "remove", id }]);
1194
+ this.requestRender();
1195
+ this.notifySelection();
1196
+ }
1197
+ };
1198
+ deleteSelection = () => {
1199
+ const removedNodes = [];
1200
+ const removedEdges = [];
1201
+ const selectedEdge = this.selectedEdgeId;
1202
+ if (selectedEdge && this.removeEdgeInternal(selectedEdge)) {
1203
+ removedEdges.push(selectedEdge);
1204
+ }
1205
+ if (this.selectedNodeIds.size > 0) {
1206
+ for (const id of [...this.selectedNodeIds]) {
1207
+ if (this.removeNodeInternal(id, removedEdges)) removedNodes.push(id);
1208
+ }
1209
+ }
1210
+ const changed = removedNodes.length > 0 || removedEdges.length > 0;
1211
+ if (changed) {
1212
+ this.setSelectionBoxActive(false);
1213
+ this.emitNodeChanges(removedNodes.map((id) => ({ type: "remove", id })));
1214
+ this.emitEdgeChanges(this.edgeRemoveChanges(removedEdges));
1215
+ this.requestRender();
1216
+ this.notifySelection();
1217
+ }
1218
+ return changed;
1219
+ };
1220
+ // --- controlled mode -----------------------------------------------------
1221
+ setChangeHandlers = (handlers) => {
1222
+ this.nodeChangeHandler = handlers.nodes ?? null;
1223
+ this.edgeChangeHandler = handlers.edges ?? null;
1224
+ };
1225
+ reconcile = (nextNodes, nextEdges) => {
1226
+ this.reconciling = true;
1227
+ try {
1228
+ this.reconcileNodes(nextNodes);
1229
+ this.reconcileEdges(nextEdges);
1230
+ } finally {
1231
+ this.reconciling = false;
1232
+ }
1233
+ this.history.rebase();
1234
+ this.requestRender();
1235
+ };
1236
+ // --- undo / redo ---------------------------------------------------------
1237
+ /** Structural snapshot for history — no selection/hover/drag state. */
1238
+ historySnapshot() {
1239
+ const nodes = [...this.nodes.values()].map((node) => {
1240
+ const copy = { ...node, position: { ...node.position } };
1241
+ if (node.size) copy.size = { ...node.size };
1242
+ copy.selected = void 0;
1243
+ copy.hovered = void 0;
1244
+ copy.dragging = void 0;
1245
+ return copy;
1246
+ });
1247
+ const edges = [...this.edges.values()].map((edge) => {
1248
+ const copy = { ...edge };
1249
+ copy.selected = void 0;
1250
+ return copy;
1251
+ });
1252
+ return { nodes, edges };
1253
+ }
1254
+ /**
1255
+ * Restore a snapshot, emitting the diff so controlled consumers update. Emits
1256
+ * with `record: false` so the restore itself isn't re-recorded, and preserves
1257
+ * live selection/hover (undo shouldn't churn what's selected).
1258
+ */
1259
+ applyHistorySnapshot(snap) {
1260
+ const nodeChanges = [];
1261
+ const snapNodeIds = new Set(snap.nodes.map((node) => node.id));
1262
+ for (const id of [...this.nodes.keys()]) {
1263
+ if (!snapNodeIds.has(id)) {
1264
+ this.dropNodeState(id);
1265
+ nodeChanges.push({ type: "remove", id });
1266
+ }
1267
+ }
1268
+ for (const node of snap.nodes) {
1269
+ const existing = this.nodes.get(node.id);
1270
+ const merged = existing ? { ...node, selected: existing.selected, hovered: existing.hovered } : node;
1271
+ if (!existing) {
1272
+ this.writeNode(node.id, merged);
1273
+ nodeChanges.push({ type: "add", item: merged });
1274
+ } else if (!nodeControlledEqual(existing, merged)) {
1275
+ this.writeNode(node.id, merged);
1276
+ this.bumpNode(node.id);
1277
+ nodeChanges.push({ type: "replace", id: node.id, item: merged });
1278
+ }
1279
+ }
1280
+ const edgeChanges = [];
1281
+ const snapEdgeIds = new Set(snap.edges.map((edge) => edge.id));
1282
+ for (const id of [...this.edges.keys()]) {
1283
+ if (!snapEdgeIds.has(id)) {
1284
+ this.edges.delete(id);
1285
+ edgeChanges.push({ type: "remove", id });
1286
+ }
1287
+ }
1288
+ for (const edge of snap.edges) {
1289
+ const existing = this.edges.get(edge.id);
1290
+ const merged = existing ? { ...edge, selected: existing.selected } : edge;
1291
+ this.edges.set(edge.id, merged);
1292
+ edgeChanges.push(
1293
+ existing ? { type: "replace", id: edge.id, item: merged } : { type: "add", item: merged }
1294
+ );
1295
+ }
1296
+ if (this.selectedEdgeId && !this.edges.has(this.selectedEdgeId)) this.selectedEdgeId = null;
1297
+ if (edgeChanges.length > 0) this.bumpEdges();
1298
+ this.emitNodeChanges(nodeChanges, false);
1299
+ this.emitEdgeChanges(edgeChanges, false);
1300
+ this.requestRender();
1301
+ }
1302
+ undo = () => this.history.undo((snap) => this.applyHistorySnapshot(snap));
1303
+ redo = () => this.history.redo((snap) => this.applyHistorySnapshot(snap));
1304
+ canUndo = () => this.history.canUndo();
1305
+ canRedo = () => this.history.canRedo();
1306
+ clearHistory = () => this.history.clear();
1307
+ subscribeHistory = (listener) => this.history.subscribe(listener);
1308
+ // --- edge labels (renderer ↔ label layer) --------------------------------
1309
+ getEdgeLabelAnchors = () => this.edgeLabelAnchors;
1310
+ setEdgeLabelAnchors = (anchors) => {
1311
+ this.edgeLabelAnchors = anchors;
1312
+ };
1313
+ getAlignmentGuides = () => this.alignmentGuides;
1314
+ setAlignmentGuides = (guides) => {
1315
+ this.alignmentGuides = guides;
1316
+ this.requestRender();
1317
+ };
1318
+ // --- handle registry -----------------------------------------------------
1319
+ registerHandle = (record) => {
1320
+ this.handles.set(handleKey(record.nodeId, record.handleId), record);
1321
+ this.requestRender();
1322
+ };
1323
+ unregisterHandle = (nodeId, handleId) => {
1324
+ this.handles.delete(handleKey(nodeId, handleId));
1325
+ this.requestRender();
1326
+ };
1327
+ // --- anchor registry -----------------------------------------------------
1328
+ /**
1329
+ * Measure one anchor against its node element. Returns true when geometry
1330
+ * changed beyond tolerance. Bails — keeping the last-known geometry — while
1331
+ * the element or node box can't produce a meaningful measurement (detached,
1332
+ * display:none, node not yet laid out).
1333
+ */
1334
+ measureAnchor(key) {
1335
+ const record = this.anchors.get(key);
1336
+ if (!record?.element?.isConnected) return false;
1337
+ const element = record.element;
1338
+ const nodeEl = this.nodeElements.get(record.nodeId);
1339
+ if (!nodeEl) return false;
1340
+ const root = nodeEl.getBoundingClientRect();
1341
+ const box = element.getBoundingClientRect();
1342
+ if (root.width <= 0 || root.height <= 0 || box.width <= 0 && box.height <= 0) return false;
1343
+ const rects = element.getClientRects();
1344
+ const first = rects.item(0) ?? box;
1345
+ const last = rects.item(rects.length - 1) ?? first;
1346
+ const geometry = computeAnchorGeometry(root, first, last, box);
1347
+ if (anchorGeometryEqual(record.geometry, geometry)) return false;
1348
+ this.anchors.set(key, { ...record, geometry });
1349
+ return true;
1350
+ }
1351
+ registerAnchor = (nodeId, anchorId, element, options) => {
1352
+ const key = handleKey(nodeId, anchorId);
1353
+ const prev = this.anchors.get(key);
1354
+ this.anchors.set(key, {
1355
+ nodeId,
1356
+ anchorId,
1357
+ color: options?.color ?? prev?.color,
1358
+ strays: options?.strays ?? prev?.strays,
1359
+ element,
1360
+ // Keep the last-known geometry until the next successful measurement, so
1361
+ // a re-bind (display remount, editor DOM swap) never blanks the chrome.
1362
+ geometry: prev?.geometry ?? null
1363
+ });
1364
+ this.measureAnchor(key);
1365
+ this.bumpAnchors(nodeId);
1366
+ this.requestRender();
1367
+ };
1368
+ detachAnchor = (nodeId, anchorId) => {
1369
+ const key = handleKey(nodeId, anchorId);
1370
+ const record = this.anchors.get(key);
1371
+ if (!record || record.element === null) return;
1372
+ this.anchors.set(key, { ...record, element: null });
1373
+ this.bumpAnchors(nodeId);
1374
+ };
1375
+ unregisterAnchor = (nodeId, anchorId) => {
1376
+ if (!this.anchors.delete(handleKey(nodeId, anchorId))) return;
1377
+ this.bumpAnchors(nodeId);
1378
+ this.requestRender();
1379
+ };
1380
+ remeasureAnchors = (nodeId) => {
1381
+ let changed = false;
1382
+ for (const [key, record] of this.anchors) {
1383
+ if (record.nodeId === nodeId && this.measureAnchor(key)) changed = true;
1384
+ }
1385
+ if (changed) {
1386
+ this.bumpAnchors(nodeId);
1387
+ this.requestRender();
1388
+ }
1389
+ };
1390
+ getNodeAnchors = (nodeId) => {
1391
+ const records = [];
1392
+ for (const record of this.anchors.values()) {
1393
+ if (record.nodeId === nodeId) records.push(record);
1394
+ }
1395
+ return records;
1396
+ };
1397
+ getNodeAnchorsVersion = (nodeId) => this.anchorVersions.get(nodeId) ?? 0;
1398
+ subscribeNodeAnchors = (nodeId, listener) => {
1399
+ let set = this.anchorListeners.get(nodeId);
1400
+ if (!set) {
1401
+ set = /* @__PURE__ */ new Set();
1402
+ this.anchorListeners.set(nodeId, set);
1403
+ }
1404
+ set.add(listener);
1405
+ return () => {
1406
+ set.delete(listener);
1407
+ if (set.size === 0) this.anchorListeners.delete(nodeId);
1408
+ };
1409
+ };
1410
+ getEdgesVersion = () => this.edgesVersion;
1411
+ subscribeEdges = (listener) => {
1412
+ this.edgesListeners.add(listener);
1413
+ return () => {
1414
+ this.edgesListeners.delete(listener);
1415
+ };
1416
+ };
1417
+ // --- in-progress connection ----------------------------------------------
1418
+ getPending = () => this.pending;
1419
+ beginConnection = (next) => {
1420
+ this.pending = next;
1421
+ this.requestRender();
1422
+ };
1423
+ updateConnection = (to, toPosition) => {
1424
+ if (!this.pending) return;
1425
+ this.pending = { ...this.pending, to, toPosition };
1426
+ this.requestRender();
1427
+ };
1428
+ endConnection = () => {
1429
+ if (!this.pending) return;
1430
+ this.pending = null;
1431
+ this.requestRender();
1432
+ };
1433
+ // --- render loop hook ----------------------------------------------------
1434
+ requestRender = () => {
1435
+ this.renderRequester();
1436
+ };
1437
+ bindRenderRequester = (fn) => {
1438
+ this.renderRequester = fn;
1439
+ };
1440
+ // --- reconnect gateway (runtime-bound; consulted by <Handle>) ------------
1441
+ reconnectDelegate = null;
1442
+ getSelectedEdge = () => this.selectedEdgeId;
1443
+ bindReconnectDelegate = (delegate) => {
1444
+ this.reconnectDelegate = delegate;
1445
+ };
1446
+ getReconnectDelegate = () => this.reconnectDelegate;
1447
+ };
1448
+ function createRivetStore(init) {
1449
+ return new RivetGraphStore(init);
1450
+ }
1451
+
1452
+ // src/viewport.ts
1453
+ function worldToScreen(point, viewport) {
1454
+ return {
1455
+ x: point.x * viewport.zoom + viewport.x,
1456
+ y: point.y * viewport.zoom + viewport.y
1457
+ };
1458
+ }
1459
+ function screenToWorld(point, viewport) {
1460
+ return {
1461
+ x: (point.x - viewport.x) / viewport.zoom,
1462
+ y: (point.y - viewport.y) / viewport.zoom
1463
+ };
1464
+ }
1465
+ function zoomAt(viewport, anchor, nextZoom, minZoom = 0.1, maxZoom = 4) {
1466
+ const zoom = clamp(nextZoom, minZoom, maxZoom);
1467
+ const world = screenToWorld(anchor, viewport);
1468
+ return {
1469
+ zoom,
1470
+ x: anchor.x - world.x * zoom,
1471
+ y: anchor.y - world.y * zoom
1472
+ };
1473
+ }
1474
+ function viewportToCss(viewport) {
1475
+ return `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
1476
+ }
1477
+ function visibleWorldRect(viewport, width, height) {
1478
+ const topLeft = screenToWorld({ x: 0, y: 0 }, viewport);
1479
+ const bottomRight = screenToWorld({ x: width, y: height }, viewport);
1480
+ return {
1481
+ x: topLeft.x,
1482
+ y: topLeft.y,
1483
+ width: bottomRight.x - topLeft.x,
1484
+ height: bottomRight.y - topLeft.y
1485
+ };
1486
+ }
1487
+ function rectsIntersect(a, b) {
1488
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
1489
+ }
1490
+
1491
+ // src/connection.ts
1492
+ function clientToWorld(store, paneRef, clientX, clientY) {
1493
+ const rect = paneRef.current?.getBoundingClientRect();
1494
+ const point = { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };
1495
+ return screenToWorld(point, store.getViewport());
1496
+ }
1497
+ function isCompatible(from, hit) {
1498
+ const canSource = from.type !== "target";
1499
+ const canTarget = from.type !== "source";
1500
+ return hit.nodeId !== from.nodeId && (canSource && hit.type !== "source" || canTarget && hit.type !== "target");
1501
+ }
1502
+ function findHandleAt(clientX, clientY) {
1503
+ const element = document.elementFromPoint(clientX, clientY);
1504
+ const handleEl = element instanceof Element ? element.closest(SELECTOR.handle) : null;
1505
+ if (!handleEl) return null;
1506
+ const { rivetHandleNode, rivetHandleId, rivetHandleType } = handleEl.dataset;
1507
+ if (!rivetHandleNode || !rivetHandleId || !rivetHandleType) return null;
1508
+ return {
1509
+ nodeId: rivetHandleNode,
1510
+ handleId: rivetHandleId,
1511
+ type: rivetHandleType
1512
+ };
1513
+ }
1514
+ function handleWorldPosition(store, hit) {
1515
+ const record = store.handles.get(handleKey(hit.nodeId, hit.handleId));
1516
+ const node = store.nodes.get(hit.nodeId);
1517
+ if (!record || !node) return null;
1518
+ const origin = store.getNodeWorldPosition(hit.nodeId);
1519
+ return { x: origin.x + record.offset.x, y: origin.y + record.offset.y };
1520
+ }
1521
+ function handleSide(store, hit) {
1522
+ return store.handles.get(handleKey(hit.nodeId, hit.handleId))?.position ?? null;
1523
+ }
1524
+ function resolveConnectionHit(store, paneRef, from, clientX, clientY) {
1525
+ const direct = findHandleAt(clientX, clientY);
1526
+ if (direct && isCompatible(from, direct)) return direct;
1527
+ const el = document.elementFromPoint(clientX, clientY);
1528
+ const nodeEl = el instanceof Element ? el.closest(SELECTOR.node) : null;
1529
+ const overNodeId = nodeEl?.dataset.rivetNode;
1530
+ if (!overNodeId || overNodeId === from.nodeId) return null;
1531
+ const world = clientToWorld(store, paneRef, clientX, clientY);
1532
+ let best = null;
1533
+ let bestDist = Number.POSITIVE_INFINITY;
1534
+ for (const record of store.handles.values()) {
1535
+ if (record.nodeId !== overNodeId) continue;
1536
+ const candidate = {
1537
+ nodeId: record.nodeId,
1538
+ handleId: record.handleId,
1539
+ type: record.type
1540
+ };
1541
+ if (!isCompatible(from, candidate)) continue;
1542
+ const node = store.nodes.get(record.nodeId);
1543
+ if (!node) continue;
1544
+ const origin = store.getNodeWorldPosition(record.nodeId);
1545
+ const dx = origin.x + record.offset.x - world.x;
1546
+ const dy = origin.y + record.offset.y - world.y;
1547
+ const dist = dx * dx + dy * dy;
1548
+ if (dist < bestDist) {
1549
+ bestDist = dist;
1550
+ best = candidate;
1551
+ }
1552
+ }
1553
+ return best;
1554
+ }
1555
+ function orientConnection(from, hit) {
1556
+ const canSource = from.type !== "target";
1557
+ const draggedAsSource = canSource && hit.type !== "source";
1558
+ return draggedAsSource ? {
1559
+ source: from.nodeId,
1560
+ sourceHandle: from.handleId,
1561
+ target: hit.nodeId,
1562
+ targetHandle: hit.handleId
1563
+ } : {
1564
+ source: hit.nodeId,
1565
+ sourceHandle: hit.handleId,
1566
+ target: from.nodeId,
1567
+ targetHandle: from.handleId
1568
+ };
1569
+ }
1570
+
1571
+ // src/input/connection-drag.ts
1572
+ function startConnectionDrag(config) {
1573
+ const {
1574
+ store,
1575
+ paneRef,
1576
+ from,
1577
+ begin,
1578
+ commit,
1579
+ validate,
1580
+ map,
1581
+ onEnd,
1582
+ resolveHit = resolveConnectionHit
1583
+ } = config;
1584
+ store.beginConnection(begin);
1585
+ const onMove = (event) => {
1586
+ const hit = resolveHit(store, paneRef, from, event.clientX, event.clientY);
1587
+ const snapped = hit && handleWorldPosition(store, hit);
1588
+ store.updateConnection(
1589
+ snapped ?? clientToWorld(store, paneRef, event.clientX, event.clientY),
1590
+ snapped && hit ? handleSide(store, hit) ?? void 0 : void 0
1591
+ );
1592
+ };
1593
+ const detach = () => {
1594
+ window.removeEventListener("pointermove", onMove);
1595
+ window.removeEventListener("pointerup", onUp);
1596
+ window.removeEventListener("pointercancel", onCancel);
1597
+ };
1598
+ const onUp = (event) => {
1599
+ detach();
1600
+ if (!store.getPending()) {
1601
+ onEnd?.();
1602
+ return;
1603
+ }
1604
+ const hit = resolveHit(store, paneRef, from, event.clientX, event.clientY);
1605
+ if (hit) {
1606
+ const connection = orientConnection(from, hit);
1607
+ if (!validate || validate(connection)) {
1608
+ const mapped = map ? map(connection) : connection;
1609
+ if (mapped) commit(mapped);
1610
+ }
1611
+ }
1612
+ onEnd?.();
1613
+ store.endConnection();
1614
+ };
1615
+ const onCancel = () => {
1616
+ detach();
1617
+ onEnd?.();
1618
+ store.endConnection();
1619
+ };
1620
+ window.addEventListener("pointermove", onMove);
1621
+ window.addEventListener("pointerup", onUp);
1622
+ window.addEventListener("pointercancel", onCancel);
1623
+ }
1624
+ var SIZE = 10;
1625
+ var positionStyles = {
1626
+ right: { right: 0, top: "50%", transform: "translate(50%, -50%)" },
1627
+ left: { left: 0, top: "50%", transform: "translate(-50%, -50%)" },
1628
+ top: { top: 0, left: "50%", transform: "translate(-50%, -50%)" },
1629
+ bottom: { bottom: 0, left: "50%", transform: "translate(-50%, 50%)" }
1630
+ };
1631
+ function Handle({
1632
+ type,
1633
+ id,
1634
+ position,
1635
+ className,
1636
+ style,
1637
+ ariaLabel
1638
+ }) {
1639
+ const { store, paneRef, isValidConnection, mapConnection, onConnect, announce } = useRivetContext();
1640
+ const { nodeId, wrapperRef } = useRivetNodeContext();
1641
+ const ref = useRef(null);
1642
+ const resolvedType = type ?? "either";
1643
+ const place = position ?? (resolvedType === "target" ? "left" : "right");
1644
+ const handleId = id ?? place;
1645
+ useEffect(() => {
1646
+ const el = ref.current;
1647
+ const wrapper = wrapperRef.current;
1648
+ if (!el || !wrapper) return;
1649
+ const measure = () => {
1650
+ const zoom = store.getViewport().zoom || 1;
1651
+ const hr = el.getBoundingClientRect();
1652
+ const wr = wrapper.getBoundingClientRect();
1653
+ store.registerHandle({
1654
+ nodeId,
1655
+ handleId,
1656
+ type: resolvedType,
1657
+ position: place,
1658
+ offset: {
1659
+ x: (hr.left + hr.width / 2 - wr.left) / zoom,
1660
+ y: (hr.top + hr.height / 2 - wr.top) / zoom
1661
+ }
1662
+ });
1663
+ };
1664
+ measure();
1665
+ const observer = new ResizeObserver(measure);
1666
+ observer.observe(wrapper);
1667
+ observer.observe(el);
1668
+ return () => {
1669
+ observer.disconnect();
1670
+ store.unregisterHandle(nodeId, handleId);
1671
+ };
1672
+ }, [store, wrapperRef, nodeId, handleId, resolvedType, place]);
1673
+ const nodeName = (id2) => store.nodes.get(id2)?.ariaLabel ?? `node ${id2}`;
1674
+ const onKeyDown = (event) => {
1675
+ if (event.key === "Escape") {
1676
+ if (!store.getPending()) return;
1677
+ event.preventDefault();
1678
+ event.stopPropagation();
1679
+ store.endConnection();
1680
+ announce("Connection cancelled.");
1681
+ return;
1682
+ }
1683
+ if (event.key !== "Enter" && event.key !== " ") return;
1684
+ event.preventDefault();
1685
+ const self = { nodeId, handleId, type: resolvedType };
1686
+ const pending = store.getPending();
1687
+ if (!pending) {
1688
+ const origin = handleWorldPosition(store, self);
1689
+ if (!origin) return;
1690
+ store.beginConnection({
1691
+ source: nodeId,
1692
+ sourceHandle: handleId,
1693
+ sourceType: resolvedType,
1694
+ from: origin,
1695
+ to: origin
1696
+ });
1697
+ announce(
1698
+ `Connection started from ${nodeName(nodeId)}. Move focus to a handle on another node and press Enter to connect, or press Escape to cancel.`
1699
+ );
1700
+ return;
1701
+ }
1702
+ if (pending.source === nodeId && pending.sourceHandle === handleId) {
1703
+ store.endConnection();
1704
+ announce("Connection cancelled.");
1705
+ return;
1706
+ }
1707
+ const from = {
1708
+ nodeId: pending.source,
1709
+ handleId: pending.sourceHandle,
1710
+ type: pending.sourceType
1711
+ };
1712
+ if (!isCompatible(from, self)) {
1713
+ announce("These handles cannot be connected.");
1714
+ return;
1715
+ }
1716
+ const connection = orientConnection(from, self);
1717
+ if (isValidConnection && !isValidConnection(connection)) {
1718
+ announce("Connection rejected.");
1719
+ return;
1720
+ }
1721
+ const mapped = mapConnection ? mapConnection(connection) : connection;
1722
+ if (mapped) {
1723
+ store.addEdge({
1724
+ id: createId("edge"),
1725
+ source: mapped.source,
1726
+ target: mapped.target,
1727
+ sourceHandle: mapped.sourceHandle ?? void 0,
1728
+ targetHandle: mapped.targetHandle ?? void 0
1729
+ });
1730
+ onConnect?.(mapped);
1731
+ announce(`Connected ${nodeName(mapped.source)} to ${nodeName(mapped.target)}.`);
1732
+ }
1733
+ store.endConnection();
1734
+ };
1735
+ const onFocus = () => {
1736
+ const pending = store.getPending();
1737
+ if (!pending || pending.source === nodeId && pending.sourceHandle === handleId) return;
1738
+ const world = handleWorldPosition(store, { nodeId, handleId});
1739
+ if (world) store.updateConnection(world, place);
1740
+ };
1741
+ const onPointerDown = (event) => {
1742
+ if (event.button !== 0) return;
1743
+ event.stopPropagation();
1744
+ event.preventDefault();
1745
+ if (!event.altKey) {
1746
+ const delegate = store.getReconnectDelegate();
1747
+ const endpointHit = delegate?.pickEndpoint(event.clientX, event.clientY);
1748
+ if (delegate && endpointHit) {
1749
+ delegate.begin(endpointHit.edgeId, endpointHit.end);
1750
+ return;
1751
+ }
1752
+ }
1753
+ const from = { nodeId, handleId, type: resolvedType };
1754
+ const start = handleWorldPosition(store, from);
1755
+ const origin = start ?? clientToWorld(store, paneRef, event.clientX, event.clientY);
1756
+ startConnectionDrag({
1757
+ store,
1758
+ paneRef,
1759
+ from,
1760
+ begin: {
1761
+ source: nodeId,
1762
+ sourceHandle: handleId,
1763
+ sourceType: resolvedType,
1764
+ from: origin,
1765
+ to: origin
1766
+ },
1767
+ validate: isValidConnection,
1768
+ map: mapConnection,
1769
+ commit: (connection) => {
1770
+ store.addEdge({
1771
+ id: createId("edge"),
1772
+ source: connection.source,
1773
+ target: connection.target,
1774
+ sourceHandle: connection.sourceHandle ?? void 0,
1775
+ targetHandle: connection.targetHandle ?? void 0
1776
+ });
1777
+ onConnect?.(connection);
1778
+ }
1779
+ });
1780
+ };
1781
+ return (
1782
+ // biome-ignore lint/a11y/useSemanticElements: the handle doubles as a drag affordance and measurement target, so it can't be a native <button>
1783
+ /* @__PURE__ */ jsx(
1784
+ "div",
1785
+ {
1786
+ ref,
1787
+ className,
1788
+ tabIndex: 0,
1789
+ role: "button",
1790
+ "aria-roledescription": "connection handle",
1791
+ "aria-label": ariaLabel ?? `${handleId} handle of ${nodeName(nodeId)}`,
1792
+ onPointerDown,
1793
+ onKeyDown,
1794
+ onFocus,
1795
+ "data-rivet-handle": "",
1796
+ "data-rivet-handle-node": nodeId,
1797
+ "data-rivet-handle-id": handleId,
1798
+ "data-rivet-handle-type": resolvedType,
1799
+ style: {
1800
+ position: "absolute",
1801
+ width: SIZE,
1802
+ height: SIZE,
1803
+ borderRadius: "50%",
1804
+ background: "#ffffff",
1805
+ border: "1.5px solid #6366f1",
1806
+ boxSizing: "border-box",
1807
+ pointerEvents: "auto",
1808
+ cursor: "crosshair",
1809
+ zIndex: 1,
1810
+ ...positionStyles[place],
1811
+ ...style
1812
+ }
1813
+ }
1814
+ )
1815
+ );
1816
+ }
1817
+ var baseStyle = {
1818
+ position: "relative",
1819
+ minWidth: 120,
1820
+ padding: "10px 14px",
1821
+ borderRadius: 8,
1822
+ border: "1px solid rgba(100, 116, 139, 0.4)",
1823
+ background: "#ffffff",
1824
+ color: "#0f172a",
1825
+ fontSize: 13,
1826
+ lineHeight: 1.3,
1827
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08)"
1828
+ };
1829
+ var hoveredStyle = {
1830
+ borderColor: "rgba(99, 102, 241, 0.6)"
1831
+ };
1832
+ var selectedStyle = {
1833
+ borderColor: "#6366f1",
1834
+ boxShadow: "0 0 0 2px rgba(99, 102, 241, 0.35)"
1835
+ };
1836
+ function DefaultNode({ data, selected, hovered }) {
1837
+ const label = data && typeof data === "object" && "label" in data ? String(data.label) : "Node";
1838
+ const style = selected ? { ...baseStyle, ...selectedStyle } : hovered ? { ...baseStyle, ...hoveredStyle } : baseStyle;
1839
+ return /* @__PURE__ */ jsxs("div", { style, children: [
1840
+ /* @__PURE__ */ jsx(Handle, { type: "target", position: "left" }),
1841
+ label,
1842
+ /* @__PURE__ */ jsx(Handle, { type: "source", position: "right" })
1843
+ ] });
1844
+ }
1845
+ var baseStyle2 = {
1846
+ position: "relative",
1847
+ width: "100%",
1848
+ height: "100%",
1849
+ // Keeps an unsized group visible; give real bounds via `width`/`height`.
1850
+ minWidth: 150,
1851
+ minHeight: 100,
1852
+ boxSizing: "border-box",
1853
+ borderRadius: 10,
1854
+ border: "1.5px dashed rgba(100, 116, 139, 0.45)",
1855
+ background: "rgba(100, 116, 139, 0.06)"
1856
+ };
1857
+ var hoveredStyle2 = {
1858
+ borderColor: "rgba(99, 102, 241, 0.6)"
1859
+ };
1860
+ var selectedStyle2 = {
1861
+ borderColor: "#6366f1",
1862
+ background: "rgba(99, 102, 241, 0.08)"
1863
+ };
1864
+ var labelStyle = {
1865
+ position: "absolute",
1866
+ top: 6,
1867
+ left: 10,
1868
+ fontSize: 11,
1869
+ fontWeight: 500,
1870
+ color: "rgba(100, 116, 139, 0.9)",
1871
+ pointerEvents: "none",
1872
+ userSelect: "none"
1873
+ };
1874
+ function GroupNode({ data, selected, hovered }) {
1875
+ const label = data && typeof data === "object" && "label" in data ? String(data.label) : null;
1876
+ const style = selected ? { ...baseStyle2, ...selectedStyle2 } : hovered ? { ...baseStyle2, ...hoveredStyle2 } : baseStyle2;
1877
+ return /* @__PURE__ */ jsx("div", { style, children: label !== null && /* @__PURE__ */ jsx("div", { style: labelStyle, children: label }) });
1878
+ }
1879
+ var BUILTIN_NODE_TYPES = {
1880
+ default: DefaultNode,
1881
+ group: GroupNode
1882
+ };
1883
+ var MAP_PADDING = 8;
1884
+ var containerStyle = {
1885
+ position: "absolute",
1886
+ bottom: 16,
1887
+ right: 16,
1888
+ borderRadius: 8,
1889
+ overflow: "hidden",
1890
+ border: "1px solid rgba(100, 116, 139, 0.4)",
1891
+ background: "rgba(248, 250, 252, 0.9)",
1892
+ boxShadow: "0 1px 3px rgba(15, 23, 42, 0.12)",
1893
+ cursor: "pointer"
1894
+ };
1895
+ function MiniMap({
1896
+ width = 200,
1897
+ height = 150,
1898
+ nodeColor = "rgba(100, 116, 139, 0.7)",
1899
+ selectedColor = "#6366f1",
1900
+ maskColor = "rgba(15, 23, 42, 0.12)",
1901
+ className,
1902
+ style
1903
+ }) {
1904
+ const { store, paneRef } = useRivetContext();
1905
+ const canvasRef = useRef(null);
1906
+ const transformRef = useRef({ scale: 1, offsetX: 0, offsetY: 0 });
1907
+ useEffect(() => {
1908
+ const canvas = canvasRef.current;
1909
+ const ctx = canvas?.getContext("2d");
1910
+ if (!canvas || !ctx) return;
1911
+ const dpr = window.devicePixelRatio || 1;
1912
+ canvas.width = Math.round(width * dpr);
1913
+ canvas.height = Math.round(height * dpr);
1914
+ const draw = () => {
1915
+ const viewport = store.getViewport();
1916
+ const pane = paneRef.current;
1917
+ const paneWidth = pane?.clientWidth ?? width;
1918
+ const paneHeight = pane?.clientHeight ?? height;
1919
+ const view = visibleWorldRect(viewport, paneWidth, paneHeight);
1920
+ let minX = view.x;
1921
+ let minY = view.y;
1922
+ let maxX = view.x + view.width;
1923
+ let maxY = view.y + view.height;
1924
+ for (const id of store.nodes.keys()) {
1925
+ const box = store.getNodeRect(id);
1926
+ minX = Math.min(minX, box.x);
1927
+ minY = Math.min(minY, box.y);
1928
+ maxX = Math.max(maxX, box.x + box.width);
1929
+ maxY = Math.max(maxY, box.y + box.height);
1930
+ }
1931
+ const boundsWidth = maxX - minX || 1;
1932
+ const boundsHeight = maxY - minY || 1;
1933
+ const innerWidth = width - MAP_PADDING * 2;
1934
+ const innerHeight = height - MAP_PADDING * 2;
1935
+ const scale = Math.min(innerWidth / boundsWidth, innerHeight / boundsHeight);
1936
+ const offsetX = MAP_PADDING + (innerWidth - boundsWidth * scale) / 2 - minX * scale;
1937
+ const offsetY = MAP_PADDING + (innerHeight - boundsHeight * scale) / 2 - minY * scale;
1938
+ transformRef.current = { scale, offsetX, offsetY };
1939
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1940
+ ctx.clearRect(0, 0, width, height);
1941
+ for (const node of store.nodes.values()) {
1942
+ const box = store.getNodeRect(node.id);
1943
+ ctx.fillStyle = node.selected ? selectedColor : nodeColor;
1944
+ ctx.fillRect(
1945
+ box.x * scale + offsetX,
1946
+ box.y * scale + offsetY,
1947
+ Math.max(1, box.width * scale),
1948
+ Math.max(1, box.height * scale)
1949
+ );
1950
+ }
1951
+ const vx = view.x * scale + offsetX;
1952
+ const vy = view.y * scale + offsetY;
1953
+ const vw = view.width * scale;
1954
+ const vh = view.height * scale;
1955
+ ctx.fillStyle = maskColor;
1956
+ ctx.beginPath();
1957
+ ctx.rect(0, 0, width, height);
1958
+ ctx.rect(vx, vy, vw, vh);
1959
+ ctx.fill("evenodd");
1960
+ ctx.strokeStyle = "rgba(99, 102, 241, 0.9)";
1961
+ ctx.lineWidth = 1;
1962
+ ctx.strokeRect(vx, vy, vw, vh);
1963
+ };
1964
+ draw();
1965
+ return store.subscribeFrame(draw);
1966
+ }, [store, paneRef, width, height, nodeColor, selectedColor, maskColor]);
1967
+ const recenter = (event) => {
1968
+ const canvas = canvasRef.current;
1969
+ const pane = paneRef.current;
1970
+ if (!canvas || !pane) return;
1971
+ const rect = canvas.getBoundingClientRect();
1972
+ const { scale, offsetX, offsetY } = transformRef.current;
1973
+ const worldX = (event.clientX - rect.left - offsetX) / scale;
1974
+ const worldY = (event.clientY - rect.top - offsetY) / scale;
1975
+ const viewport = store.getViewport();
1976
+ store.setViewport({
1977
+ zoom: viewport.zoom,
1978
+ x: pane.clientWidth / 2 - worldX * viewport.zoom,
1979
+ y: pane.clientHeight / 2 - worldY * viewport.zoom
1980
+ });
1981
+ store.requestRender();
1982
+ };
1983
+ const onPointerDown = (event) => {
1984
+ if (event.button !== 0) return;
1985
+ event.currentTarget.setPointerCapture(event.pointerId);
1986
+ recenter(event);
1987
+ };
1988
+ const onPointerMove = (event) => {
1989
+ if (event.buttons & 1) recenter(event);
1990
+ };
1991
+ return /* @__PURE__ */ jsx("div", { className, style: { ...containerStyle, ...style, width, height }, children: /* @__PURE__ */ jsx(
1992
+ "canvas",
1993
+ {
1994
+ ref: canvasRef,
1995
+ style: { width, height, display: "block" },
1996
+ onPointerDown,
1997
+ onPointerMove
1998
+ }
1999
+ ) });
2000
+ }
2001
+ var NS = "ns-resize";
2002
+ var EW = "ew-resize";
2003
+ var NWSE = "nwse-resize";
2004
+ var NESW = "nesw-resize";
2005
+ var HANDLES = [
2006
+ { key: "nw", top: true, left: true, style: { top: 0, left: 0 }, cursor: NWSE },
2007
+ { key: "n", top: true, style: { top: 0, left: "50%" }, cursor: NS },
2008
+ { key: "ne", top: true, right: true, style: { top: 0, left: "100%" }, cursor: NESW },
2009
+ { key: "e", right: true, style: { top: "50%", left: "100%" }, cursor: EW },
2010
+ { key: "se", bottom: true, right: true, style: { top: "100%", left: "100%" }, cursor: NWSE },
2011
+ { key: "s", bottom: true, style: { top: "100%", left: "50%" }, cursor: NS },
2012
+ { key: "sw", bottom: true, left: true, style: { top: "100%", left: 0 }, cursor: NESW },
2013
+ { key: "w", left: true, style: { top: "50%", left: 0 }, cursor: EW }
2014
+ ];
2015
+ var dotStyle = (color) => ({
2016
+ position: "absolute",
2017
+ width: 8,
2018
+ height: 8,
2019
+ background: "#ffffff",
2020
+ border: `1.5px solid ${color}`,
2021
+ borderRadius: 2,
2022
+ boxSizing: "border-box",
2023
+ pointerEvents: "auto",
2024
+ zIndex: 2
2025
+ });
2026
+ function NodeResizer({
2027
+ minWidth = 20,
2028
+ minHeight = 20,
2029
+ maxWidth = Number.POSITIVE_INFINITY,
2030
+ maxHeight = Number.POSITIVE_INFINITY,
2031
+ keepAspectRatio = false,
2032
+ color = "#6366f1",
2033
+ onResize,
2034
+ onResizeEnd
2035
+ }) {
2036
+ const { store } = useRivetContext();
2037
+ const { nodeId } = useRivetNodeContext();
2038
+ const startResize = (dir) => (event) => {
2039
+ if (event.button !== 0) return;
2040
+ event.stopPropagation();
2041
+ event.preventDefault();
2042
+ const node = store.nodes.get(nodeId);
2043
+ if (!node) return;
2044
+ const zoom = store.getViewport().zoom || 1;
2045
+ const startW = node.width ?? node.size?.width ?? 0;
2046
+ const startH = node.height ?? node.size?.height ?? 0;
2047
+ const start = node.position;
2048
+ const startX = event.clientX;
2049
+ const startY = event.clientY;
2050
+ const ratio = startH > 0 ? startW / startH : 1;
2051
+ const isCorner = (dir.left || dir.right) && (dir.top || dir.bottom);
2052
+ const compute = (moveEvent) => {
2053
+ const dx = (moveEvent.clientX - startX) / zoom;
2054
+ const dy = (moveEvent.clientY - startY) / zoom;
2055
+ let width = startW + (dir.right ? dx : dir.left ? -dx : 0);
2056
+ let height = startH + (dir.bottom ? dy : dir.top ? -dy : 0);
2057
+ width = clamp(width, minWidth, maxWidth);
2058
+ height = clamp(height, minHeight, maxHeight);
2059
+ if (keepAspectRatio && isCorner) {
2060
+ height = clamp(width / ratio, minHeight, maxHeight);
2061
+ width = height * ratio;
2062
+ }
2063
+ const position = {
2064
+ x: dir.left ? start.x + (startW - width) : start.x,
2065
+ y: dir.top ? start.y + (startH - height) : start.y
2066
+ };
2067
+ return { size: { width, height }, position };
2068
+ };
2069
+ const onMove = (moveEvent) => {
2070
+ const { size, position } = compute(moveEvent);
2071
+ const moved = dir.left || dir.top;
2072
+ store.resizeNode(nodeId, size, moved ? position : void 0, false);
2073
+ onResize?.(size, position);
2074
+ };
2075
+ const onUp = (upEvent) => {
2076
+ window.removeEventListener("pointermove", onMove);
2077
+ window.removeEventListener("pointerup", onUp);
2078
+ const { size, position } = compute(upEvent);
2079
+ const moved = dir.left || dir.top;
2080
+ store.resizeNode(nodeId, size, moved ? position : void 0, true);
2081
+ onResizeEnd?.(size, position);
2082
+ };
2083
+ window.addEventListener("pointermove", onMove);
2084
+ window.addEventListener("pointerup", onUp);
2085
+ };
2086
+ return /* @__PURE__ */ jsx(Fragment, { children: HANDLES.map((dir) => /* @__PURE__ */ jsx(
2087
+ "div",
2088
+ {
2089
+ "data-rivet-resize-handle": dir.key,
2090
+ onPointerDown: startResize(dir),
2091
+ style: {
2092
+ ...dotStyle(color),
2093
+ ...dir.style,
2094
+ transform: "translate(-50%, -50%)",
2095
+ cursor: dir.cursor
2096
+ }
2097
+ },
2098
+ dir.key
2099
+ )) });
2100
+ }
2101
+ function shallowEqual(a, b) {
2102
+ if (Object.is(a, b)) return true;
2103
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
2104
+ const aArr = Array.isArray(a);
2105
+ if (aArr !== Array.isArray(b)) return false;
2106
+ if (aArr) {
2107
+ const bArr = b;
2108
+ if (a.length !== bArr.length) return false;
2109
+ return a.every((v, i) => Object.is(v, bArr[i]));
2110
+ }
2111
+ const aKeys = Object.keys(a);
2112
+ const bObj = b;
2113
+ if (aKeys.length !== Object.keys(bObj).length) return false;
2114
+ return aKeys.every(
2115
+ (k) => Object.hasOwn(bObj, k) && Object.is(a[k], bObj[k])
2116
+ );
2117
+ }
2118
+ function useShallowStable(value) {
2119
+ const ref = useRef(value);
2120
+ if (!shallowEqual(ref.current, value)) ref.current = value;
2121
+ return ref.current;
2122
+ }
2123
+ function useStableOptional(fn) {
2124
+ const ref = useRef(fn);
2125
+ ref.current = fn;
2126
+ const stable = useRef(null);
2127
+ if (stable.current === null) stable.current = (...args) => ref.current?.(...args);
2128
+ return fn ? stable.current : void 0;
2129
+ }
2130
+ function useRivetConfig(params) {
2131
+ const panOnDrag = useShallowStable(params.panOnDrag);
2132
+ const selectionKeyCode = useShallowStable(params.selectionKeyCode);
2133
+ const multiSelectionKeyCode = useShallowStable(params.multiSelectionKeyCode);
2134
+ const panButtons = useMemo(
2135
+ () => panOnDrag === true ? [0] : panOnDrag === false ? [] : panOnDrag,
2136
+ [panOnDrag]
2137
+ );
2138
+ const selectionKeys = useMemo(
2139
+ () => selectionKeyCode == null ? [] : Array.isArray(selectionKeyCode) ? selectionKeyCode : [selectionKeyCode],
2140
+ [selectionKeyCode]
2141
+ );
2142
+ const multiSelectionKeys = useMemo(
2143
+ () => Array.isArray(multiSelectionKeyCode) ? multiSelectionKeyCode : [multiSelectionKeyCode],
2144
+ [multiSelectionKeyCode]
2145
+ );
2146
+ const nodeTypes = useShallowStable(params.nodeTypes);
2147
+ const edgeTypes = useShallowStable(params.edgeTypes);
2148
+ const defaultEdgeOptions = useShallowStable(params.defaultEdgeOptions);
2149
+ const snapGrid = useShallowStable(params.snapGrid ?? null);
2150
+ const swimlaneMargin = useShallowStable(params.swimlaneMargin);
2151
+ const anchorOptionsInput = useShallowStable(params.anchorOptions);
2152
+ const anchorOptions = useMemo(
2153
+ () => ({ ...DEFAULT_ANCHOR_OPTIONS, ...anchorOptionsInput }),
2154
+ [anchorOptionsInput]
2155
+ );
2156
+ const isValidConnection = useStableOptional(params.isValidConnection);
2157
+ const mapConnection = useStableOptional(params.mapConnection);
2158
+ const onConnect = useStableOptional(params.onConnect);
2159
+ const onLaneChange = useStableOptional(params.onLaneChange);
2160
+ const renderSwimlaneHeader = useStableOptional(params.renderSwimlaneHeader);
2161
+ const renderSwimlaneLabel = useStableOptional(params.renderSwimlaneLabel);
2162
+ return {
2163
+ nodeTypes,
2164
+ edgeTypes,
2165
+ defaultEdgeOptions,
2166
+ snapGrid,
2167
+ swimlaneMargin,
2168
+ anchorOptions,
2169
+ panButtons,
2170
+ selectionKeys,
2171
+ multiSelectionKeys,
2172
+ isValidConnection,
2173
+ mapConnection,
2174
+ onConnect,
2175
+ onLaneChange,
2176
+ renderSwimlaneHeader,
2177
+ renderSwimlaneLabel
2178
+ };
2179
+ }
2180
+
2181
+ // src/input/keyboard.ts
2182
+ var ARROW_STEP = 10;
2183
+ var ARROW_STEP_SHIFT = 5;
2184
+ var ARROW_DIRS = {
2185
+ ArrowUp: { x: 0, y: -1 },
2186
+ ArrowDown: { x: 0, y: 1 },
2187
+ ArrowLeft: { x: -1, y: 0 },
2188
+ ArrowRight: { x: 1, y: 0 }
2189
+ };
2190
+ function createKeyboardHandler(store) {
2191
+ return (event) => {
2192
+ if (event.defaultPrevented) return;
2193
+ const active = document.activeElement;
2194
+ if (active instanceof HTMLElement && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable)) {
2195
+ return;
2196
+ }
2197
+ if (event.key === "Delete" || event.key === "Backspace") {
2198
+ if (store.deleteSelection()) event.preventDefault();
2199
+ return;
2200
+ }
2201
+ if (event.key === "Escape") {
2202
+ if (store.getPending()) store.endConnection();
2203
+ else if (store.isSelectionBoxActive()) store.selectNode(null);
2204
+ return;
2205
+ }
2206
+ const dir = ARROW_DIRS[event.key];
2207
+ if (!dir) return;
2208
+ const movers = store.getMovers(store.getSelectedNodes());
2209
+ if (movers.length === 0) return;
2210
+ event.preventDefault();
2211
+ const step = event.shiftKey ? ARROW_STEP * ARROW_STEP_SHIFT : ARROW_STEP;
2212
+ for (const id of movers) {
2213
+ const node = store.nodes.get(id);
2214
+ if (!node) continue;
2215
+ store.moveNode(
2216
+ id,
2217
+ { x: node.position.x + dir.x * step, y: node.position.y + dir.y * step },
2218
+ true
2219
+ );
2220
+ }
2221
+ };
2222
+ }
2223
+
2224
+ // src/input/keys.ts
2225
+ var MODIFIER_KEYS = /* @__PURE__ */ new Set(["Shift", "Control", "Meta", "Alt"]);
2226
+ function keyHeld(event, keys, pressedKeys) {
2227
+ return keys.some(
2228
+ (key) => MODIFIER_KEYS.has(key) ? event.getModifierState(key) : pressedKeys?.has(key) ?? false
2229
+ );
2230
+ }
2231
+
2232
+ // src/input/layer-hint.ts
2233
+ function createLayerHint(layer, idleMs) {
2234
+ let lastTransform = "";
2235
+ let timer = 0;
2236
+ return {
2237
+ apply(transform) {
2238
+ if (transform === lastTransform) return;
2239
+ lastTransform = transform;
2240
+ layer.style.transform = transform;
2241
+ layer.style.willChange = "transform";
2242
+ if (timer) clearTimeout(timer);
2243
+ timer = window.setTimeout(() => {
2244
+ layer.style.willChange = "auto";
2245
+ }, idleMs);
2246
+ },
2247
+ dispose() {
2248
+ if (timer) clearTimeout(timer);
2249
+ }
2250
+ };
2251
+ }
2252
+
2253
+ // src/input/marquee-controller.ts
2254
+ var OVERLAY_STYLE = "position:absolute;pointer-events:none;display:none;border:1px solid rgba(99,102,241,0.9);background:rgba(99,102,241,0.12);";
2255
+ function createMarqueeController(deps) {
2256
+ const { store, pane } = deps;
2257
+ let active = false;
2258
+ let additive = false;
2259
+ let base = [];
2260
+ let start = { x: 0, y: 0 };
2261
+ const overlay = document.createElement("div");
2262
+ overlay.style.cssText = OVERLAY_STYLE;
2263
+ pane.appendChild(overlay);
2264
+ const apply = (px, py) => {
2265
+ const left = Math.min(start.x, px);
2266
+ const top = Math.min(start.y, py);
2267
+ const width = Math.abs(px - start.x);
2268
+ const height = Math.abs(py - start.y);
2269
+ overlay.style.left = `${left}px`;
2270
+ overlay.style.top = `${top}px`;
2271
+ overlay.style.width = `${width}px`;
2272
+ overlay.style.height = `${height}px`;
2273
+ const viewport = store.getViewport();
2274
+ const topLeft = screenToWorld({ x: left, y: top }, viewport);
2275
+ const bottomRight = screenToWorld({ x: left + width, y: top + height }, viewport);
2276
+ const worldRect = {
2277
+ x: topLeft.x,
2278
+ y: topLeft.y,
2279
+ width: bottomRight.x - topLeft.x,
2280
+ height: bottomRight.y - topLeft.y
2281
+ };
2282
+ const ids = [];
2283
+ for (const [id, node] of store.nodes) {
2284
+ if (node.selectable === false) continue;
2285
+ if (rectsIntersect(worldRect, store.getNodeRect(id))) ids.push(id);
2286
+ }
2287
+ store.selectNodes(additive ? [.../* @__PURE__ */ new Set([...base, ...ids])] : ids);
2288
+ };
2289
+ return {
2290
+ isActive: () => active,
2291
+ start(event, px, py, isAdditive) {
2292
+ active = true;
2293
+ additive = isAdditive;
2294
+ base = isAdditive ? store.getSelectedNodes() : [];
2295
+ start = { x: px, y: py };
2296
+ overlay.style.display = "block";
2297
+ apply(px, py);
2298
+ pane.setPointerCapture(event.pointerId);
2299
+ },
2300
+ move(px, py) {
2301
+ if (!active) return;
2302
+ apply(px, py);
2303
+ },
2304
+ end(event) {
2305
+ if (!active) return;
2306
+ active = false;
2307
+ overlay.style.display = "none";
2308
+ pane.releasePointerCapture(event.pointerId);
2309
+ store.setSelectionBoxActive(store.getSelectedNodes().length > 0);
2310
+ },
2311
+ dispose() {
2312
+ overlay.remove();
2313
+ }
2314
+ };
2315
+ }
2316
+
2317
+ // src/input/pan-controller.ts
2318
+ var PAN_MOVE_THRESHOLD = 3;
2319
+ function createPanController(deps) {
2320
+ const { store, pane, schedule } = deps;
2321
+ let panning = false;
2322
+ let button = 0;
2323
+ let moved = false;
2324
+ let start = { x: 0, y: 0 };
2325
+ let origin = { x: 0, y: 0 };
2326
+ let suppressContextMenu = false;
2327
+ return {
2328
+ isPanning: () => panning,
2329
+ start(event) {
2330
+ panning = true;
2331
+ button = event.button;
2332
+ moved = false;
2333
+ const viewport = store.getViewport();
2334
+ start = { x: event.clientX, y: event.clientY };
2335
+ origin = { x: viewport.x, y: viewport.y };
2336
+ pane.setPointerCapture(event.pointerId);
2337
+ },
2338
+ move(event) {
2339
+ if (!panning) return;
2340
+ const dx = event.clientX - start.x;
2341
+ const dy = event.clientY - start.y;
2342
+ if (!moved && Math.hypot(dx, dy) > PAN_MOVE_THRESHOLD) {
2343
+ moved = true;
2344
+ if (button === 2) suppressContextMenu = true;
2345
+ }
2346
+ const viewport = store.getViewport();
2347
+ store.setViewport({ ...viewport, x: origin.x + dx, y: origin.y + dy });
2348
+ schedule();
2349
+ },
2350
+ end(event) {
2351
+ if (!panning) return;
2352
+ panning = false;
2353
+ pane.releasePointerCapture(event.pointerId);
2354
+ },
2355
+ suppressNextContextMenu() {
2356
+ if (!suppressContextMenu) return false;
2357
+ suppressContextMenu = false;
2358
+ return true;
2359
+ }
2360
+ };
2361
+ }
2362
+
2363
+ // src/input/resize-controller.ts
2364
+ function createResizeController(deps) {
2365
+ const { pane, bgCanvas, size, edgeRenderer, foregroundRenderer, schedule } = deps;
2366
+ const resize = () => {
2367
+ const rect = pane.getBoundingClientRect();
2368
+ size.width = rect.width;
2369
+ size.height = rect.height;
2370
+ size.dpr = window.devicePixelRatio || 1;
2371
+ bgCanvas.width = Math.max(1, Math.round(rect.width * size.dpr));
2372
+ bgCanvas.height = Math.max(1, Math.round(rect.height * size.dpr));
2373
+ bgCanvas.style.width = `${rect.width}px`;
2374
+ bgCanvas.style.height = `${rect.height}px`;
2375
+ edgeRenderer.resize(rect.width, rect.height, size.dpr);
2376
+ foregroundRenderer.resize(rect.width, rect.height, size.dpr);
2377
+ schedule();
2378
+ };
2379
+ const observer = new ResizeObserver(resize);
2380
+ observer.observe(pane);
2381
+ resize();
2382
+ return { dispose: () => observer.disconnect() };
2383
+ }
2384
+
2385
+ // src/input/zoom-controller.ts
2386
+ var WHEEL_LINE_PX = 16;
2387
+ var ZOOM_EASE = 0.22;
2388
+ function createZoomController(deps) {
2389
+ const { store, pane, size, minZoom, maxZoom, zoomSpeed, scrollToPan, schedule } = deps;
2390
+ let target = null;
2391
+ return {
2392
+ cancel() {
2393
+ target = null;
2394
+ },
2395
+ step() {
2396
+ if (!target) return;
2397
+ const vp = store.getViewport();
2398
+ const diff = target.zoom - vp.zoom;
2399
+ if (Math.abs(diff) < 5e-4) {
2400
+ store.setViewport(zoomAt(vp, target.anchor, target.zoom, minZoom, maxZoom));
2401
+ target = null;
2402
+ return;
2403
+ }
2404
+ store.setViewport(zoomAt(vp, target.anchor, vp.zoom + diff * ZOOM_EASE, minZoom, maxZoom));
2405
+ schedule();
2406
+ },
2407
+ onWheel(event) {
2408
+ event.preventDefault();
2409
+ const scale = event.deltaMode === 1 ? WHEEL_LINE_PX : event.deltaMode === 2 ? size.height : 1;
2410
+ const deltaX = event.deltaX * scale;
2411
+ const deltaY = event.deltaY * scale;
2412
+ if (scrollToPan && !event.ctrlKey) {
2413
+ target = null;
2414
+ const viewport = store.getViewport();
2415
+ store.setViewport({ ...viewport, x: viewport.x - deltaX, y: viewport.y - deltaY });
2416
+ schedule();
2417
+ return;
2418
+ }
2419
+ const rect = pane.getBoundingClientRect();
2420
+ const anchor = { x: event.clientX - rect.left, y: event.clientY - rect.top };
2421
+ const base = target?.zoom ?? store.getViewport().zoom;
2422
+ const nextZoom = clamp(base * Math.exp(-deltaY * zoomSpeed), minZoom, maxZoom);
2423
+ target = { zoom: nextZoom, anchor };
2424
+ schedule();
2425
+ }
2426
+ };
2427
+ }
2428
+
2429
+ // src/renderer/background.ts
2430
+ function drawDotGrid(ctx, width, height, viewport, options) {
2431
+ const gap = options?.gap ?? 24;
2432
+ const radius = options?.radius ?? 1;
2433
+ const color = options?.color ?? "rgba(100, 116, 139, 0.35)";
2434
+ const step = gap * viewport.zoom;
2435
+ if (step < 8) return;
2436
+ const offsetX = (viewport.x % step + step) % step;
2437
+ const offsetY = (viewport.y % step + step) % step;
2438
+ ctx.fillStyle = color;
2439
+ ctx.beginPath();
2440
+ for (let x = offsetX; x < width; x += step) {
2441
+ for (let y = offsetY; y < height; y += step) {
2442
+ ctx.moveTo(x + radius, y);
2443
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
2444
+ }
2445
+ }
2446
+ ctx.fill();
2447
+ }
2448
+
2449
+ // src/renderer/edge-paths.ts
2450
+ var SIDE_DIR = {
2451
+ left: { x: -1, y: 0 },
2452
+ right: { x: 1, y: 0 },
2453
+ top: { x: 0, y: -1 },
2454
+ bottom: { x: 0, y: 1 }
2455
+ };
2456
+ var BEZIER_SEGMENT_PX = 8;
2457
+ var BEZIER_MIN_SAMPLES = 16;
2458
+ var BEZIER_MAX_SAMPLES = 160;
2459
+ var CORNER_RADIUS = 8;
2460
+ var ARC_SAMPLES = 6;
2461
+ function cubicAt(a, b, c, d, t) {
2462
+ const mt = 1 - t;
2463
+ return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
2464
+ }
2465
+ function quadAt(a, b, c, t) {
2466
+ const mt = 1 - t;
2467
+ return mt * mt * a + 2 * mt * t * b + t * t * c;
2468
+ }
2469
+ var getStraightPath = ({ sourceX, sourceY, targetX, targetY }) => ({
2470
+ points: [
2471
+ { x: sourceX, y: sourceY },
2472
+ { x: targetX, y: targetY }
2473
+ ],
2474
+ labelX: (sourceX + targetX) / 2,
2475
+ labelY: (sourceY + targetY) / 2
2476
+ });
2477
+ var getBezierPath = (params) => {
2478
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2479
+ const sDir = SIDE_DIR[sourcePosition];
2480
+ const tDir = SIDE_DIR[targetPosition];
2481
+ const dist = Math.hypot(targetX - sourceX, targetY - sourceY);
2482
+ const reach = Math.max(40, dist * 0.4);
2483
+ const p0 = { x: sourceX, y: sourceY };
2484
+ const p1 = { x: sourceX + sDir.x * reach, y: sourceY + sDir.y * reach };
2485
+ const p2 = { x: targetX + tDir.x * reach, y: targetY + tDir.y * reach };
2486
+ const p3 = { x: targetX, y: targetY };
2487
+ const ctrlLen = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y) + Math.hypot(p3.x - p2.x, p3.y - p2.y);
2488
+ const samples = Math.max(
2489
+ BEZIER_MIN_SAMPLES,
2490
+ Math.min(BEZIER_MAX_SAMPLES, Math.ceil(ctrlLen / BEZIER_SEGMENT_PX))
2491
+ );
2492
+ const points = [];
2493
+ for (let i = 0; i <= samples; i++) {
2494
+ const t = i / samples;
2495
+ points.push({
2496
+ x: cubicAt(p0.x, p1.x, p2.x, p3.x, t),
2497
+ y: cubicAt(p0.y, p1.y, p2.y, p3.y, t)
2498
+ });
2499
+ }
2500
+ return {
2501
+ points,
2502
+ labelX: cubicAt(p0.x, p1.x, p2.x, p3.x, 0.5),
2503
+ labelY: cubicAt(p0.y, p1.y, p2.y, p3.y, 0.5)
2504
+ };
2505
+ };
2506
+ var STEP_OFFSET = 20;
2507
+ function simplifyCorners(corners) {
2508
+ const out = [];
2509
+ for (const p of corners) {
2510
+ const a = out[out.length - 2];
2511
+ const b = out[out.length - 1];
2512
+ if (b && b.x === p.x && b.y === p.y) continue;
2513
+ if (a && b && (a.x === b.x && b.x === p.x || a.y === b.y && b.y === p.y)) out.pop();
2514
+ out.push(p);
2515
+ }
2516
+ return out;
2517
+ }
2518
+ function stepCorners(params) {
2519
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = params;
2520
+ const s = { x: sourceX, y: sourceY };
2521
+ const t = { x: targetX, y: targetY };
2522
+ const sDir = SIDE_DIR[sourcePosition];
2523
+ const tDir = SIDE_DIR[targetPosition];
2524
+ const sg = { x: sourceX + sDir.x * STEP_OFFSET, y: sourceY + sDir.y * STEP_OFFSET };
2525
+ const tg = { x: targetX + tDir.x * STEP_OFFSET, y: targetY + tDir.y * STEP_OFFSET };
2526
+ return simplifyCorners(routeCorners(s, t, sg, tg, sDir, tDir));
2527
+ }
2528
+ function routeCorners(s, t, sg, tg, sDir, tDir) {
2529
+ const sHoriz = sDir.x !== 0;
2530
+ const tHoriz = tDir.x !== 0;
2531
+ if (sHoriz && tHoriz) {
2532
+ if (sDir.x * tDir.x < 0) {
2533
+ if ((t.x - s.x) * sDir.x >= 2 * STEP_OFFSET) {
2534
+ const midX = (s.x + t.x) / 2;
2535
+ return [s, { x: midX, y: s.y }, { x: midX, y: t.y }, t];
2536
+ }
2537
+ const midY = (s.y + t.y) / 2;
2538
+ return [s, sg, { x: sg.x, y: midY }, { x: tg.x, y: midY }, tg, t];
2539
+ }
2540
+ const railX = sDir.x > 0 ? Math.max(sg.x, tg.x) : Math.min(sg.x, tg.x);
2541
+ return [s, { x: railX, y: s.y }, { x: railX, y: t.y }, t];
2542
+ }
2543
+ if (!sHoriz && !tHoriz) {
2544
+ if (sDir.y * tDir.y < 0) {
2545
+ if ((t.y - s.y) * sDir.y >= 2 * STEP_OFFSET) {
2546
+ const midY = (s.y + t.y) / 2;
2547
+ return [s, { x: s.x, y: midY }, { x: t.x, y: midY }, t];
2548
+ }
2549
+ const midX = (s.x + t.x) / 2;
2550
+ return [s, sg, { x: midX, y: sg.y }, { x: midX, y: tg.y }, tg, t];
2551
+ }
2552
+ const railY = sDir.y > 0 ? Math.max(sg.y, tg.y) : Math.min(sg.y, tg.y);
2553
+ return [s, { x: s.x, y: railY }, { x: t.x, y: railY }, t];
2554
+ }
2555
+ const corner = sHoriz ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
2556
+ const exitsSource = sHoriz ? (corner.x - s.x) * sDir.x >= STEP_OFFSET : (corner.y - s.y) * sDir.y >= STEP_OFFSET;
2557
+ const exitsTarget = tHoriz ? (corner.x - t.x) * tDir.x >= STEP_OFFSET : (corner.y - t.y) * tDir.y >= STEP_OFFSET;
2558
+ if (exitsSource && exitsTarget) return [s, corner, t];
2559
+ const bend = sHoriz ? { x: sg.x, y: tg.y } : { x: tg.x, y: sg.y };
2560
+ return [s, sg, bend, tg, t];
2561
+ }
2562
+ function roundCorners(corners, radius) {
2563
+ if (corners.length <= 2) return corners;
2564
+ const points = [];
2565
+ const first = corners[0];
2566
+ if (first) points.push(first);
2567
+ for (let i = 1; i < corners.length - 1; i++) {
2568
+ const prev = corners[i - 1];
2569
+ const curr = corners[i];
2570
+ const next = corners[i + 1];
2571
+ if (!prev || !curr || !next) continue;
2572
+ const inLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
2573
+ const outLen = Math.hypot(next.x - curr.x, next.y - curr.y);
2574
+ const r = Math.min(radius, inLen / 2, outLen / 2);
2575
+ const inPt = {
2576
+ x: curr.x + (prev.x - curr.x) / (inLen || 1) * r,
2577
+ y: curr.y + (prev.y - curr.y) / (inLen || 1) * r
2578
+ };
2579
+ const outPt = {
2580
+ x: curr.x + (next.x - curr.x) / (outLen || 1) * r,
2581
+ y: curr.y + (next.y - curr.y) / (outLen || 1) * r
2582
+ };
2583
+ points.push(inPt);
2584
+ for (let j = 1; j < ARC_SAMPLES; j++) {
2585
+ const tt = j / ARC_SAMPLES;
2586
+ points.push({
2587
+ x: quadAt(inPt.x, curr.x, outPt.x, tt),
2588
+ y: quadAt(inPt.y, curr.y, outPt.y, tt)
2589
+ });
2590
+ }
2591
+ points.push(outPt);
2592
+ }
2593
+ const last = corners[corners.length - 1];
2594
+ if (last) points.push(last);
2595
+ return points;
2596
+ }
2597
+ function stepLabel(corners, params) {
2598
+ const i = Math.floor((corners.length - 1) / 2);
2599
+ const a = corners[i];
2600
+ const b = corners[i + 1];
2601
+ if (!a || !b) return { x: params.sourceX, y: params.sourceY };
2602
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2603
+ }
2604
+ var getSmoothStepPath = (params) => {
2605
+ const corners = stepCorners(params);
2606
+ const label = stepLabel(corners, params);
2607
+ return { points: roundCorners(corners, CORNER_RADIUS), labelX: label.x, labelY: label.y };
2608
+ };
2609
+ var getStepPath = (params) => {
2610
+ const corners = stepCorners(params);
2611
+ const label = stepLabel(corners, params);
2612
+ return { points: corners, labelX: label.x, labelY: label.y };
2613
+ };
2614
+ var BUILTIN_EDGE_TYPES = {
2615
+ bezier: getBezierPath,
2616
+ smoothstep: getSmoothStepPath,
2617
+ step: getStepPath,
2618
+ straight: getStraightPath
2619
+ };
2620
+
2621
+ // src/renderer/edge-geometry.ts
2622
+ var EDGE_COLOR = "rgba(100, 116, 139, 0.75)";
2623
+ var EDGE_COLOR_SELECTED = "#6366f1";
2624
+ var EDGE_COLOR_HOVERED = "rgba(99, 102, 241, 0.9)";
2625
+ var EDGE_WIDTH = 1.5;
2626
+ var EDGE_WIDTH_HOVERED = 2.5;
2627
+ var PENDING_COLOR = "#6366f1";
2628
+ var OPPOSITE = {
2629
+ left: "right",
2630
+ right: "left",
2631
+ top: "bottom",
2632
+ bottom: "top"
2633
+ };
2634
+ var falseToNull = (m) => m || null;
2635
+ function buildEdges(edges, nodes, viewport, edgeTypes, defaults, extras) {
2636
+ const handles = extras?.handles;
2637
+ const hovered = extras?.hovered;
2638
+ const reconnecting = extras?.reconnecting;
2639
+ const worldPositions = extras?.worldPositions;
2640
+ const resolved = [];
2641
+ for (const edge of edges) {
2642
+ if (edge.id === reconnecting) continue;
2643
+ const source = nodes.get(edge.source);
2644
+ const target = nodes.get(edge.target);
2645
+ if (!source || !target) continue;
2646
+ const frame = { handles, viewport, worldPositions, extras };
2647
+ const start = resolveEnd(edge, "source", source, target, frame);
2648
+ const end = resolveEnd(edge, "target", target, source, frame);
2649
+ const params = {
2650
+ sourceX: start.point.x,
2651
+ sourceY: start.point.y,
2652
+ targetX: end.point.x,
2653
+ targetY: end.point.y,
2654
+ sourcePosition: start.position,
2655
+ targetPosition: end.position
2656
+ };
2657
+ const pathFn = edgeTypes[edge.type ?? defaults.type ?? "bezier"] ?? getBezierPath;
2658
+ const path = pathFn(params);
2659
+ if (path.points.length < 2) continue;
2660
+ const style = { ...defaults.style, ...edge.style };
2661
+ const isHovered = edge.id === hovered;
2662
+ resolved.push({
2663
+ id: edge.id,
2664
+ points: path.points,
2665
+ source: start.point,
2666
+ target: end.point,
2667
+ label: edge.label !== void 0 ? { x: path.labelX, y: path.labelY } : void 0,
2668
+ stroke: edge.selected ? EDGE_COLOR_SELECTED : isHovered ? EDGE_COLOR_HOVERED : style.stroke ?? EDGE_COLOR,
2669
+ width: style.strokeWidth ?? (isHovered ? EDGE_WIDTH_HOVERED : EDGE_WIDTH),
2670
+ opacity: style.opacity ?? 1,
2671
+ animated: edge.animated ?? defaults.animated ?? false,
2672
+ dash: style.strokeDasharray,
2673
+ markerStart: falseToNull(edge.markerStart ?? defaults.markerStart),
2674
+ markerEnd: falseToNull(edge.markerEnd ?? defaults.markerEnd)
2675
+ });
2676
+ }
2677
+ return resolved;
2678
+ }
2679
+ function buildPendingPath(pending, handles, viewport) {
2680
+ const record = handles?.get(handleKey(pending.source, pending.sourceHandle));
2681
+ const sourcePosition = record?.position ?? (pending.sourceType === "target" ? "left" : "right");
2682
+ const from = worldToScreen(pending.from, viewport);
2683
+ const to = worldToScreen(pending.to, viewport);
2684
+ const targetPosition = pending.toPosition ?? OPPOSITE[sourcePosition];
2685
+ return getBezierPath({
2686
+ sourceX: from.x,
2687
+ sourceY: from.y,
2688
+ targetX: to.x,
2689
+ targetY: to.y,
2690
+ sourcePosition,
2691
+ targetPosition
2692
+ }).points;
2693
+ }
2694
+ function pointOnSide(rect, side, alongPct = 50) {
2695
+ const t = alongPct / 100;
2696
+ switch (side) {
2697
+ case "left":
2698
+ return { x: rect.x, y: rect.y + rect.height * t };
2699
+ case "right":
2700
+ return { x: rect.x + rect.width, y: rect.y + rect.height * t };
2701
+ case "top":
2702
+ return { x: rect.x + rect.width * t, y: rect.y };
2703
+ case "bottom":
2704
+ return { x: rect.x + rect.width * t, y: rect.y + rect.height };
2705
+ }
2706
+ }
2707
+ function nodeRect(node, worldPositions) {
2708
+ const origin = worldPositions?.get(node.id) ?? node.position;
2709
+ const size = node.size ?? DEFAULT_NODE_SIZE;
2710
+ return { x: origin.x, y: origin.y, width: size.width, height: size.height };
2711
+ }
2712
+ var WANTED_TYPES = {
2713
+ source: ["source", "either"],
2714
+ target: ["target", "either"]
2715
+ };
2716
+ function handleOnSide(nodeId, side, role, handles) {
2717
+ if (!handles) return null;
2718
+ for (const record of handles.values()) {
2719
+ if (record.nodeId === nodeId && record.position === side && WANTED_TYPES[role].includes(record.type)) {
2720
+ return record;
2721
+ }
2722
+ }
2723
+ return null;
2724
+ }
2725
+ function resolveEnd(edge, role, node, peer, frame) {
2726
+ const { handles, viewport, worldPositions, extras } = frame;
2727
+ const handleId = role === "source" ? edge.sourceHandle : edge.targetHandle;
2728
+ const rect = nodeRect(node, worldPositions);
2729
+ const anchorId = parseAnchorAutoHandleId(handleId) ?? parseAnchorHandleId(handleId)?.anchorId ?? null;
2730
+ const endAt = (side2) => {
2731
+ const alongPct = (anchorId ? extras?.anchorPlacement?.(node.id, anchorId, side2) : null) ?? void 0;
2732
+ if (alongPct === void 0 && !anchorId) {
2733
+ const candidate = handleOnSide(node.id, side2, role, handles);
2734
+ if (candidate) {
2735
+ return {
2736
+ point: worldToScreen(
2737
+ { x: rect.x + candidate.offset.x, y: rect.y + candidate.offset.y },
2738
+ viewport
2739
+ ),
2740
+ position: side2
2741
+ };
2742
+ }
2743
+ }
2744
+ return { point: worldToScreen(pointOnSide(rect, side2, alongPct), viewport), position: side2 };
2745
+ };
2746
+ const registered = handleId && handles ? handles.get(handleKey(node.id, handleId)) : void 0;
2747
+ const pinnedSide = parseAnchorHandleId(handleId)?.side ?? registered?.position ?? parseSideHandleId(handleId);
2748
+ const live = extras?.liveAlignNodes !== void 0 && (extras.liveAlignNodes.has(edge.source) || extras.liveAlignNodes.has(edge.target)) && edge.source !== edge.target;
2749
+ if (pinnedSide && !live) {
2750
+ if (registered) {
2751
+ return {
2752
+ point: worldToScreen(
2753
+ { x: rect.x + registered.offset.x, y: rect.y + registered.offset.y },
2754
+ viewport
2755
+ ),
2756
+ position: registered.position
2757
+ };
2758
+ }
2759
+ return endAt(pinnedSide);
2760
+ }
2761
+ const key = displayedSideKey(edge.id, role);
2762
+ const side = resolveFacingSide(
2763
+ rect,
2764
+ nodeRect(peer, worldPositions),
2765
+ extras?.displayedSides?.get(key)
2766
+ );
2767
+ extras?.displayedSides?.set(key, side);
2768
+ return endAt(side);
2769
+ }
2770
+ function distanceToPolyline(point, points) {
2771
+ let best = Number.POSITIVE_INFINITY;
2772
+ for (let i = 0; i < points.length - 1; i++) {
2773
+ const a = points[i];
2774
+ const b = points[i + 1];
2775
+ if (!a || !b) continue;
2776
+ best = Math.min(best, distanceToSegment(point, a, b));
2777
+ }
2778
+ return best;
2779
+ }
2780
+ function distanceToSegment(p, a, b) {
2781
+ const dx = b.x - a.x;
2782
+ const dy = b.y - a.y;
2783
+ const lenSq = dx * dx + dy * dy;
2784
+ if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
2785
+ let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
2786
+ t = Math.max(0, Math.min(1, t));
2787
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
2788
+ }
2789
+
2790
+ // src/renderer/swimlane.ts
2791
+ var DEFAULTS = {
2792
+ separator: "rgba(100, 116, 139, 0.28)",
2793
+ tintAlpha: 0.06
2794
+ };
2795
+ function drawSwimlanes(ctx, width, height, viewport, lanes, style) {
2796
+ if (lanes.length === 0) return;
2797
+ const separator = DEFAULTS.separator;
2798
+ const tintAlpha = DEFAULTS.tintAlpha;
2799
+ for (const lane of lanes) {
2800
+ if (!lane.color) continue;
2801
+ const top = viewport.y + lane.top * viewport.zoom;
2802
+ const bottom = viewport.y + (lane.top + lane.height) * viewport.zoom;
2803
+ if (bottom < 0 || top > height) continue;
2804
+ ctx.fillStyle = withAlpha(lane.color, tintAlpha);
2805
+ ctx.fillRect(0, top, width, bottom - top);
2806
+ }
2807
+ ctx.strokeStyle = separator;
2808
+ ctx.lineWidth = 1;
2809
+ ctx.beginPath();
2810
+ for (const lane of lanes) {
2811
+ const bottom = viewport.y + (lane.top + lane.height) * viewport.zoom;
2812
+ if (bottom < 0 || bottom > height) continue;
2813
+ ctx.moveTo(0, bottom - 0.5);
2814
+ ctx.lineTo(width, bottom - 0.5);
2815
+ }
2816
+ ctx.stroke();
2817
+ }
2818
+
2819
+ // src/hooks/use-rivet-runtime.ts
2820
+ var CULL_MARGIN_PX = 240;
2821
+ var ZOOM_STEP = 1.2;
2822
+ var WILL_CHANGE_IDLE_MS = 180;
2823
+ var GUIDE_COLOR = "rgba(236, 72, 153, 0.9)";
2824
+ function drawAlignmentGuides(ctx, guides, viewport, size) {
2825
+ if (guides.length === 0) return;
2826
+ ctx.save();
2827
+ ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
2828
+ ctx.strokeStyle = GUIDE_COLOR;
2829
+ ctx.lineWidth = 1;
2830
+ ctx.beginPath();
2831
+ for (const guide of guides) {
2832
+ if (guide.axis === "x") {
2833
+ const a = worldToScreen({ x: guide.position, y: guide.start }, viewport);
2834
+ const b = worldToScreen({ x: guide.position, y: guide.end }, viewport);
2835
+ ctx.moveTo(Math.round(a.x) + 0.5, a.y);
2836
+ ctx.lineTo(Math.round(b.x) + 0.5, b.y);
2837
+ } else {
2838
+ const a = worldToScreen({ x: guide.start, y: guide.position }, viewport);
2839
+ const b = worldToScreen({ x: guide.end, y: guide.position }, viewport);
2840
+ ctx.moveTo(a.x, Math.round(a.y) + 0.5);
2841
+ ctx.lineTo(b.x, Math.round(b.y) + 0.5);
2842
+ }
2843
+ }
2844
+ ctx.stroke();
2845
+ ctx.restore();
2846
+ }
2847
+ var ENDPOINT_BUBBLE_RADIUS = 6;
2848
+ var HOVER_KEEP_TOLERANCE = 16;
2849
+ function drawEndpointBubbles(ctx, ends, size) {
2850
+ ctx.save();
2851
+ ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
2852
+ ctx.fillStyle = PENDING_COLOR;
2853
+ ctx.strokeStyle = "#ffffff";
2854
+ ctx.lineWidth = 1.5;
2855
+ for (const point of [ends.source, ends.target]) {
2856
+ ctx.beginPath();
2857
+ ctx.arc(point.x, point.y, ENDPOINT_BUBBLE_RADIUS, 0, Math.PI * 2);
2858
+ ctx.fill();
2859
+ ctx.stroke();
2860
+ }
2861
+ ctx.restore();
2862
+ }
2863
+ function useRivetRuntime(params) {
2864
+ const { store, paneRef, nodeContainerRef } = params;
2865
+ const { backgroundCanvasRef, edgeCanvasRef, foregroundCanvasRef } = params;
2866
+ const { swimlaneLanes, scrollToPan, zoomSpeed } = params;
2867
+ const { minZoom, maxZoom, gridGap, edgeTypes, defaultEdgeOptions, anchorOptions } = params;
2868
+ const { edgeRenderer: createEdgeRenderer } = params;
2869
+ const { panButtons, selectionOnDrag, selectionKeys, multiSelectionKeys } = params;
2870
+ const [visibleIds, setVisibleIds] = useState([]);
2871
+ const reconnectRef = useRef(params);
2872
+ reconnectRef.current = params;
2873
+ const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
2874
+ const visibleKeyRef = useRef("");
2875
+ const swimlaneLanesRef = useRef(swimlaneLanes);
2876
+ useEffect(() => {
2877
+ swimlaneLanesRef.current = swimlaneLanes;
2878
+ store.requestRender();
2879
+ }, [store, swimlaneLanes]);
2880
+ const controls = useMemo(
2881
+ () => buildControls({ store, sizeRef, minZoom, maxZoom }),
2882
+ [store, minZoom, maxZoom]
2883
+ );
2884
+ const getViewportElements = useMemo(
2885
+ () => buildViewportQuery({ store, sizeRef, lanesRef: swimlaneLanesRef }),
2886
+ [store]
2887
+ );
2888
+ useEffect(() => {
2889
+ const pane = paneRef.current;
2890
+ const bgCanvas = backgroundCanvasRef.current;
2891
+ const edgeCanvas = edgeCanvasRef.current;
2892
+ const foregroundCanvas = foregroundCanvasRef.current;
2893
+ const nodeContainer = nodeContainerRef.current;
2894
+ if (!pane || !bgCanvas || !edgeCanvas || !foregroundCanvas || !nodeContainer) return;
2895
+ const bgCtx = bgCanvas.getContext("2d");
2896
+ if (!bgCtx) return;
2897
+ const rendererOptions = { edgeTypes, defaultEdgeOptions };
2898
+ const edgeRenderer = createEdgeRenderer(edgeCanvas, rendererOptions);
2899
+ const foregroundRenderer = createEdgeRenderer(foregroundCanvas, rendererOptions);
2900
+ const fgCtx = foregroundCanvas.getContext("2d");
2901
+ const size = sizeRef.current;
2902
+ let frame = 0;
2903
+ let dirty = true;
2904
+ let hoveredEdgeId = null;
2905
+ const anchorPlacement = (nodeId, anchorId, side) => {
2906
+ const record = store.anchors.get(handleKey(nodeId, anchorId));
2907
+ return record?.geometry ? strayPlacementPct(record.geometry, side, anchorOptions) : null;
2908
+ };
2909
+ const layerHint = createLayerHint(nodeContainer, WILL_CHANGE_IDLE_MS);
2910
+ const zoom = createZoomController({
2911
+ store,
2912
+ pane,
2913
+ size,
2914
+ minZoom,
2915
+ maxZoom,
2916
+ zoomSpeed,
2917
+ scrollToPan,
2918
+ schedule: () => schedule()
2919
+ });
2920
+ const render = () => {
2921
+ zoom.step();
2922
+ const viewport = store.getViewport();
2923
+ layerHint.apply(viewportToCss(viewport));
2924
+ bgCtx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
2925
+ bgCtx.clearRect(0, 0, size.width, size.height);
2926
+ drawDotGrid(bgCtx, size.width, size.height, viewport, { gap: gridGap });
2927
+ drawSwimlanes(bgCtx, size.width, size.height, viewport, swimlaneLanesRef.current);
2928
+ const worldPositions = store.getWorldPositions();
2929
+ const edgeList = [...store.edges.values()];
2930
+ const pending = store.getPending();
2931
+ const liveAlignNodes = store.getEdgeAlignment() === "live" && store.isNodeDragging() ? store.getDraggingNodeIds() : void 0;
2932
+ edgeRenderer.draw(edgeList, store.nodes, viewport, {
2933
+ handles: store.handles,
2934
+ pending: null,
2935
+ hovered: hoveredEdgeId,
2936
+ reconnecting: pending?.reconnecting ?? null,
2937
+ worldPositions,
2938
+ displayedSides: store.getDisplayedSides(),
2939
+ liveAlignNodes,
2940
+ anchorPlacement
2941
+ });
2942
+ store.setEdgeLabelAnchors(edgeRenderer.getLabels());
2943
+ foregroundRenderer.draw([], store.nodes, viewport, {
2944
+ handles: store.handles,
2945
+ pending
2946
+ });
2947
+ if (fgCtx) drawAlignmentGuides(fgCtx, store.getAlignmentGuides(), viewport, size);
2948
+ if (fgCtx) {
2949
+ const affordanceId = hoveredEdgeId ?? store.getSelectedEdge();
2950
+ const edge = affordanceId ? store.edges.get(affordanceId) : void 0;
2951
+ const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
2952
+ if (edge && reconnectable && edge.id !== pending?.reconnecting) {
2953
+ const ends = edgeRenderer.getEndpoints?.().get(edge.id);
2954
+ if (ends) drawEndpointBubbles(fgCtx, ends, size);
2955
+ }
2956
+ }
2957
+ if (edgeList.some((edge) => edge.animated ?? defaultEdgeOptions?.animated)) schedule();
2958
+ const rect = visibleWorldRect(viewport, size.width, size.height);
2959
+ const margin = CULL_MARGIN_PX / viewport.zoom;
2960
+ const view = {
2961
+ x: rect.x - margin,
2962
+ y: rect.y - margin,
2963
+ width: rect.width + margin * 2,
2964
+ height: rect.height + margin * 2
2965
+ };
2966
+ const ids = [];
2967
+ for (const node of store.nodes.values()) {
2968
+ const nodeSize = node.size ?? DEFAULT_NODE_SIZE;
2969
+ const origin = worldPositions.get(node.id) ?? node.position;
2970
+ if (!node.size || rectsIntersect(view, {
2971
+ x: origin.x,
2972
+ y: origin.y,
2973
+ width: nodeSize.width,
2974
+ height: nodeSize.height
2975
+ })) {
2976
+ ids.push(node.id);
2977
+ }
2978
+ }
2979
+ const key = ids.join("|");
2980
+ if (key !== visibleKeyRef.current) {
2981
+ visibleKeyRef.current = key;
2982
+ setVisibleIds(ids);
2983
+ }
2984
+ store.notifyFrame();
2985
+ };
2986
+ const tick = () => {
2987
+ frame = 0;
2988
+ if (!dirty) return;
2989
+ dirty = false;
2990
+ render();
2991
+ };
2992
+ const schedule = () => {
2993
+ dirty = true;
2994
+ if (frame) return;
2995
+ frame = requestAnimationFrame(tick);
2996
+ };
2997
+ store.bindRenderRequester(schedule);
2998
+ const setEdgeHover = (id) => {
2999
+ if (hoveredEdgeId === id) return;
3000
+ hoveredEdgeId = id;
3001
+ schedule();
3002
+ };
3003
+ const pan = createPanController({ store, pane, schedule });
3004
+ const marquee = createMarqueeController({ store, pane });
3005
+ const pressedKeys = /* @__PURE__ */ new Set();
3006
+ const selectionKeyHeld = (event) => keyHeld(event, selectionKeys, pressedKeys);
3007
+ const multiSelectHeld = (event) => keyHeld(event, multiSelectionKeys, pressedKeys);
3008
+ const startMarquee = (event, px, py) => {
3009
+ setEdgeHover(null);
3010
+ marquee.start(event, px, py, multiSelectHeld(event));
3011
+ };
3012
+ const endpointWorld = (edge, nodeId, handleId, role) => {
3013
+ if (handleId) {
3014
+ const record = store.handles.get(handleKey(nodeId, handleId));
3015
+ if (record) {
3016
+ const wp = handleWorldPosition(store, { nodeId, handleId, type: record.type });
3017
+ if (wp) return wp;
3018
+ }
3019
+ }
3020
+ const side = parseAnchorHandleId(handleId)?.side ?? parseSideHandleId(handleId) ?? store.getDisplayedSides().get(displayedSideKey(edge.id, role));
3021
+ if (side) {
3022
+ const anchorId = parseAnchorAutoHandleId(handleId) ?? parseAnchorHandleId(handleId)?.anchorId;
3023
+ const pct = anchorId ? anchorPlacement(nodeId, anchorId, side) ?? void 0 : void 0;
3024
+ return pointOnSide(store.getNodeRect(nodeId), side, pct);
3025
+ }
3026
+ const node = store.nodes.get(nodeId);
3027
+ const size2 = node?.size ?? DEFAULT_NODE_SIZE;
3028
+ const base = store.getNodeWorldPosition(nodeId);
3029
+ const x = role === "source" ? base.x + size2.width : base.x;
3030
+ return { x, y: base.y + size2.height / 2 };
3031
+ };
3032
+ const beginReconnect = (edge, end) => {
3033
+ zoom.cancel();
3034
+ const fixedIsSource = end === "target";
3035
+ const fixedNodeId = fixedIsSource ? edge.source : edge.target;
3036
+ const fixedHandleId = fixedIsSource ? edge.sourceHandle : edge.targetHandle;
3037
+ const from = {
3038
+ nodeId: fixedNodeId,
3039
+ handleId: fixedHandleId ?? "",
3040
+ type: fixedIsSource ? "source" : "target"
3041
+ };
3042
+ const fixedWorld = endpointWorld(
3043
+ edge,
3044
+ fixedNodeId,
3045
+ fixedHandleId,
3046
+ fixedIsSource ? "source" : "target"
3047
+ );
3048
+ const looseWorld = endpointWorld(
3049
+ edge,
3050
+ fixedIsSource ? edge.target : edge.source,
3051
+ fixedIsSource ? edge.targetHandle : edge.sourceHandle,
3052
+ fixedIsSource ? "target" : "source"
3053
+ );
3054
+ startConnectionDrag({
3055
+ store,
3056
+ paneRef,
3057
+ from,
3058
+ begin: {
3059
+ source: from.nodeId,
3060
+ sourceHandle: from.handleId,
3061
+ sourceType: from.type,
3062
+ from: fixedWorld,
3063
+ to: looseWorld,
3064
+ reconnecting: edge.id
3065
+ },
3066
+ // Callbacks read `reconnectRef.current` when the drag ends, so a late
3067
+ // prop change (this effect never re-runs on callback identity) is honored.
3068
+ validate: (connection) => reconnectRef.current.isValidConnection?.(connection) ?? true,
3069
+ map: (connection) => {
3070
+ const mapFn = reconnectRef.current.mapConnection;
3071
+ return mapFn ? mapFn(connection) : connection;
3072
+ },
3073
+ commit: (connection) => {
3074
+ store.reconnectEdge(edge.id, connection);
3075
+ reconnectRef.current.onReconnect?.(edge, connection);
3076
+ },
3077
+ onEnd: () => reconnectRef.current.onReconnectEnd?.(edge)
3078
+ });
3079
+ reconnectRef.current.onReconnectStart?.(edge);
3080
+ };
3081
+ store.bindReconnectDelegate({
3082
+ pickEndpoint: (clientX, clientY) => {
3083
+ const rect = pane.getBoundingClientRect();
3084
+ const hit = edgeRenderer.pickEndpoint(clientX - rect.left, clientY - rect.top);
3085
+ if (!hit) return null;
3086
+ if (hit.edgeId !== hoveredEdgeId && hit.edgeId !== store.getSelectedEdge()) return null;
3087
+ const edge = store.edges.get(hit.edgeId);
3088
+ const reconnectable = edge ? edge.reconnectable ?? reconnectRef.current.edgesReconnectable : false;
3089
+ return reconnectable ? hit : null;
3090
+ },
3091
+ begin: (edgeId, end) => {
3092
+ const edge = store.edges.get(edgeId);
3093
+ if (edge) beginReconnect(edge, end);
3094
+ }
3095
+ });
3096
+ const onPointerDown = (event) => {
3097
+ if (store.getPending()) store.endConnection();
3098
+ if (event.target instanceof Element && event.target.closest(SELECTOR.paneGestureExempt)) {
3099
+ return;
3100
+ }
3101
+ const rect = pane.getBoundingClientRect();
3102
+ const px = event.clientX - rect.left;
3103
+ const py = event.clientY - rect.top;
3104
+ if (event.button !== 0) {
3105
+ if (panButtons.includes(event.button)) {
3106
+ event.preventDefault();
3107
+ zoom.cancel();
3108
+ pan.start(event);
3109
+ }
3110
+ return;
3111
+ }
3112
+ zoom.cancel();
3113
+ if (selectionKeyHeld(event)) {
3114
+ startMarquee(event, px, py);
3115
+ return;
3116
+ }
3117
+ const endpointHit = edgeRenderer.pickEndpoint(px, py);
3118
+ if (endpointHit) {
3119
+ const edge = store.edges.get(endpointHit.edgeId);
3120
+ const reconnectable = edge?.reconnectable ?? reconnectRef.current.edgesReconnectable;
3121
+ if (edge && reconnectable) {
3122
+ beginReconnect(edge, endpointHit.end);
3123
+ return;
3124
+ }
3125
+ }
3126
+ const hitEdge = edgeRenderer.pick(px, py);
3127
+ if (hitEdge && store.edges.get(hitEdge)?.selectable !== false) {
3128
+ store.selectEdge(hitEdge);
3129
+ return;
3130
+ }
3131
+ store.selectNode(null);
3132
+ if (selectionOnDrag) {
3133
+ startMarquee(event, px, py);
3134
+ return;
3135
+ }
3136
+ if (panButtons.includes(0)) pan.start(event);
3137
+ };
3138
+ const onPointerMove = (event) => {
3139
+ const rect = pane.getBoundingClientRect();
3140
+ const px = event.clientX - rect.left;
3141
+ const py = event.clientY - rect.top;
3142
+ if (marquee.isActive()) {
3143
+ marquee.move(px, py);
3144
+ return;
3145
+ }
3146
+ if (pan.isPanning()) {
3147
+ pan.move(event);
3148
+ return;
3149
+ }
3150
+ if (store.getPending()) return;
3151
+ const overNode = event.target instanceof Element && event.target.closest(`${SELECTOR.node}, ${SELECTOR.nodesSelection}`);
3152
+ let overEdge = overNode ? null : edgeRenderer.pick(px, py);
3153
+ if (!overEdge && hoveredEdgeId) {
3154
+ const near = edgeRenderer.pick(px, py, HOVER_KEEP_TOLERANCE) === hoveredEdgeId || edgeRenderer.pickEndpoint(px, py)?.edgeId === hoveredEdgeId;
3155
+ if (near) overEdge = hoveredEdgeId;
3156
+ }
3157
+ pane.style.cursor = overEdge ? "pointer" : "";
3158
+ setEdgeHover(overEdge);
3159
+ };
3160
+ const onPointerUp = (event) => {
3161
+ if (marquee.isActive()) {
3162
+ marquee.end(event);
3163
+ return;
3164
+ }
3165
+ pan.end(event);
3166
+ };
3167
+ const onKeyDown = createKeyboardHandler(store);
3168
+ let focusRetryFrame = 0;
3169
+ const unsubscribeFocus = store.subscribeFocus((id) => {
3170
+ if (focusRetryFrame) {
3171
+ cancelAnimationFrame(focusRetryFrame);
3172
+ focusRetryFrame = 0;
3173
+ }
3174
+ if (id === null) return;
3175
+ const el = store.getNodeElement(id);
3176
+ if (el) {
3177
+ if (document.activeElement !== el) el.focus();
3178
+ return;
3179
+ }
3180
+ controls.centerNode(id);
3181
+ let tries = 0;
3182
+ const attempt = () => {
3183
+ focusRetryFrame = 0;
3184
+ const mounted = store.getNodeElement(id);
3185
+ if (mounted) {
3186
+ mounted.focus();
3187
+ return;
3188
+ }
3189
+ if (++tries < 10) focusRetryFrame = requestAnimationFrame(attempt);
3190
+ };
3191
+ focusRetryFrame = requestAnimationFrame(attempt);
3192
+ });
3193
+ const onContextMenu = (event) => {
3194
+ if (!pan.suppressNextContextMenu()) return;
3195
+ event.preventDefault();
3196
+ event.stopPropagation();
3197
+ };
3198
+ const onTrackKeyDown = (event) => pressedKeys.add(event.key);
3199
+ const onTrackKeyUp = (event) => pressedKeys.delete(event.key);
3200
+ const clearPressedKeys = () => pressedKeys.clear();
3201
+ const resizer = createResizeController({
3202
+ pane,
3203
+ bgCanvas,
3204
+ size,
3205
+ edgeRenderer,
3206
+ foregroundRenderer,
3207
+ schedule
3208
+ });
3209
+ pane.addEventListener("wheel", zoom.onWheel, { passive: false });
3210
+ pane.addEventListener("pointerdown", onPointerDown);
3211
+ pane.addEventListener("pointermove", onPointerMove);
3212
+ pane.addEventListener("pointerup", onPointerUp);
3213
+ pane.addEventListener("pointercancel", onPointerUp);
3214
+ pane.addEventListener("contextmenu", onContextMenu, { capture: true });
3215
+ window.addEventListener("keydown", onKeyDown);
3216
+ window.addEventListener("keydown", onTrackKeyDown);
3217
+ window.addEventListener("keyup", onTrackKeyUp);
3218
+ window.addEventListener("blur", clearPressedKeys);
3219
+ return () => {
3220
+ resizer.dispose();
3221
+ store.bindRenderRequester(() => {
3222
+ });
3223
+ store.bindReconnectDelegate(null);
3224
+ unsubscribeFocus();
3225
+ if (focusRetryFrame) cancelAnimationFrame(focusRetryFrame);
3226
+ if (frame) cancelAnimationFrame(frame);
3227
+ layerHint.dispose();
3228
+ pane.removeEventListener("wheel", zoom.onWheel);
3229
+ pane.removeEventListener("pointerdown", onPointerDown);
3230
+ pane.removeEventListener("pointermove", onPointerMove);
3231
+ pane.removeEventListener("pointerup", onPointerUp);
3232
+ pane.removeEventListener("pointercancel", onPointerUp);
3233
+ pane.removeEventListener("contextmenu", onContextMenu, { capture: true });
3234
+ window.removeEventListener("keydown", onKeyDown);
3235
+ window.removeEventListener("keydown", onTrackKeyDown);
3236
+ window.removeEventListener("keyup", onTrackKeyUp);
3237
+ window.removeEventListener("blur", clearPressedKeys);
3238
+ marquee.dispose();
3239
+ edgeRenderer.dispose();
3240
+ foregroundRenderer.dispose();
3241
+ };
3242
+ }, [
3243
+ store,
3244
+ controls,
3245
+ paneRef,
3246
+ nodeContainerRef,
3247
+ backgroundCanvasRef,
3248
+ edgeCanvasRef,
3249
+ foregroundCanvasRef,
3250
+ scrollToPan,
3251
+ zoomSpeed,
3252
+ minZoom,
3253
+ maxZoom,
3254
+ gridGap,
3255
+ edgeTypes,
3256
+ defaultEdgeOptions,
3257
+ anchorOptions,
3258
+ createEdgeRenderer,
3259
+ panButtons,
3260
+ selectionOnDrag,
3261
+ selectionKeys,
3262
+ multiSelectionKeys
3263
+ ]);
3264
+ return { visibleIds, controls, getViewportElements };
3265
+ }
3266
+ function buildViewportQuery({
3267
+ store,
3268
+ sizeRef,
3269
+ lanesRef
3270
+ }) {
3271
+ return () => {
3272
+ const { width, height } = sizeRef.current;
3273
+ const rect = visibleWorldRect(store.getViewport(), width, height);
3274
+ const nodes = [];
3275
+ for (const node of store.nodes.values()) {
3276
+ if (rectsIntersect(store.getNodeRect(node.id), rect)) nodes.push(node);
3277
+ }
3278
+ const lanes = [];
3279
+ for (const lane of lanesRef.current) {
3280
+ const overlapH = Math.min(lane.top + lane.height, rect.y + rect.height) - Math.max(lane.top, rect.y);
3281
+ if (overlapH <= 0) continue;
3282
+ lanes.push({ ...lane, visibleArea: overlapH * rect.width });
3283
+ }
3284
+ lanes.sort((a, b) => b.visibleArea - a.visibleArea);
3285
+ return { rect, nodes, lanes };
3286
+ };
3287
+ }
3288
+ var sameViewport = (a, b) => a.x === b.x && a.y === b.y && a.zoom === b.zoom;
3289
+ var easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
3290
+ function buildControls({ store, sizeRef, minZoom, maxZoom }) {
3291
+ let animId = null;
3292
+ let written = null;
3293
+ const cancelAnim = () => {
3294
+ if (animId !== null) cancelAnimationFrame(animId);
3295
+ animId = null;
3296
+ written = null;
3297
+ };
3298
+ const commit = (viewport) => {
3299
+ store.setViewport(viewport);
3300
+ store.requestRender();
3301
+ };
3302
+ const applyViewport = (target, duration) => {
3303
+ cancelAnim();
3304
+ const from = store.getViewport();
3305
+ if (!duration || duration <= 0 || sameViewport(from, target)) {
3306
+ commit(target);
3307
+ return;
3308
+ }
3309
+ const start = performance.now();
3310
+ const step = (now) => {
3311
+ if (written && !sameViewport(store.getViewport(), written)) {
3312
+ animId = null;
3313
+ written = null;
3314
+ return;
3315
+ }
3316
+ const t = Math.min(1, (now - start) / duration);
3317
+ const e = easeInOutCubic(t);
3318
+ commit({
3319
+ x: from.x + (target.x - from.x) * e,
3320
+ y: from.y + (target.y - from.y) * e,
3321
+ zoom: from.zoom + (target.zoom - from.zoom) * e
3322
+ });
3323
+ written = store.getViewport();
3324
+ if (t < 1) {
3325
+ animId = requestAnimationFrame(step);
3326
+ } else {
3327
+ animId = null;
3328
+ written = null;
3329
+ }
3330
+ };
3331
+ animId = requestAnimationFrame(step);
3332
+ };
3333
+ const zoomToLevel = (nextZoom) => {
3334
+ cancelAnim();
3335
+ const { width, height } = sizeRef.current;
3336
+ const anchor = { x: width / 2, y: height / 2 };
3337
+ commit(zoomAt(store.getViewport(), anchor, nextZoom, minZoom, maxZoom));
3338
+ };
3339
+ const frameBounds = (rect, options) => {
3340
+ const { width, height } = sizeRef.current;
3341
+ if (width === 0 || height === 0) return;
3342
+ const padding = options?.padding ?? 0.1;
3343
+ const availWidth = width * (1 - padding * 2);
3344
+ const availHeight = height * (1 - padding * 2);
3345
+ const fitZoom = Math.min(
3346
+ rect.width > 0 ? availWidth / rect.width : maxZoom,
3347
+ rect.height > 0 ? availHeight / rect.height : maxZoom
3348
+ );
3349
+ const zoom = clamp(fitZoom, minZoom, options?.maxZoom ?? maxZoom);
3350
+ const centerX = rect.x + rect.width / 2;
3351
+ const centerY = rect.y + rect.height / 2;
3352
+ applyViewport(
3353
+ { zoom, x: width / 2 - centerX * zoom, y: height / 2 - centerY * zoom },
3354
+ options?.duration
3355
+ );
3356
+ };
3357
+ const boundsOfNodes = (ids) => boundingRect([...ids].map((id) => store.getNodeRect(id)));
3358
+ return {
3359
+ zoomIn: () => zoomToLevel(store.getViewport().zoom * ZOOM_STEP),
3360
+ zoomOut: () => zoomToLevel(store.getViewport().zoom / ZOOM_STEP),
3361
+ zoomTo: (zoom) => zoomToLevel(zoom),
3362
+ fitView: (options) => {
3363
+ const rect = boundsOfNodes(store.nodes.keys());
3364
+ if (rect) frameBounds(rect, options);
3365
+ },
3366
+ fitSelection: (options) => {
3367
+ const rect = boundsOfNodes(store.getSelectedNodes());
3368
+ if (rect) frameBounds(rect, options);
3369
+ },
3370
+ fitBounds: (rect, options) => frameBounds(rect, options),
3371
+ centerNode: (id, options) => {
3372
+ if (!store.nodes.has(id)) return;
3373
+ const { width, height } = sizeRef.current;
3374
+ if (width === 0 || height === 0) return;
3375
+ const box = store.getNodeRect(id);
3376
+ const zoom = clamp(options?.zoom ?? store.getViewport().zoom, minZoom, maxZoom);
3377
+ const centerX = box.x + box.width / 2;
3378
+ const centerY = box.y + box.height / 2;
3379
+ applyViewport(
3380
+ { zoom, x: width / 2 - centerX * zoom, y: height / 2 - centerY * zoom },
3381
+ options?.duration
3382
+ );
3383
+ },
3384
+ getViewport: () => store.getViewport(),
3385
+ setViewport: (viewport) => {
3386
+ cancelAnim();
3387
+ commit(viewport);
3388
+ }
3389
+ };
3390
+ }
3391
+ function useSwimlanes(params) {
3392
+ const { store, swimlane, swimlaneMargin, swimlaneResizable } = params;
3393
+ const { clampToSwimlane, swimlaneBottomReveal, onSwimlaneSizeChange } = params;
3394
+ const [laneHeights, setLaneHeights] = useState({});
3395
+ const resizableEnabled = (swimlaneResizable ?? true) && Boolean(swimlane?.length);
3396
+ const effectiveSwimlane = useMemo(() => {
3397
+ if (!swimlane) return void 0;
3398
+ if (Object.keys(laneHeights).length === 0) return swimlane;
3399
+ return swimlane.map((group) => ({
3400
+ ...group,
3401
+ lanes: group.lanes.map((lane) => ({
3402
+ ...lane,
3403
+ size: laneHeights[lane.id] ?? lane.size
3404
+ }))
3405
+ }));
3406
+ }, [swimlane, laneHeights]);
3407
+ const groups = useMemo(
3408
+ () => effectiveSwimlane ? resolveSwimlanes(effectiveSwimlane) : [],
3409
+ [effectiveSwimlane]
3410
+ );
3411
+ const lanes = useMemo(() => flattenLanes(groups), [groups]);
3412
+ const margin = useMemo(() => resolveMargin(swimlaneMargin), [swimlaneMargin]);
3413
+ const clampEnabled = (clampToSwimlane ?? true) && lanes.length > 0;
3414
+ const lanesRef = useRef(lanes);
3415
+ lanesRef.current = lanes;
3416
+ const marginRef = useRef(margin);
3417
+ marginRef.current = margin;
3418
+ const onSizeChangeRef = useRef(onSwimlaneSizeChange);
3419
+ onSizeChangeRef.current = onSwimlaneSizeChange;
3420
+ const resizeSwimlane = useCallback(
3421
+ (laneId, height, commit, reason = "resize") => {
3422
+ const lane = lanesRef.current.find((l) => l.id === laneId);
3423
+ if (!lane) return;
3424
+ const min = requiredLaneHeight(lane, store.nodes.values(), marginRef.current.bottom);
3425
+ const next = Math.max(min, height);
3426
+ setLaneHeights((prev) => prev[laneId] === next ? prev : { ...prev, [laneId]: next });
3427
+ if (commit) onSizeChangeRef.current?.({ laneId, height: next, reason });
3428
+ },
3429
+ [store]
3430
+ );
3431
+ useEffect(() => {
3432
+ if (!clampEnabled) return;
3433
+ return store.subscribeFrame(() => {
3434
+ if (store.isNodeDragging()) return;
3435
+ const marginBottom = marginRef.current.bottom;
3436
+ const grown = {};
3437
+ for (const lane of lanesRef.current) {
3438
+ const need = requiredLaneHeight(lane, store.nodes.values(), marginBottom);
3439
+ if (need > lane.height + 0.5) grown[lane.id] = need;
3440
+ }
3441
+ const entries = Object.entries(grown);
3442
+ if (entries.length === 0) return;
3443
+ setLaneHeights((prev) => ({ ...prev, ...grown }));
3444
+ for (const [id, height] of entries) {
3445
+ onSizeChangeRef.current?.({ laneId: id, height, reason: "autofit" });
3446
+ }
3447
+ });
3448
+ }, [store, clampEnabled]);
3449
+ useEffect(() => {
3450
+ if (!clampEnabled) {
3451
+ store.setViewportClamp(null);
3452
+ return;
3453
+ }
3454
+ const minX = Math.min(...groups.map((group) => group.x));
3455
+ const minY = Math.min(...groups.map((group) => group.top));
3456
+ const last = lanes.reduce((a, b) => b.top + b.height > a.top + a.height ? b : a);
3457
+ const maxY = last.top + (1 - swimlaneBottomReveal) * last.height;
3458
+ store.setViewportClamp({ minX, minY, maxY });
3459
+ return () => store.setViewportClamp(null);
3460
+ }, [store, clampEnabled, groups, lanes, swimlaneBottomReveal]);
3461
+ return { groups, lanes, margin, clampEnabled, resizableEnabled, resizeSwimlane };
3462
+ }
3463
+
3464
+ // src/renderer/canvas-2d-edge-renderer.ts
3465
+ var PICK_TOLERANCE = 6;
3466
+ var ENDPOINT_RADIUS = 12;
3467
+ var ARROW_SIZE = 9;
3468
+ var DASH_SPEED = 40;
3469
+ var Canvas2DEdgeRenderer = class {
3470
+ constructor(canvas, options = {}) {
3471
+ this.canvas = canvas;
3472
+ const ctx = canvas.getContext("2d");
3473
+ if (!ctx) throw new Error("rivet: 2D canvas context is unavailable");
3474
+ this.ctx = ctx;
3475
+ this.edgeTypes = { ...BUILTIN_EDGE_TYPES, ...options.edgeTypes };
3476
+ this.defaults = options.defaultEdgeOptions ?? {};
3477
+ }
3478
+ canvas;
3479
+ ctx;
3480
+ dpr = 1;
3481
+ width = 0;
3482
+ height = 0;
3483
+ edgeTypes;
3484
+ defaults;
3485
+ /** Screen-space polylines from the last frame, keyed by edge id, for picking. */
3486
+ geometry = /* @__PURE__ */ new Map();
3487
+ /** Screen-space endpoints per edge, for endpoint (reconnection) picking. */
3488
+ endpoints = /* @__PURE__ */ new Map();
3489
+ /** Screen-space label anchor per edge that has a label. */
3490
+ labels = /* @__PURE__ */ new Map();
3491
+ resize(width, height, dpr) {
3492
+ this.width = width;
3493
+ this.height = height;
3494
+ this.dpr = dpr;
3495
+ this.canvas.width = Math.max(1, Math.round(width * dpr));
3496
+ this.canvas.height = Math.max(1, Math.round(height * dpr));
3497
+ this.canvas.style.width = `${width}px`;
3498
+ this.canvas.style.height = `${height}px`;
3499
+ }
3500
+ /** Screen-space label anchors for the edges drawn last frame. */
3501
+ getLabels() {
3502
+ return this.labels;
3503
+ }
3504
+ /** Screen-space endpoints per edge from the last frame (reconnect affordance). */
3505
+ getEndpoints() {
3506
+ return this.endpoints;
3507
+ }
3508
+ draw(edges, nodes, viewport, extras) {
3509
+ const ctx = this.ctx;
3510
+ const now = performance.now();
3511
+ ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
3512
+ ctx.clearRect(0, 0, this.width, this.height);
3513
+ ctx.lineJoin = "round";
3514
+ ctx.lineCap = "round";
3515
+ this.geometry.clear();
3516
+ this.endpoints.clear();
3517
+ this.labels.clear();
3518
+ const resolved = buildEdges(edges, nodes, viewport, this.edgeTypes, this.defaults, extras);
3519
+ for (const edge of resolved) {
3520
+ this.geometry.set(edge.id, edge.points);
3521
+ this.endpoints.set(edge.id, { source: edge.source, target: edge.target });
3522
+ if (edge.label) this.labels.set(edge.id, edge.label);
3523
+ ctx.strokeStyle = edge.stroke;
3524
+ ctx.lineWidth = edge.width;
3525
+ ctx.globalAlpha = edge.opacity;
3526
+ if (edge.animated) {
3527
+ ctx.setLineDash([8, 6]);
3528
+ ctx.lineDashOffset = -(now / 1e3 * DASH_SPEED) % 14;
3529
+ } else if (edge.dash) {
3530
+ ctx.setLineDash(edge.dash);
3531
+ ctx.lineDashOffset = 0;
3532
+ } else {
3533
+ ctx.setLineDash([]);
3534
+ ctx.lineDashOffset = 0;
3535
+ }
3536
+ strokePolyline(ctx, edge.points);
3537
+ ctx.setLineDash([]);
3538
+ ctx.globalAlpha = 1;
3539
+ const markerColor = ctx.strokeStyle;
3540
+ if (edge.markerEnd) drawMarker(ctx, edge.points, "end", edge.markerEnd, markerColor);
3541
+ if (edge.markerStart) drawMarker(ctx, edge.points, "start", edge.markerStart, markerColor);
3542
+ }
3543
+ const pending = extras?.pending;
3544
+ if (pending) {
3545
+ const points = buildPendingPath(pending, extras?.handles, viewport);
3546
+ ctx.strokeStyle = PENDING_COLOR;
3547
+ ctx.lineWidth = EDGE_WIDTH;
3548
+ ctx.globalAlpha = 1;
3549
+ ctx.setLineDash([6, 4]);
3550
+ strokePolyline(ctx, points);
3551
+ ctx.setLineDash([]);
3552
+ }
3553
+ ctx.globalAlpha = 1;
3554
+ ctx.setLineDash([]);
3555
+ }
3556
+ pick(x, y, tolerance = PICK_TOLERANCE) {
3557
+ for (const [id, points] of this.geometry) {
3558
+ if (distanceToPolyline({ x, y }, points) <= tolerance) return id;
3559
+ }
3560
+ return null;
3561
+ }
3562
+ pickEndpoint(x, y) {
3563
+ let best = null;
3564
+ let bestDist = ENDPOINT_RADIUS;
3565
+ for (const [edgeId, ends] of this.endpoints) {
3566
+ const ds = Math.hypot(x - ends.source.x, y - ends.source.y);
3567
+ if (ds < bestDist) {
3568
+ bestDist = ds;
3569
+ best = { edgeId, end: "source" };
3570
+ }
3571
+ const dt = Math.hypot(x - ends.target.x, y - ends.target.y);
3572
+ if (dt < bestDist) {
3573
+ bestDist = dt;
3574
+ best = { edgeId, end: "target" };
3575
+ }
3576
+ }
3577
+ return best;
3578
+ }
3579
+ dispose() {
3580
+ this.geometry.clear();
3581
+ this.endpoints.clear();
3582
+ this.labels.clear();
3583
+ }
3584
+ };
3585
+ var canvas2DEdgeRenderer = (canvas, options) => new Canvas2DEdgeRenderer(canvas, options);
3586
+ function strokePolyline(ctx, points) {
3587
+ const first = points[0];
3588
+ if (!first) return;
3589
+ ctx.beginPath();
3590
+ ctx.moveTo(first.x, first.y);
3591
+ for (let i = 1; i < points.length; i++) {
3592
+ const p = points[i];
3593
+ if (p) ctx.lineTo(p.x, p.y);
3594
+ }
3595
+ ctx.stroke();
3596
+ }
3597
+ function drawMarker(ctx, points, at, kind, color) {
3598
+ const tip = at === "end" ? points[points.length - 1] : points[0];
3599
+ const prev = at === "end" ? points[points.length - 2] : points[1];
3600
+ if (!tip || !prev) return;
3601
+ const angle = Math.atan2(tip.y - prev.y, tip.x - prev.x);
3602
+ const spread = 0.5;
3603
+ const left = {
3604
+ x: tip.x - ARROW_SIZE * Math.cos(angle - spread),
3605
+ y: tip.y - ARROW_SIZE * Math.sin(angle - spread)
3606
+ };
3607
+ const right = {
3608
+ x: tip.x - ARROW_SIZE * Math.cos(angle + spread),
3609
+ y: tip.y - ARROW_SIZE * Math.sin(angle + spread)
3610
+ };
3611
+ ctx.beginPath();
3612
+ ctx.moveTo(left.x, left.y);
3613
+ ctx.lineTo(tip.x, tip.y);
3614
+ ctx.lineTo(right.x, right.y);
3615
+ if (kind === "arrowclosed") {
3616
+ ctx.closePath();
3617
+ ctx.fillStyle = color;
3618
+ ctx.fill();
3619
+ } else {
3620
+ ctx.strokeStyle = color;
3621
+ ctx.stroke();
3622
+ }
3623
+ }
3624
+ var overlayStyle = {
3625
+ position: "absolute",
3626
+ inset: 0,
3627
+ overflow: "hidden",
3628
+ pointerEvents: "none"
3629
+ };
3630
+ var labelStyle2 = {
3631
+ position: "absolute",
3632
+ top: 0,
3633
+ left: 0,
3634
+ padding: "1px 6px",
3635
+ borderRadius: 4,
3636
+ background: "rgba(255, 255, 255, 0.9)",
3637
+ color: "#0f172a",
3638
+ fontSize: 11,
3639
+ lineHeight: 1.4,
3640
+ whiteSpace: "nowrap",
3641
+ pointerEvents: "none"
3642
+ };
3643
+ function sameIds(a, b) {
3644
+ if (a.length !== b.length) return false;
3645
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
3646
+ return true;
3647
+ }
3648
+ function EdgeLabelLayer() {
3649
+ const { store, defaultEdgeOptions } = useRivetContext();
3650
+ const [ids, setIds] = useState([]);
3651
+ const refs = useRef(/* @__PURE__ */ new Map());
3652
+ const position = useCallback(() => {
3653
+ const anchors = store.getEdgeLabelAnchors();
3654
+ for (const [id, el] of refs.current) {
3655
+ if (!el) continue;
3656
+ const anchor = anchors.get(id);
3657
+ if (anchor) {
3658
+ el.style.display = "";
3659
+ el.style.transform = `translate(${anchor.x}px, ${anchor.y}px) translate(-50%, -50%)`;
3660
+ } else {
3661
+ el.style.display = "none";
3662
+ }
3663
+ }
3664
+ }, [store]);
3665
+ useEffect(() => {
3666
+ return store.subscribeFrame(() => {
3667
+ const anchors = store.getEdgeLabelAnchors();
3668
+ const next = [...anchors.keys()];
3669
+ setIds((prev) => sameIds(prev, next) ? prev : next);
3670
+ position();
3671
+ });
3672
+ }, [store, position]);
3673
+ useLayoutEffect(position);
3674
+ return /* @__PURE__ */ jsx("div", { style: overlayStyle, children: ids.map((id) => {
3675
+ const edge = store.edges.get(id);
3676
+ if (!edge || edge.label === void 0) return null;
3677
+ return /* @__PURE__ */ jsx(
3678
+ "div",
3679
+ {
3680
+ ref: (el) => {
3681
+ refs.current.set(id, el);
3682
+ },
3683
+ style: { ...labelStyle2, ...defaultEdgeOptions?.labelStyle, ...edge.labelStyle },
3684
+ children: String(edge.label)
3685
+ },
3686
+ id
3687
+ );
3688
+ }) });
3689
+ }
3690
+
3691
+ // src/input/group-drag.ts
3692
+ var DRAG_THRESHOLD = 4;
3693
+ var ALIGN_THRESHOLD = 6;
3694
+ function startGroupDrag(params) {
3695
+ const { store, el, snapGrid, alignmentGuides, onLaneChange, onFrame } = params;
3696
+ const { swimlaneLanes, clampToSwimlane, swimlaneMargin, swimlaneLabelWidth } = params;
3697
+ const { zoom } = store.getViewport();
3698
+ const originX = params.event.clientX;
3699
+ const originY = params.event.clientY;
3700
+ const pointerId = params.event.pointerId;
3701
+ const clampActive = clampToSwimlane && swimlaneLanes.length > 0;
3702
+ let started = false;
3703
+ let starts = /* @__PURE__ */ new Map();
3704
+ let affected = /* @__PURE__ */ new Set();
3705
+ const repositionAffected = () => {
3706
+ for (const affectedId of affected) {
3707
+ const affectedEl = store.getNodeElement(affectedId);
3708
+ if (!affectedEl) continue;
3709
+ const world = store.getNodeWorldPosition(affectedId);
3710
+ affectedEl.style.transform = `translate(${world.x}px, ${world.y}px)`;
3711
+ }
3712
+ onFrame?.();
3713
+ };
3714
+ const beginDrag = () => {
3715
+ started = true;
3716
+ const movers = store.getMovers(store.getSelectedNodes());
3717
+ starts = /* @__PURE__ */ new Map();
3718
+ for (const moverId of movers) {
3719
+ const moverNode = store.nodes.get(moverId);
3720
+ if (moverNode) starts.set(moverId, moverNode.position);
3721
+ }
3722
+ affected = /* @__PURE__ */ new Set();
3723
+ for (const moverId of movers) {
3724
+ affected.add(moverId);
3725
+ for (const descendant of store.getDescendantIds(moverId)) affected.add(descendant);
3726
+ }
3727
+ store.setNodeDragging(true);
3728
+ el.setPointerCapture(pointerId);
3729
+ el.style.cursor = "grabbing";
3730
+ window.getSelection()?.removeAllRanges();
3731
+ document.body.style.userSelect = "none";
3732
+ document.body.style.webkitUserSelect = "none";
3733
+ };
3734
+ const onMove = (moveEvent) => {
3735
+ if (!started) {
3736
+ const moved = Math.hypot(moveEvent.clientX - originX, moveEvent.clientY - originY);
3737
+ if (moved < DRAG_THRESHOLD) return;
3738
+ beginDrag();
3739
+ }
3740
+ const dx = (moveEvent.clientX - originX) / zoom;
3741
+ const dy = (moveEvent.clientY - originY) / zoom;
3742
+ const single = starts.size === 1;
3743
+ for (const [moverId, start] of starts) {
3744
+ const moverNode = store.nodes.get(moverId);
3745
+ let next = { x: start.x + dx, y: start.y + dy };
3746
+ if (snapGrid) next = snapToGrid(next, snapGrid);
3747
+ if (moverNode?.parentId && moverNode.extent === "parent" && moverNode.size) {
3748
+ next = clampToParent(store, moverNode.parentId, next, moverNode.size);
3749
+ } else if (alignmentGuides && single && moverNode?.size && !moverNode.parentId) {
3750
+ const movingRect = {
3751
+ x: next.x,
3752
+ y: next.y,
3753
+ width: moverNode.size.width,
3754
+ height: moverNode.size.height
3755
+ };
3756
+ const statics = [];
3757
+ for (const [otherId, other] of store.nodes) {
3758
+ if (otherId === moverId || other.parentId) continue;
3759
+ statics.push(store.getNodeRect(otherId));
3760
+ }
3761
+ const { offset, guides } = alignRect(movingRect, statics, ALIGN_THRESHOLD / zoom);
3762
+ next = { x: next.x + offset.x, y: next.y + offset.y };
3763
+ store.setAlignmentGuides(guides);
3764
+ }
3765
+ store.moveNode(moverId, next, false);
3766
+ }
3767
+ repositionAffected();
3768
+ };
3769
+ const onUp = () => {
3770
+ window.removeEventListener("pointermove", onMove);
3771
+ window.removeEventListener("pointerup", onUp);
3772
+ if (!started) return;
3773
+ el.style.cursor = "grab";
3774
+ document.body.style.userSelect = "";
3775
+ document.body.style.webkitUserSelect = "";
3776
+ store.setAlignmentGuides([]);
3777
+ store.setNodeDragging(false);
3778
+ for (const [moverId, start] of starts) {
3779
+ const moverNode = store.nodes.get(moverId);
3780
+ let position = moverNode?.position ?? start;
3781
+ if (moverNode?.parentId && moverNode.extent === "parent" && moverNode.size) {
3782
+ position = clampToParent(store, moverNode.parentId, position, moverNode.size);
3783
+ } else if (clampActive && moverNode?.size) {
3784
+ const result = clampNodeToLanes(
3785
+ position,
3786
+ moverNode.size,
3787
+ swimlaneLanes,
3788
+ swimlaneMargin,
3789
+ swimlaneLabelWidth
3790
+ );
3791
+ position = result.position;
3792
+ const previousLaneId = store.setNodeLane(moverId, result.laneId);
3793
+ if (previousLaneId !== false) {
3794
+ onLaneChange?.({ nodeId: moverId, laneId: result.laneId, previousLaneId });
3795
+ }
3796
+ }
3797
+ store.moveNode(moverId, position, true);
3798
+ }
3799
+ repositionAffected();
3800
+ };
3801
+ window.addEventListener("pointermove", onMove);
3802
+ window.addEventListener("pointerup", onUp);
3803
+ }
3804
+ function clampToParent(store, parentId, position, size) {
3805
+ const parent = store.nodes.get(parentId);
3806
+ const parentSize = {
3807
+ width: parent?.width ?? parent?.size?.width ?? 0,
3808
+ height: parent?.height ?? parent?.size?.height ?? 0
3809
+ };
3810
+ return clampChildToParent(position, size, parentSize);
3811
+ }
3812
+ var DEFAULT_ANCHOR_COLOR = "#6366f1";
3813
+ function NodeAnchors({ nodeId }) {
3814
+ const { store, anchorOptions } = useRivetContext();
3815
+ const subscribeAnchors = useCallback(
3816
+ (listener) => store.subscribeNodeAnchors(nodeId, listener),
3817
+ [store, nodeId]
3818
+ );
3819
+ const getAnchorsVersion = useCallback(() => store.getNodeAnchorsVersion(nodeId), [store, nodeId]);
3820
+ useSyncExternalStore(subscribeAnchors, getAnchorsVersion, getAnchorsVersion);
3821
+ useSyncExternalStore(store.subscribeEdges, store.getEdgesVersion, store.getEdgesVersion);
3822
+ const records = store.getNodeAnchors(nodeId);
3823
+ if (records.length === 0) return null;
3824
+ const dotsVisible = Boolean(store.nodes.get(nodeId)?.hovered);
3825
+ const connectedSides = /* @__PURE__ */ new Map();
3826
+ for (const edge of store.edges.values()) {
3827
+ const ends = [
3828
+ edge.source === nodeId ? parseAnchorHandleId(edge.sourceHandle) : null,
3829
+ edge.target === nodeId ? parseAnchorHandleId(edge.targetHandle) : null
3830
+ ];
3831
+ for (const end of ends) {
3832
+ if (!end) continue;
3833
+ const sides = connectedSides.get(end.anchorId) ?? [];
3834
+ if (!sides.includes(end.side)) sides.push(end.side);
3835
+ connectedSides.set(end.anchorId, sides);
3836
+ }
3837
+ }
3838
+ return /* @__PURE__ */ jsx(Fragment, { children: records.map((record) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
3839
+ /* @__PURE__ */ jsx(AnchorDots, { record, options: anchorOptions, visible: dotsVisible }),
3840
+ record.strays !== "none" && /* @__PURE__ */ jsx(
3841
+ AnchorStrayHandles,
3842
+ {
3843
+ record,
3844
+ options: anchorOptions,
3845
+ connectedSides: connectedSides.get(record.anchorId)
3846
+ }
3847
+ )
3848
+ ] }, record.anchorId)) });
3849
+ }
3850
+ function AnchorDots(props) {
3851
+ const { record, options } = props;
3852
+ if (!record.geometry || !record.element?.isConnected) return null;
3853
+ const dots = record.geometry.dots;
3854
+ return /* @__PURE__ */ jsx(Fragment, { children: ANCHOR_SIDES.map((side) => {
3855
+ const dot = dots[side];
3856
+ return /* @__PURE__ */ jsx(
3857
+ Handle,
3858
+ {
3859
+ id: anchorDotHandleId(record.anchorId, side),
3860
+ type: "either",
3861
+ position: side,
3862
+ style: {
3863
+ left: `${dot.xPct}%`,
3864
+ top: `${dot.yPct}%`,
3865
+ right: "auto",
3866
+ bottom: "auto",
3867
+ transform: "translate(-50%, -50%)",
3868
+ width: options.dotSize,
3869
+ height: options.dotSize,
3870
+ background: record.color ?? DEFAULT_ANCHOR_COLOR,
3871
+ border: "none",
3872
+ opacity: props.visible ? 1 : 0,
3873
+ transition: "opacity 150ms"
3874
+ }
3875
+ },
3876
+ `${side}:${dot.xPct.toFixed(1)},${dot.yPct.toFixed(1)}`
3877
+ );
3878
+ }) });
3879
+ }
3880
+ function AnchorStrayHandles(props) {
3881
+ const { record, options } = props;
3882
+ const geometry = record.geometry;
3883
+ if (!geometry) return null;
3884
+ const sides = props.connectedSides && props.connectedSides.length > 0 ? props.connectedSides : [options.defaultSide];
3885
+ return /* @__PURE__ */ jsx(Fragment, { children: sides.map((side) => {
3886
+ const placed = strayPlacementPct(geometry, side, options);
3887
+ const axis = side === "top" || side === "bottom" ? { left: `${placed}%` } : { top: `${placed}%` };
3888
+ return /* @__PURE__ */ jsx(
3889
+ Handle,
3890
+ {
3891
+ id: anchorHandleId(record.anchorId, side),
3892
+ type: "either",
3893
+ position: side,
3894
+ style: {
3895
+ width: options.strayHandleSize,
3896
+ height: options.strayHandleSize,
3897
+ background: record.color ?? DEFAULT_ANCHOR_COLOR,
3898
+ border: "none",
3899
+ ...axis
3900
+ }
3901
+ },
3902
+ `${side}:${placed.toFixed(1)}`
3903
+ );
3904
+ }) });
3905
+ }
3906
+ var wrapperStyle = {
3907
+ position: "absolute",
3908
+ top: 0,
3909
+ left: 0,
3910
+ pointerEvents: "auto",
3911
+ cursor: "grab",
3912
+ userSelect: "none"
3913
+ };
3914
+ var NodeWrapper = memo(function NodeWrapper2({ id }) {
3915
+ const { store, nodeTypes, onLaneChange, multiSelectionKeys, snapGrid } = useRivetContext();
3916
+ const { swimlaneLanes, clampToSwimlane, swimlaneMargin, swimlaneLabelWidth, alignmentGuides } = useRivetContext();
3917
+ const { announce, nodeDescriptionId } = useRivetContext();
3918
+ const ref = useRef(null);
3919
+ const [grabbed, setGrabbed] = useState(false);
3920
+ const grabOrigins = useRef(null);
3921
+ const nodeName = () => {
3922
+ const node2 = store.nodes.get(id);
3923
+ return node2?.ariaLabel ?? `node ${id}`;
3924
+ };
3925
+ const grab = () => {
3926
+ const movers = store.getMovers(store.isNodeSelected(id) ? store.getSelectedNodes() : [id]);
3927
+ const origins = /* @__PURE__ */ new Map();
3928
+ for (const moverId of movers) {
3929
+ const mover = store.nodes.get(moverId);
3930
+ if (mover) origins.set(moverId, { ...mover.position });
3931
+ }
3932
+ grabOrigins.current = origins;
3933
+ setGrabbed(true);
3934
+ announce(`Grabbed ${nodeName()}. Use the arrow keys to move, Enter to drop, Escape to cancel.`);
3935
+ };
3936
+ const releaseGrab = (revert) => {
3937
+ const origins = grabOrigins.current;
3938
+ grabOrigins.current = null;
3939
+ setGrabbed(false);
3940
+ if (!origins) return;
3941
+ if (revert) {
3942
+ for (const [moverId, origin] of origins) store.moveNode(moverId, origin, true);
3943
+ announce("Move cancelled.");
3944
+ return;
3945
+ }
3946
+ const node2 = store.nodes.get(id);
3947
+ if (node2) {
3948
+ const { x, y } = node2.position;
3949
+ announce(`Dropped ${nodeName()} at ${Math.round(x)}, ${Math.round(y)}.`);
3950
+ }
3951
+ };
3952
+ const subscribe = useCallback(
3953
+ (onChange) => store.subscribeNode(id, onChange),
3954
+ [store, id]
3955
+ );
3956
+ const getSnapshot = useCallback(() => store.getNodeVersion(id), [store, id]);
3957
+ useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
3958
+ const nodeContext = useMemo(() => ({ nodeId: id, wrapperRef: ref }), [id]);
3959
+ useEffect(() => {
3960
+ const el = ref.current;
3961
+ if (!el) return;
3962
+ store.registerNodeElement(id, el);
3963
+ const observer = new ResizeObserver(() => {
3964
+ store.setNodeSize(id, { width: el.offsetWidth, height: el.offsetHeight });
3965
+ store.remeasureAnchors(id);
3966
+ });
3967
+ observer.observe(el);
3968
+ return () => {
3969
+ observer.disconnect();
3970
+ store.unregisterNodeElement(id, el);
3971
+ };
3972
+ }, [id, store]);
3973
+ const onPointerEnter = () => store.hoverNode(id);
3974
+ const onPointerLeave = () => store.hoverNode(null);
3975
+ const onFocus = (event) => {
3976
+ if (event.target !== event.currentTarget) return;
3977
+ store.setFocusedNode(id);
3978
+ };
3979
+ const onBlur = (event) => {
3980
+ if (event.target !== event.currentTarget) return;
3981
+ if (grabOrigins.current) releaseGrab(false);
3982
+ const next = event.relatedTarget;
3983
+ if (next instanceof Element && next.closest(SELECTOR.node)) return;
3984
+ if (store.getFocusedNode() === id) store.setFocusedNode(null);
3985
+ };
3986
+ const onKeyDown = (event) => {
3987
+ if (event.target !== event.currentTarget) return;
3988
+ const node2 = store.nodes.get(id);
3989
+ if (!node2 || node2.selectable === false) return;
3990
+ if (event.key === "Enter" || event.key === " ") {
3991
+ event.preventDefault();
3992
+ if (grabOrigins.current) {
3993
+ releaseGrab(false);
3994
+ return;
3995
+ }
3996
+ if (keyHeld(event, multiSelectionKeys)) {
3997
+ store.selectNode(id, true);
3998
+ return;
3999
+ }
4000
+ if (!store.isNodeSelected(id)) store.selectNode(id);
4001
+ if (node2.draggable !== false) grab();
4002
+ return;
4003
+ }
4004
+ if (event.key === "Escape") {
4005
+ if (grabOrigins.current) {
4006
+ event.preventDefault();
4007
+ releaseGrab(true);
4008
+ return;
4009
+ }
4010
+ store.selectNode(null);
4011
+ return;
4012
+ }
4013
+ const dir = ARROW_DIRS[event.key];
4014
+ if (!dir || !grabOrigins.current) return;
4015
+ event.preventDefault();
4016
+ const factor = event.shiftKey ? ARROW_STEP_SHIFT : 1;
4017
+ const stepX = (snapGrid?.[0] || ARROW_STEP) * factor;
4018
+ const stepY = (snapGrid?.[1] || ARROW_STEP) * factor;
4019
+ for (const moverId of grabOrigins.current.keys()) {
4020
+ const mover = store.nodes.get(moverId);
4021
+ if (!mover) continue;
4022
+ let next = {
4023
+ x: mover.position.x + dir.x * stepX,
4024
+ y: mover.position.y + dir.y * stepY
4025
+ };
4026
+ if (snapGrid) next = snapToGrid(next, snapGrid);
4027
+ if (mover.parentId && mover.extent === "parent" && mover.size) {
4028
+ next = clampToParent(store, mover.parentId, next, mover.size);
4029
+ }
4030
+ store.moveNode(moverId, next, true);
4031
+ }
4032
+ };
4033
+ const onPointerDown = (event) => {
4034
+ if (event.button !== 0) return;
4035
+ if (ref.current && event.target instanceof Node && !ref.current.contains(event.target)) return;
4036
+ if (event.target instanceof Element && event.target.closest(SELECTOR.noPan)) return;
4037
+ event.stopPropagation();
4038
+ const el = ref.current;
4039
+ const node2 = store.nodes.get(id);
4040
+ if (!el || !node2) return;
4041
+ const target = event.target instanceof Element ? event.target : null;
4042
+ const noSelect = node2.selectable === false || Boolean(target?.closest(SELECTOR.noSelect));
4043
+ const noDrag = node2.draggable === false || Boolean(target?.closest(SELECTOR.noDrag));
4044
+ const additive = keyHeld(event, multiSelectionKeys);
4045
+ if (additive) {
4046
+ if (!noSelect) store.selectNode(id, true);
4047
+ return;
4048
+ }
4049
+ if (!noSelect && !store.isNodeSelected(id)) store.selectNode(id);
4050
+ if (noDrag) return;
4051
+ startGroupDrag({
4052
+ store,
4053
+ el,
4054
+ event,
4055
+ snapGrid,
4056
+ alignmentGuides,
4057
+ swimlaneLanes,
4058
+ clampToSwimlane,
4059
+ swimlaneMargin,
4060
+ swimlaneLabelWidth,
4061
+ onLaneChange
4062
+ });
4063
+ };
4064
+ const node = store.nodes.get(id);
4065
+ if (!node) return null;
4066
+ const key = node.type ?? "default";
4067
+ const Component = nodeTypes[key] ?? BUILTIN_NODE_TYPES[key] ?? DefaultNode;
4068
+ const world = store.getNodeWorldPosition(id);
4069
+ const depth = store.getNodeDepth(id);
4070
+ return (
4071
+ // biome-ignore lint/a11y/useSemanticElements: a node wraps arbitrary (often interactive) content, so it can't be a native <button>
4072
+ /* @__PURE__ */ jsx(
4073
+ "div",
4074
+ {
4075
+ ref,
4076
+ "data-rivet-node": id,
4077
+ "data-rivet-grabbed": grabbed ? "" : void 0,
4078
+ tabIndex: node.selectable === false ? -1 : 0,
4079
+ role: "button",
4080
+ "aria-roledescription": "graph node",
4081
+ "aria-pressed": Boolean(node.selected),
4082
+ "aria-label": node.ariaLabel,
4083
+ "aria-describedby": node.selectable === false ? void 0 : nodeDescriptionId,
4084
+ onPointerDown,
4085
+ onPointerEnter,
4086
+ onPointerLeave,
4087
+ onKeyDown,
4088
+ onFocus,
4089
+ onBlur,
4090
+ style: {
4091
+ ...wrapperStyle,
4092
+ transform: `translate(${world.x}px, ${world.y}px)`,
4093
+ zIndex: depth,
4094
+ // Explicit dimensions from NodeResizer override the intrinsic size.
4095
+ ...node.width !== void 0 ? { width: node.width } : {},
4096
+ ...node.height !== void 0 ? { height: node.height } : {}
4097
+ },
4098
+ children: /* @__PURE__ */ jsxs(RivetNodeContext.Provider, { value: nodeContext, children: [
4099
+ /* @__PURE__ */ jsx(
4100
+ Component,
4101
+ {
4102
+ id,
4103
+ type: node.type,
4104
+ data: node.data,
4105
+ selected: Boolean(node.selected),
4106
+ hovered: Boolean(node.hovered),
4107
+ width: node.width ?? node.size?.width,
4108
+ height: node.height ?? node.size?.height,
4109
+ position: node.position,
4110
+ positionAbsolute: world
4111
+ }
4112
+ ),
4113
+ /* @__PURE__ */ jsx(NodeAnchors, { nodeId: id })
4114
+ ] })
4115
+ }
4116
+ )
4117
+ );
4118
+ });
4119
+ var NODES_SELECTION_Z = 1e3;
4120
+ var boxStyle = {
4121
+ position: "absolute",
4122
+ top: 0,
4123
+ left: 0,
4124
+ pointerEvents: "auto",
4125
+ cursor: "grab",
4126
+ userSelect: "none",
4127
+ zIndex: NODES_SELECTION_Z,
4128
+ // Same accent as the marquee overlay it succeeds, but dotted: the marquee is
4129
+ // the live gesture, this is the settled selection.
4130
+ border: "1px dotted rgba(99, 102, 241, 0.8)",
4131
+ background: "rgba(99, 102, 241, 0.08)"
4132
+ };
4133
+ var applyRect = (el, rect) => {
4134
+ el.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
4135
+ el.style.width = `${rect.width}px`;
4136
+ el.style.height = `${rect.height}px`;
4137
+ };
4138
+ var NodesSelection = memo(function NodesSelection2() {
4139
+ const { store, snapGrid, alignmentGuides, onLaneChange } = useRivetContext();
4140
+ const { swimlaneLanes, clampToSwimlane, swimlaneMargin, swimlaneLabelWidth } = useRivetContext();
4141
+ const ref = useRef(null);
4142
+ const subscribe = useCallback(
4143
+ (onChange) => store.subscribeSelectionBox(onChange),
4144
+ [store]
4145
+ );
4146
+ const getSnapshot = useCallback(() => store.getSelectionBoxVersion(), [store]);
4147
+ useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
4148
+ const selectionRect = () => boundingRect(store.getSelectedNodes().map((id) => store.getNodeRect(id)));
4149
+ const onPointerDown = (event) => {
4150
+ if (event.button !== 0) return;
4151
+ const el = ref.current;
4152
+ if (!el) return;
4153
+ event.stopPropagation();
4154
+ startGroupDrag({
4155
+ store,
4156
+ el,
4157
+ event,
4158
+ snapGrid,
4159
+ alignmentGuides,
4160
+ swimlaneLanes,
4161
+ clampToSwimlane,
4162
+ swimlaneMargin,
4163
+ swimlaneLabelWidth,
4164
+ onLaneChange,
4165
+ // Node moves are imperative during the drag (no React re-render), so the
4166
+ // box tracks its nodes the same way; the drop's commit re-renders it.
4167
+ onFrame: () => {
4168
+ const rect2 = selectionRect();
4169
+ if (rect2 && ref.current) applyRect(ref.current, rect2);
4170
+ }
4171
+ });
4172
+ };
4173
+ if (!store.isSelectionBoxActive()) return null;
4174
+ const rect = selectionRect();
4175
+ if (!rect) return null;
4176
+ return (
4177
+ // biome-ignore lint/a11y/useSemanticElements: a drag affordance over canvas-positioned nodes, not a form grouping — it can't be a <fieldset>
4178
+ /* @__PURE__ */ jsx(
4179
+ "div",
4180
+ {
4181
+ ref,
4182
+ "data-rivet-nodes-selection": true,
4183
+ role: "group",
4184
+ "aria-label": "Selected nodes",
4185
+ onPointerDown,
4186
+ style: {
4187
+ ...boxStyle,
4188
+ transform: `translate(${rect.x}px, ${rect.y}px)`,
4189
+ width: rect.width,
4190
+ height: rect.height
4191
+ }
4192
+ }
4193
+ )
4194
+ );
4195
+ });
4196
+ var layerStyle = {
4197
+ position: "absolute",
4198
+ top: 0,
4199
+ left: 0,
4200
+ transformOrigin: "0 0",
4201
+ // `will-change: transform` is applied imperatively only while panning/zooming
4202
+ // (see the runtime), then cleared — a permanently promoted layer would upscale a
4203
+ // cached bitmap and blur node text when zoomed in.
4204
+ pointerEvents: "none"
4205
+ };
4206
+ function NodeLayer({ containerRef, visibleIds }) {
4207
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, style: layerStyle, children: [
4208
+ visibleIds.map((id) => /* @__PURE__ */ jsx(NodeWrapper, { id }, id)),
4209
+ /* @__PURE__ */ jsx(NodesSelection, {})
4210
+ ] });
4211
+ }
4212
+ var containerStyle2 = {
4213
+ position: "absolute",
4214
+ inset: 0,
4215
+ overflow: "hidden",
4216
+ // Chrome only — never intercept canvas panning or node dragging.
4217
+ pointerEvents: "none",
4218
+ zIndex: 5
4219
+ };
4220
+ var headerWrapperStyle = {
4221
+ position: "absolute",
4222
+ left: 0,
4223
+ right: 0,
4224
+ overflow: "hidden"
4225
+ };
4226
+ var defaultHeaderStyle = {
4227
+ display: "flex",
4228
+ alignItems: "center",
4229
+ gap: 8,
4230
+ height: "100%",
4231
+ paddingLeft: 12,
4232
+ paddingRight: 12,
4233
+ background: "#ffffff",
4234
+ borderBottom: "1px solid rgba(100, 116, 139, 0.2)",
4235
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
4236
+ fontSize: 12,
4237
+ fontWeight: 600,
4238
+ letterSpacing: 0.4,
4239
+ textTransform: "uppercase",
4240
+ whiteSpace: "nowrap"
4241
+ };
4242
+ var labelWrapperStyle = {
4243
+ position: "absolute",
4244
+ overflow: "hidden"
4245
+ };
4246
+ var defaultLabelStyle = {
4247
+ display: "flex",
4248
+ alignItems: "center",
4249
+ justifyContent: "center",
4250
+ width: "100%",
4251
+ height: "100%",
4252
+ background: "#ffffff",
4253
+ borderRight: "1px solid rgba(100, 116, 139, 0.2)"
4254
+ };
4255
+ var labelTextStyle = {
4256
+ writingMode: "vertical-rl",
4257
+ transform: "rotate(180deg)",
4258
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
4259
+ fontSize: 10,
4260
+ fontWeight: 600,
4261
+ letterSpacing: 0.6,
4262
+ textTransform: "uppercase",
4263
+ color: "rgba(51, 65, 85, 0.85)",
4264
+ whiteSpace: "nowrap",
4265
+ userSelect: "none"
4266
+ };
4267
+ var RESIZE_HANDLE_HIT = 8;
4268
+ function SwimlaneOverlay() {
4269
+ const {
4270
+ swimlaneGroups,
4271
+ swimlaneLabelWidth,
4272
+ swimlaneHeaderSize,
4273
+ swimlaneResizable,
4274
+ renderSwimlaneHeader,
4275
+ renderSwimlaneLabel
4276
+ } = useRivetContext();
4277
+ const { x: vx, y: vy, zoom } = useRivetViewport();
4278
+ if (swimlaneGroups.length === 0) return null;
4279
+ const headerBottomByGroup = /* @__PURE__ */ new Map();
4280
+ return /* @__PURE__ */ jsxs("div", { style: containerStyle2, children: [
4281
+ swimlaneGroups.map((group) => {
4282
+ const screenTop = group.top * zoom + vy;
4283
+ const screenBottom = group.bottom * zoom + vy;
4284
+ const stickyY = Math.max(0, Math.min(screenTop, screenBottom - swimlaneHeaderSize));
4285
+ headerBottomByGroup.set(group.id, stickyY + swimlaneHeaderSize);
4286
+ if (screenBottom <= 0) return null;
4287
+ const sticky = stickyY > screenTop;
4288
+ return /* @__PURE__ */ jsx(
4289
+ "div",
4290
+ {
4291
+ style: { ...headerWrapperStyle, top: stickyY, height: swimlaneHeaderSize },
4292
+ children: renderSwimlaneHeader ? renderSwimlaneHeader({ id: group.id, label: group.label, color: group.color, sticky }) : /* @__PURE__ */ jsx(
4293
+ "div",
4294
+ {
4295
+ style: {
4296
+ ...defaultHeaderStyle,
4297
+ boxShadow: sticky ? "0 1px 2px rgba(0, 0, 0, 0.08)" : void 0
4298
+ },
4299
+ children: group.label
4300
+ }
4301
+ )
4302
+ },
4303
+ group.id
4304
+ );
4305
+ }),
4306
+ swimlaneGroups.flatMap(
4307
+ (group) => group.lanes.map((lane) => {
4308
+ if (!lane.label) return null;
4309
+ const headerBottom = headerBottomByGroup.get(group.id) ?? 0;
4310
+ const screenX = lane.x * zoom + vx;
4311
+ const screenTop = lane.top * zoom + vy;
4312
+ const screenBottom = screenTop + lane.height * zoom;
4313
+ const screenEndX = screenX + lane.width * zoom;
4314
+ const stickyX = Math.max(0, Math.min(screenX, screenEndX - swimlaneLabelWidth));
4315
+ const clampedTop = Math.max(headerBottom, screenTop);
4316
+ const labelHeight = screenBottom - clampedTop;
4317
+ if (screenBottom <= headerBottom || labelHeight <= 0) return null;
4318
+ return /* @__PURE__ */ jsx(
4319
+ "div",
4320
+ {
4321
+ style: {
4322
+ ...labelWrapperStyle,
4323
+ left: stickyX,
4324
+ top: clampedTop,
4325
+ width: swimlaneLabelWidth,
4326
+ height: labelHeight
4327
+ },
4328
+ children: renderSwimlaneLabel ? renderSwimlaneLabel({
4329
+ id: lane.id,
4330
+ label: lane.label,
4331
+ color: lane.color,
4332
+ groupId: lane.groupId
4333
+ }) : /* @__PURE__ */ jsx("div", { style: defaultLabelStyle, children: /* @__PURE__ */ jsx("span", { style: labelTextStyle, children: lane.label }) })
4334
+ },
4335
+ lane.id
4336
+ );
4337
+ })
4338
+ ),
4339
+ swimlaneResizable && swimlaneGroups.flatMap(
4340
+ (group) => group.lanes.map((lane) => {
4341
+ const bottom = (lane.top + lane.height) * zoom + vy;
4342
+ if (bottom < 0) return null;
4343
+ return /* @__PURE__ */ jsx(LaneResizeHandle, { lane, top: bottom - RESIZE_HANDLE_HIT / 2 }, lane.id);
4344
+ })
4345
+ )
4346
+ ] });
4347
+ }
4348
+ var handleStyle = {
4349
+ position: "absolute",
4350
+ left: 0,
4351
+ right: 0,
4352
+ height: RESIZE_HANDLE_HIT,
4353
+ cursor: "ns-resize",
4354
+ pointerEvents: "auto",
4355
+ touchAction: "none"
4356
+ };
4357
+ function LaneResizeHandle({ lane, top }) {
4358
+ const { store, resizeSwimlane, swimlaneMargin } = useRivetContext();
4359
+ const onDoubleClick = useCallback(
4360
+ (event) => {
4361
+ event.stopPropagation();
4362
+ const next = requiredLaneHeight(lane, store.nodes.values(), swimlaneMargin.bottom);
4363
+ resizeSwimlane(lane.id, next, true, "autofit");
4364
+ },
4365
+ [store, resizeSwimlane, swimlaneMargin.bottom, lane]
4366
+ );
4367
+ const onPointerDown = useCallback(
4368
+ (event) => {
4369
+ if (event.button !== 0) return;
4370
+ event.stopPropagation();
4371
+ event.preventDefault();
4372
+ const startClientY = event.clientY;
4373
+ const startHeight = lane.height;
4374
+ const laneId = lane.id;
4375
+ const onMove = (moveEvent) => {
4376
+ const zoom = store.getViewport().zoom || 1;
4377
+ const next = startHeight + (moveEvent.clientY - startClientY) / zoom;
4378
+ resizeSwimlane(laneId, next, false);
4379
+ };
4380
+ const onUp = (upEvent) => {
4381
+ window.removeEventListener("pointermove", onMove);
4382
+ window.removeEventListener("pointerup", onUp);
4383
+ const zoom = store.getViewport().zoom || 1;
4384
+ const next = startHeight + (upEvent.clientY - startClientY) / zoom;
4385
+ resizeSwimlane(laneId, next, true);
4386
+ };
4387
+ window.addEventListener("pointermove", onMove);
4388
+ window.addEventListener("pointerup", onUp);
4389
+ },
4390
+ [store, resizeSwimlane, lane.id, lane.height]
4391
+ );
4392
+ const onKeyDown = useCallback(
4393
+ (event) => {
4394
+ const dir = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
4395
+ if (dir === 0) return;
4396
+ event.preventDefault();
4397
+ const step = event.shiftKey ? 50 : 10;
4398
+ resizeSwimlane(lane.id, lane.height + dir * step, true);
4399
+ },
4400
+ [resizeSwimlane, lane.id, lane.height]
4401
+ );
4402
+ return (
4403
+ // biome-ignore lint/a11y/useSemanticElements: a focusable, arrow-key-resizable separator is an interactive widget — <hr> can't take focus or gestures
4404
+ /* @__PURE__ */ jsx(
4405
+ "div",
4406
+ {
4407
+ "data-rivet-swimlane-handle": "",
4408
+ role: "separator",
4409
+ "aria-orientation": "horizontal",
4410
+ "aria-label": `Resize lane ${lane.label ?? lane.id}`,
4411
+ "aria-valuenow": Math.round(lane.height),
4412
+ tabIndex: 0,
4413
+ style: { ...handleStyle, top },
4414
+ onPointerDown,
4415
+ onDoubleClick,
4416
+ onKeyDown
4417
+ }
4418
+ )
4419
+ );
4420
+ }
4421
+ var paneStyle = {
4422
+ position: "relative",
4423
+ width: "100%",
4424
+ height: "100%",
4425
+ overflow: "hidden",
4426
+ touchAction: "none"
4427
+ };
4428
+ var canvasStyle = {
4429
+ position: "absolute",
4430
+ inset: 0,
4431
+ pointerEvents: "none"
4432
+ };
4433
+ var srOnlyStyle = {
4434
+ position: "absolute",
4435
+ width: 1,
4436
+ height: 1,
4437
+ margin: -1,
4438
+ padding: 0,
4439
+ overflow: "hidden",
4440
+ clipPath: "inset(50%)",
4441
+ whiteSpace: "nowrap",
4442
+ border: 0
4443
+ };
4444
+ 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.";
4445
+ var DEFAULT_MULTI_SELECTION_KEY_CODE = ["Meta", "Control", "Shift"];
4446
+ var EMPTY_NODE_TYPES = {};
4447
+ var EMPTY_NODES = [];
4448
+ var EMPTY_EDGES = [];
4449
+ var DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1 };
4450
+ function Rivet({
4451
+ defaultNodes = EMPTY_NODES,
4452
+ defaultEdges = EMPTY_EDGES,
4453
+ nodes: controlledNodes,
4454
+ edges: controlledEdges,
4455
+ onNodesChange,
4456
+ onEdgesChange,
4457
+ defaultViewport = DEFAULT_VIEWPORT,
4458
+ nodeTypes = EMPTY_NODE_TYPES,
4459
+ edgeTypes,
4460
+ defaultEdgeOptions,
4461
+ ariaLabel,
4462
+ renderer = canvas2DEdgeRenderer,
4463
+ minZoom = 0.2,
4464
+ maxZoom = 2.5,
4465
+ gridGap = 24,
4466
+ snapGrid,
4467
+ alignmentGuides = false,
4468
+ anchorOptions,
4469
+ panOnDrag = true,
4470
+ selectionOnDrag = false,
4471
+ selectionKeyCode = "Shift",
4472
+ multiSelectionKeyCode = DEFAULT_MULTI_SELECTION_KEY_CODE,
4473
+ scrollToPan = false,
4474
+ zoomSpeed = 15e-4,
4475
+ swimlane,
4476
+ clampToSwimlane,
4477
+ swimlaneMargin,
4478
+ swimlaneHeaderHeight = 32,
4479
+ swimlaneResizable,
4480
+ swimlaneBottomReveal = 0.5,
4481
+ renderSwimlaneHeader,
4482
+ renderSwimlaneLabel,
4483
+ onSwimlaneSizeChange,
4484
+ onPaneContextMenu,
4485
+ onSwimlaneHeaderContextMenu,
4486
+ onSwimlaneGroupHeaderContextMenu,
4487
+ onLaneChange,
4488
+ isValidConnection,
4489
+ mapConnection,
4490
+ onConnect,
4491
+ edgesReconnectable,
4492
+ edgeAlignment = "manual",
4493
+ onReconnect,
4494
+ onReconnectStart,
4495
+ onReconnectEnd,
4496
+ onSelectionChange,
4497
+ onFocusChange,
4498
+ className,
4499
+ style,
4500
+ children
4501
+ }) {
4502
+ const storeRef = useRef(null);
4503
+ if (storeRef.current === null) {
4504
+ storeRef.current = createRivetStore({
4505
+ nodes: controlledNodes ?? defaultNodes,
4506
+ edges: controlledEdges ?? defaultEdges,
4507
+ viewport: defaultViewport
4508
+ });
4509
+ }
4510
+ const store = storeRef.current;
4511
+ const cfg = useRivetConfig({
4512
+ nodeTypes,
4513
+ edgeTypes,
4514
+ defaultEdgeOptions,
4515
+ snapGrid,
4516
+ swimlaneMargin,
4517
+ anchorOptions,
4518
+ panOnDrag,
4519
+ selectionKeyCode,
4520
+ multiSelectionKeyCode,
4521
+ isValidConnection,
4522
+ mapConnection,
4523
+ onConnect,
4524
+ onLaneChange,
4525
+ renderSwimlaneHeader,
4526
+ renderSwimlaneLabel
4527
+ });
4528
+ const mapConnectionWithAnchors = useMemo(
4529
+ () => (connection) => {
4530
+ const normalized = normalizeAnchorConnection(connection);
4531
+ return cfg.mapConnection ? cfg.mapConnection(normalized) : normalized;
4532
+ },
4533
+ [cfg.mapConnection]
4534
+ );
4535
+ useEffect(() => {
4536
+ store.setEdgeAlignment(edgeAlignment);
4537
+ }, [store, edgeAlignment]);
4538
+ const onSelectionChangeRef = useRef(onSelectionChange);
4539
+ onSelectionChangeRef.current = onSelectionChange;
4540
+ useEffect(
4541
+ () => store.subscribeSelection((selection) => onSelectionChangeRef.current?.(selection)),
4542
+ [store]
4543
+ );
4544
+ const onFocusChangeRef = useRef(onFocusChange);
4545
+ onFocusChangeRef.current = onFocusChange;
4546
+ useEffect(() => store.subscribeFocus((id) => onFocusChangeRef.current?.(id)), [store]);
4547
+ const [announcement, setAnnouncement] = useState("");
4548
+ const announce = useCallback((message) => setAnnouncement(message), []);
4549
+ const nodeDescriptionId = useId();
4550
+ const onNodesChangeRef = useRef(onNodesChange);
4551
+ onNodesChangeRef.current = onNodesChange;
4552
+ const onEdgesChangeRef = useRef(onEdgesChange);
4553
+ onEdgesChangeRef.current = onEdgesChange;
4554
+ const hasNodeChange = Boolean(onNodesChange);
4555
+ const hasEdgeChange = Boolean(onEdgesChange);
4556
+ useEffect(() => {
4557
+ store.setChangeHandlers({
4558
+ nodes: hasNodeChange ? (changes) => onNodesChangeRef.current?.(changes) : void 0,
4559
+ edges: hasEdgeChange ? (changes) => onEdgesChangeRef.current?.(changes) : void 0
4560
+ });
4561
+ return () => store.setChangeHandlers({});
4562
+ }, [store, hasNodeChange, hasEdgeChange]);
4563
+ const isControlled = controlledNodes !== void 0 || controlledEdges !== void 0;
4564
+ useEffect(() => {
4565
+ if (!isControlled || store.isNodeDragging() || store.isNodeResizing()) return;
4566
+ store.reconcile(
4567
+ controlledNodes ?? [...store.nodes.values()],
4568
+ controlledEdges ?? [...store.edges.values()]
4569
+ );
4570
+ }, [store, isControlled, controlledNodes, controlledEdges]);
4571
+ const swimlanes = useSwimlanes({
4572
+ store,
4573
+ swimlane,
4574
+ swimlaneMargin: cfg.swimlaneMargin,
4575
+ swimlaneResizable,
4576
+ clampToSwimlane,
4577
+ swimlaneBottomReveal,
4578
+ onSwimlaneSizeChange
4579
+ });
4580
+ const handleContextMenu = useCallback(
4581
+ (event) => {
4582
+ if (event.target instanceof Element && event.target.closest(SELECTOR.node)) return;
4583
+ const viewport = store.getViewport();
4584
+ const rect = paneRef.current?.getBoundingClientRect();
4585
+ const point = { x: event.clientX - (rect?.left ?? 0), y: event.clientY - (rect?.top ?? 0) };
4586
+ if (swimlanes.groups.length > 0) {
4587
+ const hit = swimlaneChromeAt(
4588
+ swimlanes.groups,
4589
+ viewport,
4590
+ point,
4591
+ swimlaneHeaderHeight,
4592
+ SWIMLANE_LABEL_WIDTH
4593
+ );
4594
+ if (hit?.type === "group") return onSwimlaneGroupHeaderContextMenu?.(hit.id, event);
4595
+ if (hit?.type === "lane") return onSwimlaneHeaderContextMenu?.(hit.id, event);
4596
+ }
4597
+ const world = screenToWorld(point, viewport);
4598
+ const lane = swimlanes.lanes.find(
4599
+ (l) => world.y >= l.top && world.y <= l.top + l.height && world.x >= l.x
4600
+ ) ?? null;
4601
+ onPaneContextMenu?.(event, { world, lane });
4602
+ },
4603
+ [
4604
+ store,
4605
+ swimlanes.groups,
4606
+ swimlanes.lanes,
4607
+ swimlaneHeaderHeight,
4608
+ onPaneContextMenu,
4609
+ onSwimlaneHeaderContextMenu,
4610
+ onSwimlaneGroupHeaderContextMenu
4611
+ ]
4612
+ );
4613
+ const paneRef = useRef(null);
4614
+ const nodeContainerRef = useRef(null);
4615
+ const backgroundCanvasRef = useRef(null);
4616
+ const edgeCanvasRef = useRef(null);
4617
+ const foregroundCanvasRef = useRef(null);
4618
+ const { visibleIds, controls, getViewportElements } = useRivetRuntime({
4619
+ store,
4620
+ paneRef,
4621
+ nodeContainerRef,
4622
+ backgroundCanvasRef,
4623
+ edgeCanvasRef,
4624
+ foregroundCanvasRef,
4625
+ swimlaneLanes: swimlanes.lanes,
4626
+ edgeTypes: cfg.edgeTypes,
4627
+ defaultEdgeOptions: cfg.defaultEdgeOptions,
4628
+ anchorOptions: cfg.anchorOptions,
4629
+ edgeRenderer: renderer,
4630
+ edgesReconnectable,
4631
+ isValidConnection: cfg.isValidConnection,
4632
+ mapConnection: mapConnectionWithAnchors,
4633
+ onReconnect,
4634
+ onReconnectStart,
4635
+ onReconnectEnd,
4636
+ panButtons: cfg.panButtons,
4637
+ selectionOnDrag,
4638
+ selectionKeys: cfg.selectionKeys,
4639
+ multiSelectionKeys: cfg.multiSelectionKeys,
4640
+ scrollToPan,
4641
+ zoomSpeed,
4642
+ minZoom,
4643
+ maxZoom,
4644
+ gridGap
4645
+ });
4646
+ const contextValue = useMemo(
4647
+ () => ({
4648
+ store,
4649
+ nodeTypes: cfg.nodeTypes,
4650
+ paneRef,
4651
+ controls,
4652
+ getViewportElements,
4653
+ multiSelectionKeys: cfg.multiSelectionKeys,
4654
+ announce,
4655
+ nodeDescriptionId,
4656
+ defaultEdgeOptions: cfg.defaultEdgeOptions,
4657
+ snapGrid: cfg.snapGrid,
4658
+ alignmentGuides,
4659
+ anchorOptions: cfg.anchorOptions,
4660
+ swimlaneGroups: swimlanes.groups,
4661
+ swimlaneLanes: swimlanes.lanes,
4662
+ clampToSwimlane: swimlanes.clampEnabled,
4663
+ swimlaneMargin: swimlanes.margin,
4664
+ swimlaneLabelWidth: SWIMLANE_LABEL_WIDTH,
4665
+ swimlaneHeaderSize: swimlaneHeaderHeight,
4666
+ swimlaneResizable: swimlanes.resizableEnabled,
4667
+ resizeSwimlane: swimlanes.resizeSwimlane,
4668
+ renderSwimlaneHeader: cfg.renderSwimlaneHeader,
4669
+ renderSwimlaneLabel: cfg.renderSwimlaneLabel,
4670
+ onLaneChange: cfg.onLaneChange,
4671
+ isValidConnection: cfg.isValidConnection,
4672
+ mapConnection: mapConnectionWithAnchors,
4673
+ onConnect: cfg.onConnect
4674
+ }),
4675
+ [
4676
+ store,
4677
+ cfg.nodeTypes,
4678
+ controls,
4679
+ getViewportElements,
4680
+ cfg.multiSelectionKeys,
4681
+ announce,
4682
+ nodeDescriptionId,
4683
+ cfg.defaultEdgeOptions,
4684
+ cfg.snapGrid,
4685
+ alignmentGuides,
4686
+ cfg.anchorOptions,
4687
+ swimlanes.groups,
4688
+ swimlanes.lanes,
4689
+ swimlanes.clampEnabled,
4690
+ swimlanes.margin,
4691
+ swimlaneHeaderHeight,
4692
+ swimlanes.resizableEnabled,
4693
+ swimlanes.resizeSwimlane,
4694
+ cfg.renderSwimlaneHeader,
4695
+ cfg.renderSwimlaneLabel,
4696
+ cfg.onLaneChange,
4697
+ cfg.isValidConnection,
4698
+ mapConnectionWithAnchors,
4699
+ cfg.onConnect
4700
+ ]
4701
+ );
4702
+ return /* @__PURE__ */ jsx(RivetContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
4703
+ "div",
4704
+ {
4705
+ ref: paneRef,
4706
+ className,
4707
+ style: { ...paneStyle, ...style },
4708
+ onContextMenu: handleContextMenu,
4709
+ role: "application",
4710
+ "aria-label": ariaLabel ?? "Node graph",
4711
+ children: [
4712
+ /* @__PURE__ */ jsx("canvas", { ref: backgroundCanvasRef, style: canvasStyle }),
4713
+ /* @__PURE__ */ jsx("canvas", { ref: edgeCanvasRef, style: canvasStyle }),
4714
+ /* @__PURE__ */ jsx(NodeLayer, { containerRef: nodeContainerRef, visibleIds }),
4715
+ /* @__PURE__ */ jsx("canvas", { ref: foregroundCanvasRef, style: canvasStyle }),
4716
+ /* @__PURE__ */ jsx(EdgeLabelLayer, {}),
4717
+ /* @__PURE__ */ jsx(SwimlaneOverlay, {}),
4718
+ children,
4719
+ /* @__PURE__ */ jsx("div", { id: nodeDescriptionId, style: srOnlyStyle, children: NODE_KEYBOARD_INSTRUCTIONS }),
4720
+ /* @__PURE__ */ jsx("div", { style: srOnlyStyle, role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement })
4721
+ ]
4722
+ }
4723
+ ) });
4724
+ }
4725
+ var clipboards = /* @__PURE__ */ new WeakMap();
4726
+ var PASTE_OFFSET = { x: 24, y: 24 };
4727
+ function buildInstance(store, controls, getViewportElements) {
4728
+ const getNodes = () => [...store.nodes.values()];
4729
+ const getEdges = () => [...store.edges.values()];
4730
+ const setNodes = (arg) => {
4731
+ const next = typeof arg === "function" ? arg(getNodes()) : arg;
4732
+ const nextIds = new Set(next.map((node) => node.id));
4733
+ for (const current of getNodes()) {
4734
+ if (!nextIds.has(current.id)) store.removeNode(current.id);
4735
+ }
4736
+ for (const node of next) {
4737
+ if (store.nodes.has(node.id)) store.replaceNode(node.id, node);
4738
+ else store.addNode(node);
4739
+ }
4740
+ };
4741
+ const setEdges = (arg) => {
4742
+ const next = typeof arg === "function" ? arg(getEdges()) : arg;
4743
+ const nextIds = new Set(next.map((edge) => edge.id));
4744
+ for (const current of getEdges()) {
4745
+ if (!nextIds.has(current.id)) store.removeEdge(current.id);
4746
+ }
4747
+ for (const edge of next) {
4748
+ if (store.edges.has(edge.id)) store.replaceEdge(edge.id, edge);
4749
+ else store.addEdge(edge);
4750
+ }
4751
+ };
4752
+ const snapshot = (ids) => {
4753
+ const nodes = ids.map((id) => store.nodes.get(id)).filter((node) => node !== void 0).map((node) => ({ ...node, position: { ...node.position } }));
4754
+ const idSet = new Set(nodes.map((node) => node.id));
4755
+ const edges = getEdges().filter((edge) => idSet.has(edge.source) && idSet.has(edge.target)).map((edge) => ({ ...edge }));
4756
+ return { nodes, edges };
4757
+ };
4758
+ const insert = (source, offset) => {
4759
+ const cloned = cloneElements(source.nodes, source.edges, { offset });
4760
+ for (const node of cloned.nodes) store.addNode(node);
4761
+ for (const edge of cloned.edges) store.addEdge(edge);
4762
+ store.selectNodes(cloned.nodes.map((node) => node.id));
4763
+ return cloned;
4764
+ };
4765
+ return {
4766
+ ...controls,
4767
+ getViewportElements,
4768
+ toObject: () => serializeGraph(
4769
+ store.nodes.values(),
4770
+ store.edges.values(),
4771
+ store.getViewport()
4772
+ ),
4773
+ getNodes,
4774
+ getNode: (id) => store.nodes.get(id),
4775
+ setNodes,
4776
+ addNodes: (nodes) => {
4777
+ for (const node of Array.isArray(nodes) ? nodes : [nodes]) store.addNode(node);
4778
+ },
4779
+ updateNode: (id, patch) => {
4780
+ const current = store.nodes.get(id);
4781
+ if (!current) return;
4782
+ store.replaceNode(id, typeof patch === "function" ? patch(current) : patch);
4783
+ },
4784
+ getEdges,
4785
+ getEdge: (id) => store.edges.get(id),
4786
+ setEdges,
4787
+ addEdges: (edges) => {
4788
+ for (const edge of Array.isArray(edges) ? edges : [edges]) store.addEdge(edge);
4789
+ },
4790
+ updateEdge: (id, patch) => {
4791
+ const current = store.edges.get(id);
4792
+ if (!current) return;
4793
+ store.replaceEdge(id, typeof patch === "function" ? patch(current) : patch);
4794
+ },
4795
+ deleteElements: ({ nodes, edges }) => {
4796
+ for (const id of edges ?? []) store.removeEdge(id);
4797
+ for (const id of nodes ?? []) store.removeNode(id);
4798
+ },
4799
+ registerAnchor: (nodeId, anchorId, element, options) => {
4800
+ store.registerAnchor(nodeId, anchorId, element, options);
4801
+ return {
4802
+ rebind: (el) => el === null ? store.detachAnchor(nodeId, anchorId) : store.registerAnchor(nodeId, anchorId, el, options),
4803
+ unregister: () => store.unregisterAnchor(nodeId, anchorId)
4804
+ };
4805
+ },
4806
+ unregisterAnchor: (nodeId, anchorId) => store.unregisterAnchor(nodeId, anchorId),
4807
+ remeasureAnchors: (nodeId) => store.remeasureAnchors(nodeId),
4808
+ copy: (ids) => {
4809
+ const targetIds = ids ?? store.getSelectedNodes();
4810
+ if (targetIds.length === 0) return;
4811
+ clipboards.set(store, snapshot(targetIds));
4812
+ },
4813
+ cut: (ids) => {
4814
+ const targetIds = ids ?? store.getSelectedNodes();
4815
+ if (targetIds.length === 0) return;
4816
+ clipboards.set(store, snapshot(targetIds));
4817
+ for (const id of targetIds) store.removeNode(id);
4818
+ },
4819
+ paste: (options) => {
4820
+ const clip = clipboards.get(store);
4821
+ if (!clip || clip.nodes.length === 0) return;
4822
+ return insert(clip, options?.offset ?? PASTE_OFFSET);
4823
+ },
4824
+ duplicate: (ids) => {
4825
+ const targetIds = ids ?? store.getSelectedNodes();
4826
+ if (targetIds.length === 0) return;
4827
+ return insert(snapshot(targetIds), PASTE_OFFSET);
4828
+ },
4829
+ undo: () => store.undo(),
4830
+ redo: () => store.redo(),
4831
+ canUndo: () => store.canUndo(),
4832
+ canRedo: () => store.canRedo(),
4833
+ clearHistory: () => store.clearHistory()
4834
+ };
4835
+ }
4836
+ function useRivet() {
4837
+ const { store, controls, getViewportElements } = useRivetContext();
4838
+ return useMemo(
4839
+ () => buildInstance(store, controls, getViewportElements),
4840
+ [store, controls, getViewportElements]
4841
+ );
4842
+ }
4843
+
4844
+ // src/index.ts
4845
+ var VERSION = "0.0.0";
4846
+
4847
+ export { BUILTIN_EDGE_TYPES, BUILTIN_NODE_TYPES, Canvas2DEdgeRenderer, Controls, DEFAULT_ALIGNMENT_HYSTERESIS, DefaultNode, GroupNode, Handle, MiniMap, NodeResizer, Rivet, VERSION, alignRect, applyEdgeChanges, applyNodeChanges, boundingRect, canvas2DEdgeRenderer, clampChildToParent, cloneElements, createRivetStore, edgeEndpointHandle, facingSide, getBezierPath, getSmoothStepPath, getStepPath, getStraightPath, handleKey, parseEdgeEndpoint, parseSideHandleId, rectsIntersect, resolveFacingSide, screenToWorld, serializeGraph, snapToGrid, useRivet, viewportToCss, visibleWorldRect, worldToScreen, zoomAt };
4848
+ //# sourceMappingURL=index.js.map
4849
+ //# sourceMappingURL=index.js.map