@3plate/graph-core 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,1612 @@
1
+ // src/graph.js
2
+ import { Map as IMap, List as IList, Set as ISet4 } from "immutable";
3
+
4
+ // src/options.js
5
+ var defaultOptions = {
6
+ mergeOrder: ["target", "source"],
7
+ nodeMargin: 15,
8
+ dummyNodeSize: 15,
9
+ defaultPortOffset: 20,
10
+ nodeAlign: "natural",
11
+ // 'natural' || 'top' || 'bottom' || 'left' || 'right'
12
+ edgeSpacing: 10,
13
+ turnRadius: 10,
14
+ graphPadding: 20,
15
+ orientation: "TB",
16
+ layerMargin: 5,
17
+ alignIterations: 5,
18
+ alignThreshold: 10,
19
+ separateTrackSets: true
20
+ };
21
+
22
+ // src/graph-nodes.js
23
+ import { Set as ISet } from "immutable";
24
+
25
+ // src/log.js
26
+ import winston, { loggers } from "winston";
27
+
28
+ // src/vitest-transport.js
29
+ import Transport from "winston-transport";
30
+ var VitestTransport = class extends Transport {
31
+ constructor(opts) {
32
+ super(opts);
33
+ }
34
+ log(info, callback) {
35
+ setImmediate(() => {
36
+ this.emit("logged", info);
37
+ });
38
+ const message = info[/* @__PURE__ */ Symbol.for("message")] || info.message;
39
+ switch (info.level) {
40
+ case "error":
41
+ console.error(message);
42
+ break;
43
+ case "warn":
44
+ console.warn(message);
45
+ break;
46
+ case "info":
47
+ console.info(message);
48
+ break;
49
+ default:
50
+ console.log(message);
51
+ }
52
+ callback();
53
+ }
54
+ };
55
+
56
+ // src/log.js
57
+ var { combine, timestamp, printf, colorize, align } = winston.format;
58
+ var format;
59
+ var transports;
60
+ switch (process.env.NODE_ENV) {
61
+ case "development":
62
+ case "test":
63
+ format = combine(
64
+ colorize(),
65
+ align(),
66
+ printf((info) => `${info.level}: ${info.module ?? "core"}: ${info.message}`)
67
+ );
68
+ transports = process.env.VITEST ? [new VitestTransport()] : [new winston.transports.Console()];
69
+ break;
70
+ default:
71
+ format = combine(
72
+ timestamp({
73
+ format: "YYYY-MM-DD hh:mm:ss.SSS A"
74
+ }),
75
+ json()
76
+ );
77
+ transports = [new winston.transports.Console()];
78
+ break;
79
+ }
80
+ var log = winston.createLogger({
81
+ level: process.env.LOG_LEVEL || "warn",
82
+ format,
83
+ transports
84
+ });
85
+ function logger(module) {
86
+ return log.child({ module });
87
+ }
88
+
89
+ // src/graph-nodes.js
90
+ var log2 = logger("nodes");
91
+ var GraphNodes = {
92
+ /**
93
+ * Add a node to the graph
94
+ *
95
+ * @param {Object} props - Node properties
96
+ * @param {string} props.id - Node ID (required)
97
+ * @param {string} props.layerId - Node layer ID (optional)
98
+ * @returns {Object} The added node
99
+ */
100
+ _addNode(props) {
101
+ const node = {
102
+ ...props,
103
+ edges: { in: ISet(), out: ISet() },
104
+ segs: { in: ISet(), out: ISet() },
105
+ aligned: {}
106
+ };
107
+ if (!node.layerId)
108
+ node.layerId = this._layerAtIndex(0).id;
109
+ this._layerAddNode(node.layerId, node.id);
110
+ this.nodes.set(node.id, node);
111
+ this._markDirty(node.id);
112
+ return node;
113
+ },
114
+ /**
115
+ * Add a dummy node to the graph
116
+ *
117
+ * @param {Object} props - Node properties
118
+ * @param {string} props.layerId - Node layer ID (optional)
119
+ * @returns {Object} The added node
120
+ */
121
+ _addDummy(props) {
122
+ return this._addNode({
123
+ ...props,
124
+ id: this._newDummyId(),
125
+ isDummy: true,
126
+ dims: {
127
+ width: this.options.dummyNodeSize,
128
+ height: this.options.dummyNodeSize
129
+ }
130
+ });
131
+ },
132
+ /**
133
+ * Remove a node from the graph
134
+ *
135
+ * @param {string} nodeId - Node ID
136
+ */
137
+ _deleteNode(nodeId) {
138
+ log2.debug(`deleting node ${nodeId}`);
139
+ const node = this.nodes.get(nodeId);
140
+ if (!node) return;
141
+ this._layerDeleteNode(node.layerId, nodeId);
142
+ for (const relId of this._relIds(nodeId))
143
+ this._deleteRelById(relId);
144
+ this.nodes.delete(nodeId);
145
+ },
146
+ /**
147
+ * Check if a node is unlinked (has no relationships)
148
+ *
149
+ * @param {string} nodeId - Node ID
150
+ * @returns {boolean} True if the node is unlinked, false otherwise
151
+ */
152
+ _nodeIsUnlinked(nodeId) {
153
+ const node = this.nodes.get(nodeId);
154
+ if (!node) return false;
155
+ return node.edges.in.isEmpty() && node.edges.out.isEmpty() && node.segs.in.isEmpty() && node.segs.out.isEmpty();
156
+ },
157
+ /**
158
+ * Generate a new dummy node ID
159
+ *
160
+ * @returns {string} A new dummy node ID
161
+ */
162
+ _newDummyId() {
163
+ return `d:${this.nextDummyId++}`;
164
+ },
165
+ /**
166
+ * Check if an ID is a dummy node ID
167
+ *
168
+ * @param {string} nodeId - ID to check (required)
169
+ * @returns {boolean} True if the ID is a dummy node ID, false otherwise
170
+ */
171
+ _isDummyId(nodeId) {
172
+ return nodeId.startsWith("d:");
173
+ },
174
+ /**
175
+ * Iterate over all node IDs
176
+ *
177
+ * @returns {Iterator} Iterator over node IDs
178
+ */
179
+ *_nodeIds() {
180
+ yield* this.nodes.keySeq();
181
+ }
182
+ };
183
+
184
+ // src/graph-edges.js
185
+ var log3 = logger("edges");
186
+ var GraphEdges = {
187
+ /**
188
+ * Get a relationship (edge or segment) by ID
189
+ *
190
+ * @param {string} relId - Relationship ID
191
+ * @returns {Object} The relationship
192
+ */
193
+ _getRel(relId) {
194
+ return relId.startsWith("e:") ? this.getEdge(relId) : this.getSeg(relId);
195
+ },
196
+ /**
197
+ * Generate the ID of an edge or segment. The ID is in the format
198
+ *
199
+ * {type}:{source.id}[.{source.port}]-[{type}]-{target.id}[.{target.port}]
200
+ *
201
+ * Examples:
202
+ *
203
+ * e:n1--n2
204
+ * e:n1.out1--n2
205
+ * e:n1--n2.in1
206
+ * e:n1.out1--n2.in1
207
+ * e:n1.out1-bold-n2.in1
208
+ *
209
+ * @param {Object} obj - Edge or segment
210
+ * @param {string} type - Type of relationship
211
+ * @returns {string} The relationship ID
212
+ */
213
+ _edgeSegId(obj, type, side = "both") {
214
+ let source = "", target = "";
215
+ if (side == "source" || side == "both") {
216
+ source = obj.source.id;
217
+ if (obj.source.port) source += `.${obj.source.port}`;
218
+ source += "-";
219
+ }
220
+ if (side == "target" || side == "both") {
221
+ target = "-" + obj.target.id;
222
+ if (obj.target.port) target += `.${obj.target.port}`;
223
+ }
224
+ return `${type}:${source}${obj.type || ""}${target}`;
225
+ },
226
+ /**
227
+ * Generate the ID of an edge
228
+ *
229
+ * @param {Object} edge - Edge
230
+ * @returns {string} The edge ID
231
+ */
232
+ _edgeId(edge) {
233
+ return this._edgeSegId(edge, "e");
234
+ },
235
+ /**
236
+ * Generate the ID of a segment
237
+ *
238
+ * @param {Object} seg - Segment
239
+ * @returns {string} The segment ID
240
+ */
241
+ _segId(seg) {
242
+ return this._edgeSegId(seg, "s");
243
+ },
244
+ /**
245
+ * Generate a new layer ID
246
+ *
247
+ * @returns {string} A new layer ID
248
+ */
249
+ _newLayerId() {
250
+ return `l:${this.nextLayerId++}`;
251
+ },
252
+ /**
253
+ * Link a segment to its source and target nodes
254
+ *
255
+ * @param {Object} seg - Segment
256
+ */
257
+ _linkSeg(seg) {
258
+ this._linkObj(seg, "segs");
259
+ },
260
+ /**
261
+ * Link an edge to its source and target nodes
262
+ *
263
+ * @param {Object} edge - Edge
264
+ */
265
+ _linkEdge(edge) {
266
+ this._linkObj(edge, "edges");
267
+ },
268
+ /**
269
+ * Unlink a segment from its source and target nodes
270
+ *
271
+ * @param {Object} seg - Segment
272
+ */
273
+ _unlinkSeg(seg) {
274
+ this._unlinkRel(seg, "segs");
275
+ },
276
+ /**
277
+ * Unlink an edge from its source and target nodes
278
+ *
279
+ * @param {Object} edge - Edge
280
+ */
281
+ _unlinkEdge(edge) {
282
+ this._unlinkRel(edge, "edges");
283
+ },
284
+ /**
285
+ * Link a relationship (edge or segment) to its source and target nodes
286
+ *
287
+ * @param {Object} rel - Relationship
288
+ * @param {string} type - Type of relationship
289
+ */
290
+ _linkObj(rel, type) {
291
+ this._addRel(rel.source.id, rel.id, type, "out");
292
+ this._addRel(rel.target.id, rel.id, type, "in");
293
+ },
294
+ /**
295
+ * Unlink a relationship (edge or segment) from its source and target nodes
296
+ *
297
+ * @param {Object} rel - Relationship
298
+ * @param {string} type - Type of relationship
299
+ */
300
+ _unlinkRel(rel, type) {
301
+ log3.debug(`unlinking rel ${rel.id} from ${rel.source.id} and ${rel.target.id}`);
302
+ this._deleteRel(rel.source.id, rel.id, type, "out");
303
+ this._deleteRel(rel.target.id, rel.id, type, "in");
304
+ },
305
+ /**
306
+ * Modify a relationship (edge or segment) in the graph.
307
+ * Either adds or deletes the relation from the appropriate
308
+ * immutable set on the node.
309
+ *
310
+ * @param {string} nodeId - Node ID
311
+ * @param {string} relId - Relationship ID
312
+ * @param {string} type - Type of relationship
313
+ * @param {string} dir - Direction of relationship
314
+ * @param {string} op - Operation (add or delete)
315
+ */
316
+ _modRel(nodeId, relId, type, dir, op) {
317
+ log3.debug(`${op} rel ${relId} on ${nodeId} ${type} ${dir}`);
318
+ let node = this.getNode(nodeId);
319
+ let sets = node[type];
320
+ let set = sets[dir];
321
+ const exists = set.has(relId);
322
+ if (op == "add" && exists) return;
323
+ else if (op == "delete" && !exists) return;
324
+ set = set[op](relId);
325
+ sets = { ...sets, [dir]: set };
326
+ node = { ...node, [type]: sets };
327
+ this.nodes.set(nodeId, node);
328
+ this._markDirty(nodeId);
329
+ },
330
+ /**
331
+ * Add a relationship (edge or segment) to its source and target nodes
332
+ *
333
+ * @param {string} nodeId - Node ID
334
+ * @param {string} relId - Relationship ID
335
+ * @param {string} type - Type of relationship
336
+ * @param {string} dir - Direction of relationship
337
+ */
338
+ _addRel(nodeId, relId, type, dir) {
339
+ this._modRel(nodeId, relId, type, dir, "add");
340
+ },
341
+ /**
342
+ * Delete a relationship (edge or segment) from its source and target nodes
343
+ *
344
+ * @param {string} nodeId - Node ID
345
+ * @param {string} relId - Relationship ID
346
+ * @param {string} type - Type of relationship
347
+ * @param {string} dir - Direction of relationship
348
+ */
349
+ _deleteRel(nodeId, relId, type, dir) {
350
+ this._modRel(nodeId, relId, type, dir, "delete");
351
+ },
352
+ /**
353
+ * Add a new edge to the graph and link it to its source and target nodes
354
+ *
355
+ * @param {Object} props - Edge properties
356
+ * @returns {Object} The edge object
357
+ */
358
+ _addEdge(props) {
359
+ const edge = {
360
+ ...props,
361
+ id: this._edgeId(props),
362
+ segs: []
363
+ };
364
+ this.edges.set(edge.id, edge);
365
+ this._linkEdge(edge);
366
+ return edge;
367
+ },
368
+ /**
369
+ * Remove an edge from the graph and unlink it from its source and target nodes.
370
+ * Also remove it from the edge list of any segments it includes. Any segments
371
+ * that become unused are deleted.
372
+ *
373
+ * @param {string} edgeId - Edge ID
374
+ */
375
+ _deleteEdge(edgeId) {
376
+ log3.debug(`deleting edge ${edgeId}`);
377
+ const edge = this.edges.get(edgeId);
378
+ if (!edge) return;
379
+ log3.debug(`unlinking edge ${edgeId}`);
380
+ this._unlinkEdge(edge);
381
+ for (const segId of edge.segs)
382
+ this._segDeleteEdge(segId, edgeId);
383
+ this.edges.delete(edgeId);
384
+ },
385
+ /**
386
+ * Add a new segment to the graph and link it to its source and target nodes
387
+ *
388
+ * @param {Object} props - Segment properties
389
+ * @returns {Object} The segment object
390
+ */
391
+ _addSeg(props) {
392
+ const seg = {
393
+ ...props,
394
+ id: this._segId(props)
395
+ };
396
+ this.segs.set(seg.id, seg);
397
+ this._linkSeg(seg);
398
+ return seg;
399
+ },
400
+ /**
401
+ * Remove a segment from the graph and unlink it from its source and target nodes.
402
+ * If a source or target is a dummy node and becomes unlinked (no segments), delete it.
403
+ *
404
+ * @param {string} segId - Segment ID
405
+ */
406
+ _deleteSeg(segId) {
407
+ const seg = this.segs.get(segId);
408
+ if (!seg) return;
409
+ this._unlinkSeg(seg);
410
+ this.segs.delete(segId);
411
+ for (const side of ["source", "target"]) {
412
+ const node = this.getNode(seg[side].id);
413
+ if (node.isDummy && this._nodeIsUnlinked(node.id))
414
+ this._deleteNode(node.id);
415
+ }
416
+ },
417
+ /**
418
+ * Remove a relationship (edge or segment) from the graph and unlink it from its source and target nodes
419
+ *
420
+ * @param {string} relId - Relationship ID
421
+ */
422
+ _deleteRelById(relId) {
423
+ if (relId.startsWith("e:"))
424
+ this._deleteEdge(relId);
425
+ else
426
+ this._deleteSeg(relId);
427
+ },
428
+ /**
429
+ * Return an iterator over the relationships (edges and segments) of a node.
430
+ *
431
+ * @param {string} nodeId - Node ID
432
+ * @param {string} type - Type of relationship (defaults to 'both')
433
+ * @param {string} dir - Direction of relationship (defaults to 'both')
434
+ * @returns {Iterator} Iterator over the relationships
435
+ */
436
+ *_relIds(nodeId, type = "both", dir = "both") {
437
+ const node = this.getNode(nodeId);
438
+ const types = type == "both" ? ["edges", "segs"] : [type];
439
+ const dirs = dir == "both" ? ["in", "out"] : [dir];
440
+ for (const type2 of types)
441
+ for (const dir2 of dirs)
442
+ yield* node[type2][dir2];
443
+ },
444
+ /**
445
+ * Return an iterator over the relationships (edges and segments) of a node.
446
+ *
447
+ * @param {string} nodeId - Node ID
448
+ * @param {string} type - Type of relationship (defaults to 'both')
449
+ * @param {string} dir - Direction of relationship (defaults to 'both')
450
+ * @returns {Iterator} Iterator over the relationships
451
+ */
452
+ *_rels(nodeId, type = "both", dir = "both") {
453
+ for (const relId of this._relIds(nodeId, type, dir))
454
+ yield this._getRel(relId);
455
+ },
456
+ /**
457
+ * Return an iterator over the neighbors of a node.
458
+ *
459
+ * @param {string} nodeId - Node ID
460
+ * @param {string} type - Type of relationship (defaults to 'both')
461
+ * @param {string} dir - Direction of relationship (defaults to 'both')
462
+ * @returns {Iterator} Iterator over the neighbors
463
+ */
464
+ *_adjIds(nodeId, type = "both", dir = "both") {
465
+ const nodeIds = /* @__PURE__ */ new Set();
466
+ if (dir == "both" || dir == "in")
467
+ for (const rel of this._rels(nodeId, type, "in"))
468
+ nodeIds.add(rel.source.id);
469
+ if (dir == "both" || dir == "out")
470
+ for (const rel of this._rels(nodeId, type, "out"))
471
+ nodeIds.add(rel.target.id);
472
+ yield* nodeIds;
473
+ },
474
+ /**
475
+ * Return an iterator over the neighbors of a node.
476
+ *
477
+ * @param {string} nodeId - Node ID
478
+ * @param {string} type - Type of relationship (defaults to 'both')
479
+ * @param {string} dir - Direction of relationship (defaults to 'both')
480
+ */
481
+ *_adjs(nodeId, type = "both", dir = "both") {
482
+ for (const adjId of this._adjIds(nodeId, type, dir))
483
+ yield this.getNode(adjId);
484
+ },
485
+ /**
486
+ * Remove a segment from an edge
487
+ *
488
+ * @param {string} edgeId - Edge ID
489
+ * @param {string} segId - Segment ID
490
+ */
491
+ _edgeDeleteSeg(edgeId, segId) {
492
+ const edge = this.getEdge(edgeId);
493
+ const segs = edge.segs.filter((id) => id == segId);
494
+ this.edges.set(edgeId, { ...edge, segs });
495
+ },
496
+ /**
497
+ * Remove an edge from a segment and delete the segment if it becomes empty.
498
+ *
499
+ * @param {string} segId - Segment ID
500
+ * @param {string} edgeId - Edge ID
501
+ */
502
+ _segDeleteEdge(segId, edgeId) {
503
+ const seg = this.getSeg(segId);
504
+ const edges = seg.edges.remove(edgeId);
505
+ if (edges.size == 0)
506
+ this._deleteSeg(segId);
507
+ else
508
+ this.segs.set(segId, { ...seg, edges });
509
+ },
510
+ /**
511
+ * Replace a segment in an edge
512
+ *
513
+ * @param {string} edgeId - Edge ID
514
+ * @param {string} oldSegId - Old segment ID
515
+ * @param {string} newSegId - New segment ID
516
+ */
517
+ _edgeReplaceSeg(edgeId, oldSegId, newSegId) {
518
+ log3.debug(`edge ${edgeId}: replacing segment ${oldSegId} with ${newSegId}`);
519
+ this._segDeleteEdge(oldSegId, edgeId);
520
+ const edge = this.getEdge(edgeId);
521
+ const segs = edge.segs.map((id) => id == oldSegId ? newSegId : id);
522
+ this.edges.set(edgeId, { ...edge, segs });
523
+ }
524
+ };
525
+
526
+ // src/graph-layers.js
527
+ import { Set as ISet2, Seq } from "immutable";
528
+ var log4 = logger("layers");
529
+ var GraphLayers = {
530
+ /**
531
+ * Get the index of a node's layer
532
+ *
533
+ * @param {string} nodeId - Node ID
534
+ * @returns {number} The index of the node's layer
535
+ */
536
+ _nodeLayerIndex(nodeId) {
537
+ return this.getLayer(this.getNode(nodeId).layerId).index;
538
+ },
539
+ /**
540
+ * Add a node to a layer.
541
+ *
542
+ * @param {string} layerId - Layer ID
543
+ * @param {string} nodeId - Node ID
544
+ */
545
+ _layerAddNode(layerId, nodeId) {
546
+ const layer = this.getLayer(layerId);
547
+ this.layers.set(layerId, {
548
+ ...layer,
549
+ nodes: layer.nodes.add(nodeId)
550
+ });
551
+ },
552
+ /**
553
+ * Remove a node from a layer.
554
+ *
555
+ * @param {string} layerId - Layer ID
556
+ * @param {string} nodeId - Node ID
557
+ */
558
+ _layerDeleteNode(layerId, nodeId) {
559
+ const layer = this.getLayer(layerId);
560
+ let sorted = layer.sorted;
561
+ if (sorted) {
562
+ const idx = sorted.findIndex((id) => id == nodeId);
563
+ if (idx >= 0) {
564
+ sorted = sorted.filter((id) => id != nodeId);
565
+ for (let i = idx; i < sorted.length; i++) {
566
+ const node = this.getNode(sorted[i]);
567
+ this.nodes.set(sorted[i], { ...node, index: i });
568
+ }
569
+ }
570
+ }
571
+ this.layers.set(layerId, {
572
+ ...layer,
573
+ nodes: layer.nodes.delete(nodeId),
574
+ sorted
575
+ });
576
+ if (this._layerIsEmpty(layerId))
577
+ this._deleteLayer(layerId);
578
+ },
579
+ /**
580
+ * Update layers in two passes:
581
+ *
582
+ * - Move children up or down to just below lowest parent
583
+ * - Move parents down to just above highest child
584
+ *
585
+ * While moving nodes between layers, if any layer becomes empty,
586
+ * remove it from the list; at the end, renumber the remaining layers
587
+ */
588
+ _updateLayers() {
589
+ const stack = [...this._dirtyNodes].filter((id) => {
590
+ const node = this.nodes.get(id);
591
+ if (!node || node.isDummy) return false;
592
+ return true;
593
+ });
594
+ stack.sort((a, b) => this._nodeLayerIndex(b) - this._nodeLayerIndex(a));
595
+ const phase2 = new Set(stack);
596
+ const moved = /* @__PURE__ */ new Set();
597
+ while (stack.length > 0) {
598
+ const id = stack.pop();
599
+ const parentIds = [...this._adjIds(id, "edges", "in")];
600
+ let correctLayer;
601
+ if (parentIds.length == 0) {
602
+ correctLayer = 0;
603
+ } else {
604
+ const maxParent = Seq(parentIds).map((id2) => this._nodeLayerIndex(id2)).max();
605
+ correctLayer = maxParent + 1;
606
+ }
607
+ const curLayer = this._nodeLayerIndex(id);
608
+ if (curLayer != correctLayer) {
609
+ moved.add(id);
610
+ this._moveNodeLayer(id, correctLayer);
611
+ stack.push(...this._adjIds(id, "edges", "out"));
612
+ for (const parentId of parentIds)
613
+ phase2.add(parentId);
614
+ }
615
+ }
616
+ const byLayer = /* @__PURE__ */ new Map();
617
+ const addParent = (nodeId) => {
618
+ let set;
619
+ const layerId = this.getNode(nodeId).layerId;
620
+ if (!byLayer.has(layerId)) {
621
+ set = /* @__PURE__ */ new Set();
622
+ byLayer.set(layerId, set);
623
+ } else {
624
+ set = byLayer.get(layerId);
625
+ }
626
+ set.add(nodeId);
627
+ };
628
+ for (const id of phase2) addParent(id);
629
+ const layerIds = [...byLayer.keys()].sort(
630
+ (a, b) => this.layers.get(b).index - this.layers.get(a).index
631
+ );
632
+ for (const layerId of layerIds) {
633
+ const curLayer = this.layers.get(layerId).index;
634
+ for (const parentId of byLayer.get(layerId)) {
635
+ const children = [...this._adjIds(parentId, "edges", "out")];
636
+ if (children.length == 0) continue;
637
+ const minChild = Seq(children).map((id) => this._nodeLayerIndex(id)).min();
638
+ const correctLayer = minChild - 1;
639
+ if (curLayer != correctLayer) {
640
+ moved.add(parentId);
641
+ this._moveNodeLayer(parentId, correctLayer);
642
+ for (const grandParentId of this._adjIds(parentId, "edges", "in"))
643
+ addParent(grandParentId);
644
+ }
645
+ }
646
+ }
647
+ for (const id of moved)
648
+ for (const edgeId of this._relIds(id, "edges", "both"))
649
+ this._dirtyEdges.add(edgeId);
650
+ for (const edge of this.changes.addedEdges)
651
+ this._dirtyEdges.add(this._edgeId(edge));
652
+ },
653
+ /**
654
+ * Move the node to a new layer, crushing the original layer
655
+ * if it becomes empty
656
+ *
657
+ * @param {string} nodeId - Node ID
658
+ * @param {number} newIndex - New layer index
659
+ */
660
+ _moveNodeLayer(nodeId, newIndex) {
661
+ log4.debug(`moving node ${nodeId} to layer ${newIndex}`);
662
+ const node = this.getNode(nodeId);
663
+ const oldLayerId = node.layerId;
664
+ const newLayerId = this._layerAtIndex(newIndex).id;
665
+ this._layerDeleteNode(oldLayerId, nodeId);
666
+ this._layerAddNode(newLayerId, nodeId);
667
+ this.nodes.set(nodeId, { ...node, layerId: newLayerId });
668
+ },
669
+ /**
670
+ * Get the layer at the given index, creating it if necessary
671
+ *
672
+ * @param {number} index - Layer index
673
+ * @returns {Object} The layer
674
+ */
675
+ _layerAtIndex(index) {
676
+ while (index >= this.layerList.size)
677
+ this._addLayer();
678
+ const layerId = this.layerList.get(index);
679
+ return this.layers.get(layerId);
680
+ },
681
+ /**
682
+ * Add a new layer. The caller should add a node to it so that
683
+ * it's not empty.
684
+ */
685
+ _addLayer() {
686
+ const id = `l:${this.nextLayerId++}`;
687
+ this.layers.set(id, {
688
+ id,
689
+ index: this.layerList.size,
690
+ nodes: ISet2()
691
+ });
692
+ this.layerList.push(id);
693
+ this.dirtyLayers.add(id);
694
+ },
695
+ /**
696
+ * Check if a layer is empty
697
+ *
698
+ * @param {string} layerId - Layer ID
699
+ * @returns {boolean} True if the layer is empty
700
+ */
701
+ _layerIsEmpty(layerId) {
702
+ return this.layers.get(layerId).nodes.size == 0;
703
+ },
704
+ /**
705
+ * Delete a layer and renumber the remaining layers
706
+ *
707
+ * @param {string} layerId - Layer ID
708
+ */
709
+ _deleteLayer(layerId) {
710
+ const layer = this.getLayer(layerId);
711
+ const index = layer.index;
712
+ log4.debug(`deleting layer ${layerId} at index ${index} / ${this.layerList.size}`);
713
+ this.layerList.remove(index);
714
+ this.layers.delete(layerId);
715
+ for (let i = index; i < this.layerList.size; i++) {
716
+ const id = this.layerList.get(i);
717
+ this.layers.set(id, {
718
+ ...this.layers.get(id),
719
+ index: i
720
+ });
721
+ }
722
+ }
723
+ };
724
+
725
+ // src/mutator.js
726
+ var Mutator = class {
727
+ constructor() {
728
+ this.changes = {
729
+ addedNodes: [],
730
+ removedNodes: [],
731
+ addedEdges: [],
732
+ removedEdges: []
733
+ };
734
+ }
735
+ addNode(node) {
736
+ this.changes.addedNodes.push(node);
737
+ }
738
+ addNodes(...nodes) {
739
+ nodes.forEach((node) => this.addNode(node));
740
+ }
741
+ addEdge(edge) {
742
+ this.changes.addedEdges.push(edge);
743
+ }
744
+ addEdges(...edges) {
745
+ edges.forEach((edge) => this.addEdge(edge));
746
+ }
747
+ removeNode(node) {
748
+ if (typeof node == "string")
749
+ this.changes.removedNodes.push({ id: node });
750
+ else
751
+ this.changes.removedNodes.push(node);
752
+ }
753
+ removeNodes(...nodes) {
754
+ nodes.forEach((node) => this.removeNode(node));
755
+ }
756
+ removeEdge(edge) {
757
+ this.changes.removedEdges.push(edge);
758
+ }
759
+ removeEdges(...edges) {
760
+ edges.forEach((edge) => this.removeEdge(edge));
761
+ }
762
+ };
763
+
764
+ // src/graph-api.js
765
+ var GraphAPI = {
766
+ isEmpty() {
767
+ return this.nodes.isEmpty();
768
+ },
769
+ numNodes() {
770
+ return this.nodes.size;
771
+ },
772
+ numEdges() {
773
+ return this.edges.size;
774
+ },
775
+ getNode(nodeId) {
776
+ const node = this.nodes.get(nodeId);
777
+ if (node) return node;
778
+ throw new Error(`cannot find node ${nodeId}`);
779
+ },
780
+ getEdge(edgeId) {
781
+ const edge = this.edges.get(edgeId);
782
+ if (edge) return edge;
783
+ throw new Error(`cannot find edge ${edgeId}`);
784
+ },
785
+ getSeg(segId) {
786
+ const seg = this.segs.get(segId);
787
+ if (seg) return seg;
788
+ throw new Error(`cannot find segment ${segId}`);
789
+ },
790
+ getLayer(layerId) {
791
+ const layer = this.layers.get(layerId);
792
+ if (layer) return layer;
793
+ throw new Error(`cannot find layer ${layerId}`);
794
+ },
795
+ hasNode(nodeId) {
796
+ return this.nodes.has(nodeId);
797
+ },
798
+ hasEdge(edgeId) {
799
+ return this.edges.has(edgeId);
800
+ },
801
+ withMutations(callback) {
802
+ const mut = new Mutator();
803
+ callback(mut);
804
+ return new Graph({ prior: this, changes: mut.changes });
805
+ },
806
+ addNodes(...nodes) {
807
+ return this.withMutations((mutator) => {
808
+ nodes.forEach((node) => mutator.addNode(node));
809
+ });
810
+ },
811
+ addNode(node) {
812
+ return this.withMutations((mutator) => {
813
+ mutator.addNode(node);
814
+ });
815
+ },
816
+ addEdges(...edges) {
817
+ return this.withMutations((mutator) => {
818
+ edges.forEach((edge) => mutator.addEdge(edge));
819
+ });
820
+ },
821
+ addEdge(edge) {
822
+ return this.withMutations((mutator) => {
823
+ mutator.addEdge(edge);
824
+ });
825
+ },
826
+ removeNodes(...nodes) {
827
+ return this.withMutations((mutator) => {
828
+ nodes.forEach((node) => mutator.removeNode(node));
829
+ });
830
+ },
831
+ removeNode(node) {
832
+ return this.withMutations((mutator) => {
833
+ mutator.removeNode(node);
834
+ });
835
+ },
836
+ removeEdges(...edges) {
837
+ return this.withMutations((mutator) => {
838
+ edges.forEach((edge) => mutator.removeEdge(edge));
839
+ });
840
+ },
841
+ removeEdge(edge) {
842
+ return this.withMutations((mutator) => {
843
+ mutator.removeEdge(edge);
844
+ });
845
+ }
846
+ };
847
+
848
+ // src/graph-mutate.js
849
+ var GraphMutate = {
850
+ /**
851
+ * Put the graph in mutate mode, where all the listed
852
+ * stateful (immutable) collections are also put in
853
+ * mutate mode. Within the callback, the collections
854
+ * are modified in place.
855
+ *
856
+ * @param {Function} callback - The callback to run
857
+ */
858
+ _mutate(callback) {
859
+ const state = [
860
+ "nodes",
861
+ "edges",
862
+ "layers",
863
+ "layerList",
864
+ "segs"
865
+ ];
866
+ const mut = () => {
867
+ if (state.length == 0) return callback();
868
+ const name = state.shift();
869
+ this[name] = this[name].withMutations((map) => {
870
+ this[name] = map;
871
+ mut();
872
+ });
873
+ };
874
+ mut();
875
+ },
876
+ /**
877
+ * Update the graph by applying changes and updating
878
+ * the computed graph state.
879
+ */
880
+ _update() {
881
+ if (!this._dirty) return;
882
+ this._mutate(() => {
883
+ this._applyChanges();
884
+ this._checkCycles();
885
+ this._updateLayers();
886
+ this._updateDummies();
887
+ this._mergeDummies();
888
+ this._positionNodes();
889
+ this._alignAll();
890
+ });
891
+ this._dirty = false;
892
+ },
893
+ /**
894
+ * Mark a node as dirty if it exists in the graph.
895
+ *
896
+ * @param {string} nodeId - Node ID
897
+ */
898
+ _markDirty(nodeId) {
899
+ if (this.nodes.has(nodeId))
900
+ this._dirtyNodes.add(nodeId);
901
+ },
902
+ /**
903
+ * Apply node and edge changes to the graph
904
+ */
905
+ _applyChanges() {
906
+ for (const node of this.changes.addedNodes)
907
+ this._addNode(node);
908
+ for (const node of this.changes.removedNodes)
909
+ this._deleteNode(node.id);
910
+ for (const edge of this.changes.addedEdges)
911
+ this._addEdge(edge);
912
+ for (const edge of this.changes.removedEdges)
913
+ this._deleteEdge(edge.id ?? this._edgeId(edge));
914
+ }
915
+ };
916
+
917
+ // src/graph-cycle.js
918
+ var GraphCycle = {
919
+ /**
920
+ * Get the cycle info for a node
921
+ *
922
+ * @param {string} nodeId - Node ID
923
+ * @returns {string} The cycle info
924
+ */
925
+ _cycleInfo(nodeId) {
926
+ return nodeId;
927
+ },
928
+ /**
929
+ * Check for cycles in the graph. If any are detected, throw an error.
930
+ * Depending on the size of the graph and the number of changes, use
931
+ * different algorithms.
932
+ */
933
+ _checkCycles() {
934
+ const totalNodes = this.nodes.size;
935
+ const newStuff = this.changes.addedNodes.length + this.changes.addedEdges.length;
936
+ const changeRatio = newStuff / totalNodes;
937
+ if (changeRatio > 0.2 || totalNodes < 20)
938
+ this._checkCyclesFull();
939
+ else
940
+ this._checkCyclesIncremental();
941
+ },
942
+ /**
943
+ * Use a graph traversal algorithm to check for cycles.
944
+ */
945
+ _checkCyclesFull() {
946
+ const colorMap = /* @__PURE__ */ new Map();
947
+ const parentMap = /* @__PURE__ */ new Map();
948
+ const white = 0, gray = 1, black = 2;
949
+ let start, end;
950
+ const visit = (nodeId2) => {
951
+ colorMap.set(nodeId2, gray);
952
+ for (const nextId of this._adjIds(nodeId2, "edges", "out")) {
953
+ switch (colorMap.get(nextId) ?? white) {
954
+ case gray:
955
+ start = nextId;
956
+ end = nodeId2;
957
+ return true;
958
+ case white:
959
+ parentMap.set(nextId, nodeId2);
960
+ if (visit(nextId)) return true;
961
+ }
962
+ }
963
+ colorMap.set(nodeId2, black);
964
+ return false;
965
+ };
966
+ for (const nodeId2 of this._nodeIds())
967
+ if ((colorMap.get(nodeId2) ?? white) == white) {
968
+ if (visit(nodeId2)) break;
969
+ }
970
+ if (!start) return;
971
+ const cycle = [this._cycleInfo(start)];
972
+ let nodeId = end;
973
+ while (nodeId != start) {
974
+ cycle.push(this._cycleInfo(nodeId));
975
+ nodeId = parentMap.get(nodeId);
976
+ }
977
+ cycle.push(this._cycleInfo(start));
978
+ cycle.reverse();
979
+ const error = new Error(`Cycle detected: ${cycle.join(" \u2192 ")}`);
980
+ error.cycle = cycle;
981
+ throw error;
982
+ },
983
+ /**
984
+ * Check for cycles in the graph incrementally. For each potential
985
+ * new edge, if the source is < the target, there won't be a cycle.
986
+ * Otherwise, check if there is a route from the target to the source;
987
+ * if so, throw an error.
988
+ */
989
+ _checkCyclesIncremental() {
990
+ for (const edge of this.changes.addedEdges) {
991
+ const layer1 = this._nodeLayerIndex(edge.source.id);
992
+ const layer2 = this._nodeLayerIndex(edge.target.id);
993
+ if (layer1 < layer2) continue;
994
+ const route = this._findRoute(edge.target.id, edge.source.id);
995
+ if (!route) continue;
996
+ const cycle = route.map((id) => this._cycleInfo(id));
997
+ cycle.reverse();
998
+ const error = new Error(`Cycle detected: ${cycle.join(" \u2192 ")}`);
999
+ error.cycle = cycle;
1000
+ throw error;
1001
+ }
1002
+ },
1003
+ /**
1004
+ * Find a route from the source to the target.
1005
+ *
1006
+ * @param {string} sourceId - Source node ID
1007
+ * @param {string} targetId - Target node ID
1008
+ * @returns {Array} The route, or null if no route exists
1009
+ */
1010
+ _findRoute(sourceId, targetId) {
1011
+ const parentMap = /* @__PURE__ */ new Map();
1012
+ const queue = [sourceId];
1013
+ const visited = /* @__PURE__ */ new Set([sourceId]);
1014
+ while (queue.length > 0) {
1015
+ const nodeId = queue.shift();
1016
+ if (nodeId == targetId) {
1017
+ const route = [];
1018
+ let currId = targetId;
1019
+ while (currId != sourceId) {
1020
+ route.push(currId);
1021
+ currId = parentMap.get(currId);
1022
+ }
1023
+ route.push(sourceId);
1024
+ route.reverse();
1025
+ return route;
1026
+ }
1027
+ for (const nextId of this._adjIds(nodeId, "edges", "out")) {
1028
+ if (!visited.has(nextId)) {
1029
+ visited.add(nextId);
1030
+ parentMap.set(nextId, nodeId);
1031
+ queue.push(nextId);
1032
+ }
1033
+ }
1034
+ }
1035
+ return null;
1036
+ }
1037
+ };
1038
+
1039
+ // src/graph-dummy.js
1040
+ import { Set as ISet3 } from "immutable";
1041
+ var log5 = logger("dummy");
1042
+ var GraphDummy = {
1043
+ /**
1044
+ * Update dummy nodes and segments. Dummy nodes are inserted along
1045
+ * edges that span multiple layers. Segments are one-hop connections
1046
+ * between adjacent layers. Edges store an array of their segment IDs.
1047
+ * Since segments can be re-used once dummies are merged, the segments
1048
+ * also store a set of the edges that use them.
1049
+ */
1050
+ _updateDummies() {
1051
+ for (const edgeId of this._dirtyEdges) {
1052
+ const edge = this.getEdge(edgeId);
1053
+ const { type } = edge;
1054
+ const sourceLayer = this._nodeLayerIndex(edge.source.id);
1055
+ const targetLayer = this._nodeLayerIndex(edge.target.id);
1056
+ let segIndex = 0;
1057
+ let changed = false;
1058
+ let source = edge.source;
1059
+ const segs = edge.segs;
1060
+ for (let layerIndex = sourceLayer + 1; layerIndex <= targetLayer; layerIndex++) {
1061
+ const layer = this._layerAtIndex(layerIndex);
1062
+ while (true) {
1063
+ const segId = segs[segIndex];
1064
+ let seg = segId ? this.getSeg(segId) : null;
1065
+ const segLayer = seg ? this._nodeLayerIndex(seg.target.id) : null;
1066
+ if (segIndex == segs.length || segLayer > layerIndex) {
1067
+ let target;
1068
+ if (layerIndex == targetLayer) {
1069
+ target = edge.target;
1070
+ } else {
1071
+ const dummy = this._addDummy({
1072
+ edgeId,
1073
+ layerId: layer.id
1074
+ });
1075
+ target = { ...target, id: dummy.id };
1076
+ }
1077
+ seg = this._addSeg({ source, target, type, edges: ISet3([edgeId]) });
1078
+ log5.debug(`edge ${edgeId}: adding segment ${seg.id} from ${source.id} at layer ${layerIndex - 1} to ${target.id} at layer ${layerIndex}`);
1079
+ segs.splice(segIndex, 0, seg.id);
1080
+ changed = true;
1081
+ } else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
1082
+ log5.debug(`edge ${edgeId}: removing segment ${seg.id} from layer ${layerIndex - 1} to layer ${layerIndex}`);
1083
+ this._segDeleteEdge(segId, edgeId);
1084
+ segs.splice(segIndex, 1);
1085
+ changed = true;
1086
+ continue;
1087
+ }
1088
+ source = seg.target;
1089
+ segIndex++;
1090
+ break;
1091
+ }
1092
+ }
1093
+ while (segIndex < segs.length) {
1094
+ log5.debug(`edge ${edgeId}: removing trailing segment ${segs[segIndex]}`);
1095
+ this._segDeleteEdge(segs[segIndex], edgeId);
1096
+ segs.splice(segIndex, 1);
1097
+ changed = true;
1098
+ segIndex++;
1099
+ }
1100
+ if (changed) {
1101
+ log5.debug(`edge ${edgeId}: updated segments to ${segs.join(", ")}`);
1102
+ this.edges.set(edgeId, { ...edge, segs });
1103
+ }
1104
+ }
1105
+ },
1106
+ _mergeDummies() {
1107
+ for (const side of this.options.mergeOrder)
1108
+ this._mergeScan(side);
1109
+ },
1110
+ _mergeScan(side) {
1111
+ let layers = [...this.layerList];
1112
+ if (side == "target") layers.reverse();
1113
+ const dir = side == "source" ? "in" : "out";
1114
+ const altSide = side == "source" ? "target" : "source";
1115
+ const altDir = altSide == "source" ? "in" : "out";
1116
+ log5.debug(`merging dummies by ${side}`);
1117
+ for (const layerId of layers) {
1118
+ let layer = this.layers.get(layerId);
1119
+ const groups = /* @__PURE__ */ new Map();
1120
+ for (const nodeId of layer.nodes) {
1121
+ if (!this._isDummyId(nodeId)) continue;
1122
+ const node = this.getNode(nodeId);
1123
+ if (node.merged) continue;
1124
+ const edge = this.getEdge(node.edgeId);
1125
+ const key = this._edgeSegId(edge, "k", side);
1126
+ if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
1127
+ groups.get(key).add(node);
1128
+ }
1129
+ for (const [key, group] of groups) {
1130
+ if (group.size == 1) continue;
1131
+ const edgeIds = [...group].map((node) => node.edgeId);
1132
+ const dummy = this._addDummy({ edgeIds, layerId, merged: true });
1133
+ let seg;
1134
+ for (const old of group) {
1135
+ for (const segId of this._relIds(old.id, "segs", dir)) {
1136
+ if (!seg) {
1137
+ const example = this.getSeg(segId);
1138
+ seg = this._addSeg({
1139
+ ...example,
1140
+ edges: ISet3([old.edgeId]),
1141
+ [altSide]: { ...example[altSide], id: dummy.id }
1142
+ });
1143
+ }
1144
+ this._edgeReplaceSeg(old.edgeId, segId, seg.id);
1145
+ }
1146
+ }
1147
+ for (const old of group) {
1148
+ for (const segId of this._relIds(old.id, "segs", altDir)) {
1149
+ const example = this.getSeg(segId);
1150
+ const seg2 = this._addSeg({
1151
+ ...example,
1152
+ edges: ISet3([old.edgeId]),
1153
+ [side]: { ...example[side], id: dummy.id }
1154
+ });
1155
+ this._edgeReplaceSeg(old.edgeId, segId, seg2.id);
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+ };
1162
+
1163
+ // src/graph-pos.js
1164
+ import { Seq as Seq2 } from "immutable";
1165
+ var log6 = logger("pos");
1166
+ var GraphPos = {
1167
+ /**
1168
+ * Find the minimum index of incoming edges to a node
1169
+ *
1170
+ * @param {Object} node - Node
1171
+ * @returns {number} The minimum index of incoming edges
1172
+ */
1173
+ _parentIndex(node) {
1174
+ const parents = Seq2(this._adjs(node.id, "segs", "in"));
1175
+ const pidx = parents.map((p) => p.index).min();
1176
+ log6.debug(`node ${node.id}: parent index ${pidx}`);
1177
+ if (pidx !== void 0) return pidx;
1178
+ return node.isDummy ? -Infinity : Infinity;
1179
+ },
1180
+ /**
1181
+ * Compare two nodes based on their parent index and natural ordering
1182
+ *
1183
+ * @param {Object} aId - First node ID
1184
+ * @param {Object} bId - Second node ID
1185
+ * @returns {number} -1, 0, or 1
1186
+ */
1187
+ _compareNodes(aId, bId, pidxs) {
1188
+ const ai = pidxs.get(aId);
1189
+ const bi = pidxs.get(bId);
1190
+ if (ai !== bi) return ai - bi;
1191
+ const a = this.getNode(aId);
1192
+ const b = this.getNode(bId);
1193
+ if (a.isDummy && !b.isDummy) return -1;
1194
+ if (!a.isDummy && b.isDummy) return 1;
1195
+ if (!a.isDummy) return a.id.localeCompare(b.id);
1196
+ const minA = a.edgeId ?? Seq2(a.edgeIds).min();
1197
+ const minB = b.edgeId ?? Seq2(b.edgeIds).min();
1198
+ return minA.localeCompare(minB);
1199
+ },
1200
+ /**
1201
+ * Does a first pass of assigning X and Y positions to nodes.
1202
+ * Nodes in each layer are ordered first by the order of their parents, and
1203
+ * then by comparing their natural ordering. The Y position is assigned based
1204
+ * on the layer index, and the X position is assigned based on the node index
1205
+ * within the layer.
1206
+ */
1207
+ _positionNodes() {
1208
+ var _a;
1209
+ for (const nodeId of this._dirtyNodes) {
1210
+ const node = this.nodes.get(nodeId);
1211
+ if (!node) continue;
1212
+ const layerId = node.layerId;
1213
+ this.dirtyLayers.add(layerId);
1214
+ }
1215
+ let adjustNext = false;
1216
+ for (const layerId of this.layerList) {
1217
+ if (!adjustNext && !this.dirtyLayers.has(layerId)) continue;
1218
+ adjustNext = false;
1219
+ const layer = this.getLayer(layerId);
1220
+ const pidxs = /* @__PURE__ */ new Map();
1221
+ for (const nodeId of layer.nodes)
1222
+ pidxs.set(nodeId, this._parentIndex(this.getNode(nodeId)));
1223
+ const sorted = [...layer.nodes].sort((a, b) => this._compareNodes(a, b, pidxs));
1224
+ if (layer.sorted && sorted.every((nodeId, i) => layer.sorted[i] == nodeId)) continue;
1225
+ this.dirtyLayers.add(layerId);
1226
+ this.layers.set(layerId, { ...layer, sorted });
1227
+ adjustNext = true;
1228
+ let lpos = 0;
1229
+ for (let i = 0; i < sorted.length; i++) {
1230
+ const node = this.getNode(sorted[i]);
1231
+ log6.debug(`node ${node.id}: final index ${i}`);
1232
+ this.nodes.set(node.id, { ...node, index: i, lpos });
1233
+ const size = ((_a = node.dims) == null ? void 0 : _a[this._width]) ?? 0;
1234
+ lpos += size + this.options.nodeMargin;
1235
+ }
1236
+ }
1237
+ },
1238
+ /**
1239
+ * Align the nodes based on either a specified procedure, or a default procedure,
1240
+ * which consists of a number of iterations of:
1241
+ *
1242
+ * - Align children to parents
1243
+ * - Align parents to children
1244
+ * - Compact layout
1245
+ */
1246
+ _alignAll() {
1247
+ if (this.options.layoutSteps !== void 0) {
1248
+ for (const step of this.options.layoutSteps)
1249
+ this[`_${step}`]();
1250
+ } else {
1251
+ for (let i = 0; i < this.options.alignIterations; i++) {
1252
+ let anyChanged = this._alignChildren() || this._alignParents() || this._compact();
1253
+ if (!anyChanged) break;
1254
+ }
1255
+ }
1256
+ return this;
1257
+ },
1258
+ // Align children to their parents.
1259
+ //
1260
+ // - Sweep layers first to last
1261
+ // - Sweep nodes left to right
1262
+ // - Move nodes only to the right
1263
+ // - On overlap, shift the colliding nodes to the right
1264
+ _alignChildren() {
1265
+ return this._alignNodes(false, false, false, "in", false);
1266
+ },
1267
+ // Align parents to their children.
1268
+ //
1269
+ // - Sweep layers last to first
1270
+ // - Sweep nodes right to left
1271
+ // - Move nodes only to the left
1272
+ // - On overlap, abort the shift
1273
+ _alignParents() {
1274
+ return this._alignNodes(true, true, false, "out", true);
1275
+ },
1276
+ /**
1277
+ * Aligns nodes in each layer, attempting to align child nodes
1278
+ * with their parents (or vice versa). If this causes nodes to overlap as
1279
+ * a result, they are pushed to the right (or left, depending on reverseMove).
1280
+ * However, if conservative is true, nodes will only be moved if they would
1281
+ * not cause a collision with another node.
1282
+ *
1283
+ * "Aligned" means that the edge between the nodes is straight. This could mean
1284
+ * the nodes themselves are not aligned, if they have different anchor positions.
1285
+ *
1286
+ * @param {boolean} reverseLayers - Whether to reverse the order of layers
1287
+ * @param {boolean} reverseNodes - Whether to reverse the order of nodes within each layer
1288
+ * @param {boolean} reverseMove - Whether to move nodes to the left or right
1289
+ * @param {'in' | 'out'} dir - Whether to align nodes based on incoming or outgoing edges
1290
+ * @param {boolean} conservative - Whether to move nodes only if they would not cause a collision
1291
+ */
1292
+ _alignNodes(reverseLayers, reverseNodes, reverseMove, dir, conservative) {
1293
+ let layerIds = [...this.layerList];
1294
+ let anyChanged = false;
1295
+ if (reverseLayers) layerIds.reverse();
1296
+ let adjustNext = false;
1297
+ for (const layerId of layerIds) {
1298
+ if (!adjustNext && !this.dirtyLayers.has(layerId)) continue;
1299
+ adjustNext = false;
1300
+ while (true) {
1301
+ let changed = false;
1302
+ const nodeIds = this._sortLayer(layerId, reverseNodes);
1303
+ for (const nodeId of nodeIds) {
1304
+ const { isAligned, pos: newPos, nodeId: otherId } = this._nearestNode(nodeId, dir, reverseMove, !reverseMove);
1305
+ if (isAligned || newPos === void 0) continue;
1306
+ if (this._shiftNode(nodeId, otherId, dir, newPos, reverseMove, conservative)) {
1307
+ changed = true;
1308
+ anyChanged = true;
1309
+ break;
1310
+ }
1311
+ }
1312
+ if (!changed) break;
1313
+ this.dirtyLayers.add(layerId);
1314
+ adjustNext = true;
1315
+ }
1316
+ }
1317
+ return anyChanged;
1318
+ },
1319
+ /**
1320
+ * Sort the nodes of a layer by their position and store
1321
+ * on layer.sorted. Return the sorted array, reversed if requested.
1322
+ *
1323
+ * @param {string} layerId - The ID of the layer to sort
1324
+ * @param {boolean} reverseNodes - Whether to reverse the order of nodes within the layer
1325
+ * @returns {string[]} The sorted array of node IDs
1326
+ */
1327
+ _sortLayer(layerId, reverseNodes) {
1328
+ const layer = this.getLayer(layerId);
1329
+ const sorted = [...layer.nodes];
1330
+ sorted.sort((a, b) => this.getNode(a).lpos - this.getNode(b).lpos);
1331
+ if (!sorted.every((nodeId, i) => layer.sorted[i] == nodeId)) {
1332
+ this.dirtyLayers.add(layerId);
1333
+ this.layers.set(layerId, { ...layer, sorted });
1334
+ for (let i = 0; i < sorted.length; i++) {
1335
+ const node = this.getNode(sorted[i]);
1336
+ if (node.index !== i)
1337
+ this.nodes.set(sorted[i], { ...node, index: i });
1338
+ }
1339
+ }
1340
+ if (reverseNodes)
1341
+ return sorted.toReversed();
1342
+ return sorted;
1343
+ },
1344
+ /**
1345
+ * Find the nearest node in the given relation that is in the correct direction.
1346
+ * If the nearest is already aligned, return isAligned: true. Nearest means that the anchor
1347
+ * positions are close and in the right direction. Returns both the near node
1348
+ * and the position to which the given node should move in order to be aligned.
1349
+ *
1350
+ * @param {string} nodeId - The ID of the node to find the nearest node for
1351
+ * @param {'in' | 'out'} dir - The direction to find the nearest node in
1352
+ * @param {boolean} allowLeft - Whether to allow the nearest node to be to the left of the given node
1353
+ * @param {boolean} allowRight - Whether to allow the nearest node to be to the right of the given node
1354
+ * @returns {{ nodeId: string, pos: number, isAligned: boolean }} The nearest node and the position to which the given node should move
1355
+ */
1356
+ _nearestNode(nodeId, dir, allowLeft, allowRight) {
1357
+ const node = this.getNode(nodeId);
1358
+ let minDist = Infinity;
1359
+ let bestPos, bestNodeId;
1360
+ const mySide = dir == "in" ? "target" : "source";
1361
+ const altSide = dir == "in" ? "source" : "target";
1362
+ for (const seg of this._rels(nodeId, "segs", dir)) {
1363
+ const altId = seg[altSide].id;
1364
+ const myPos = this._anchorPos(seg, mySide)[this._x];
1365
+ const altPos = this._anchorPos(seg, altSide)[this._x];
1366
+ const diff = altPos - myPos;
1367
+ if (diff == 0) return { nodeId: altId, isAligned: true };
1368
+ if (diff < 0 && !allowLeft) continue;
1369
+ if (diff > 0 && !allowRight) continue;
1370
+ const dist = Math.abs(diff);
1371
+ if (dist < minDist) {
1372
+ minDist = dist;
1373
+ bestNodeId = altId;
1374
+ bestPos = node.lpos + diff;
1375
+ }
1376
+ }
1377
+ return { nodeId: bestNodeId, pos: bestPos, isAligned: false };
1378
+ },
1379
+ /**
1380
+ * Get the anchor point for an edge connection on a node
1381
+ *
1382
+ * @param {Object} seg - The segment to get the anchor point for
1383
+ * @param {'source' | 'target'} side - The side of the segment to get the anchor point for
1384
+ * @returns {{ x: number, y: number }} The anchor point
1385
+ */
1386
+ _anchorPos(seg, side) {
1387
+ var _a, _b;
1388
+ const { _x, _y } = this;
1389
+ const nodeId = seg[side].id;
1390
+ const node = this.getNode(nodeId);
1391
+ let p = { [_x]: node.lpos, ...node.pos || {} };
1392
+ let w = ((_a = node.dims) == null ? void 0 : _a[this._width]) ?? 0;
1393
+ let h = ((_b = node.dims) == null ? void 0 : _b[this._height]) ?? 0;
1394
+ if (node.isDummy)
1395
+ return { [_x]: p[_x] + w / 2, [_y]: p[_y] + h / 2 };
1396
+ p[_x] += this._nodePortOffset(nodeId, seg[side].port);
1397
+ if (side == "source" == this._reverse)
1398
+ p[_y] += h;
1399
+ return p;
1400
+ },
1401
+ /**
1402
+ * Return an offset for a node's port; port is optional.
1403
+ *
1404
+ * @param {string} nodeId - Node ID to check
1405
+ * @param {string} port - The port to compute offset for
1406
+ */
1407
+ _nodePortOffset(nodeId, port) {
1408
+ if (!port) return this.options.defaultPortOffset;
1409
+ return this.options.defaultPortOffset;
1410
+ },
1411
+ /**
1412
+ * Shift the node to the given x position, pushing it to the right (or left, depending on reverseMove).
1413
+ * If conservative is true, nodes will only be moved if they would not cause a collision with another node.
1414
+ * If a collision does occur, recursively move collided nodes to find a valid position.
1415
+ * If the shift is successful, this node and the aligned node are linked explicitly; if the aligned
1416
+ * node was already set, it is unlinked first.
1417
+ *
1418
+ * @param {string} nodeId - ID of node to shift
1419
+ * @param {string} alignId - ID of node we're aligning to
1420
+ * @param {'in' | 'out'} dir - Direction of aligned node from this node
1421
+ * @param {number} lpos - Position within layer to shift node to
1422
+ * @param {boolean} reverseMove - Whether to move nodes to the left or right
1423
+ * @param {boolean} conservative - Whether to move nodes only if they would not cause a collision
1424
+ */
1425
+ _shiftNode(nodeId, alignId, dir, lpos, reverseMove, conservative) {
1426
+ var _a, _b;
1427
+ const node = this.getNode(nodeId);
1428
+ if (!conservative)
1429
+ this._markAligned(nodeId, alignId, dir, lpos);
1430
+ const space = this.options.nodeMargin;
1431
+ const nodeWidth = ((_a = node.dims) == null ? void 0 : _a[this._width]) ?? 0;
1432
+ const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1433
+ repeat:
1434
+ for (const otherId of this.getLayer(node.layerId).nodes) {
1435
+ if (otherId == nodeId) continue;
1436
+ const other = this.getNode(otherId);
1437
+ const opos = other.lpos;
1438
+ const otherWidth = ((_b = other.dims) == null ? void 0 : _b[this._width]) ?? 0;
1439
+ const bMin = opos, bMax = opos + otherWidth;
1440
+ if (aMin < bMax && bMin < aMax) {
1441
+ if (conservative) return false;
1442
+ const safePos = reverseMove ? aMin - otherWidth : aMax;
1443
+ this._shiftNode(otherId, void 0, dir, safePos, reverseMove, conservative);
1444
+ continue repeat;
1445
+ }
1446
+ }
1447
+ if (conservative)
1448
+ this._markAligned(nodeId, alignId, dir, lpos);
1449
+ return true;
1450
+ },
1451
+ /**
1452
+ * Mark nodes as aligned, unlinking any existing alignment.
1453
+ *
1454
+ * @param {string} nodeId - Node being aligned
1455
+ * @param {string} otherId - Node we're aligning to
1456
+ * @param {'in' | 'out'} dir - direction of other from node
1457
+ * @param {number} lpos - new layer position
1458
+ */
1459
+ _markAligned(nodeId, otherId, dir, lpos) {
1460
+ const node = this.getNode(nodeId);
1461
+ const alt = dir == "in" ? "out" : "in";
1462
+ if (node.aligned[dir]) {
1463
+ const ex = this.getNode(node.aligned[dir]);
1464
+ this.nodes.set(node.aligned[dir], { ...ex, aligned: { ...ex.aligned, [alt]: void 0 } });
1465
+ }
1466
+ if (otherId) {
1467
+ const other = this.getNode(otherId);
1468
+ this.nodes.set(otherId, { ...other, aligned: { ...other.aligned, [alt]: nodeId } });
1469
+ }
1470
+ this.nodes.set(nodeId, { ...node, lpos, aligned: { [dir]: otherId, [alt]: void 0 } });
1471
+ },
1472
+ /**
1473
+ * Iterate over all nodes aligned with the given node, including itself,
1474
+ * exactly once.
1475
+ *
1476
+ * @param {string} nodeId - Node ID
1477
+ * @param {'in' | 'out' | 'both'} dir - direction of alignment
1478
+ * @returns {Iterator<Object>} Iterator over aligned nodes
1479
+ */
1480
+ *_aligned(nodeId, dir) {
1481
+ const visit = function* (node2, dir2) {
1482
+ const otherId = node2.aligned[dir2];
1483
+ if (!otherId) return;
1484
+ const other = this.getNode(otherId);
1485
+ yield other;
1486
+ yield* visit.call(this, other, dir2);
1487
+ }.bind(this);
1488
+ const node = this.getNode(nodeId);
1489
+ yield node;
1490
+ if (dir == "both") {
1491
+ yield* visit(node, "in");
1492
+ yield* visit(node, "out");
1493
+ } else {
1494
+ yield* visit(node, dir);
1495
+ }
1496
+ },
1497
+ /**
1498
+ * Get the node ID immediately to the left of the given node in the same layer
1499
+ *
1500
+ * @param {Object} node - Node to get left of
1501
+ * @returns {string | null} Node ID to the left of the given node, or null if there is none
1502
+ */
1503
+ _leftOf(node) {
1504
+ if (node.index == 0) return null;
1505
+ return this.getLayer(node.layerId).sorted[node.index - 1];
1506
+ },
1507
+ /**
1508
+ * Get the node id immediately to the right of the given node in the same layer
1509
+ *
1510
+ * @param {Object} node - Node to get right of
1511
+ * @returns {string | null} Node ID to the right of the given node, or null if there is none
1512
+ */
1513
+ _rightOf(node) {
1514
+ const layer = this.getLayer(node.layerId);
1515
+ if (node.index == layer.sorted.length - 1) return null;
1516
+ return layer.sorted[node.index + 1];
1517
+ },
1518
+ /**
1519
+ * Compact tries to eliminate empty space between nodes
1520
+ */
1521
+ _compact() {
1522
+ var _a;
1523
+ let anyChanged = false;
1524
+ for (const layerId of this.layerList) {
1525
+ const layer = this.getLayer(layerId);
1526
+ if (layer.sorted.length < 2) continue;
1527
+ for (const nodeId of layer.sorted) {
1528
+ const node = this.getNode(nodeId);
1529
+ if (node.index == 0) continue;
1530
+ let minGap = Infinity;
1531
+ const stack = [];
1532
+ for (const right of this._aligned(nodeId, "both")) {
1533
+ stack.push(right);
1534
+ const leftId = this._leftOf(right);
1535
+ if (!leftId) return;
1536
+ const left = this.getNode(leftId);
1537
+ const leftWidth = ((_a = left.dims) == null ? void 0 : _a[this._width]) ?? 0;
1538
+ const gap = right.lpos - left.lpos - leftWidth;
1539
+ if (gap < minGap) minGap = gap;
1540
+ }
1541
+ const delta = minGap - this.options.nodeMargin;
1542
+ if (delta <= 0) continue;
1543
+ anyChanged = true;
1544
+ for (const right of stack)
1545
+ this.nodes.set(right.id, { ...right, lpos: right.lpos - delta });
1546
+ }
1547
+ }
1548
+ return anyChanged;
1549
+ }
1550
+ };
1551
+
1552
+ // src/graph.js
1553
+ var Graph = class {
1554
+ constructor({ prior, changes, options, nodes, edges } = {}) {
1555
+ this.nodes = (prior == null ? void 0 : prior.nodes) ?? IMap();
1556
+ this.edges = (prior == null ? void 0 : prior.edges) ?? IMap();
1557
+ this.layers = (prior == null ? void 0 : prior.layers) ?? IMap();
1558
+ this.layerList = (prior == null ? void 0 : prior.layerList) ?? IList();
1559
+ this.segs = (prior == null ? void 0 : prior.segs) ?? IMap();
1560
+ this.nextLayerId = (prior == null ? void 0 : prior.nextLayerId) ?? 0;
1561
+ this.nextDummyId = (prior == null ? void 0 : prior.nextDummyId) ?? 0;
1562
+ this._dirtyNodes = /* @__PURE__ */ new Set();
1563
+ this._dirtyEdges = /* @__PURE__ */ new Set();
1564
+ this.dirtyLayers = /* @__PURE__ */ new Set();
1565
+ this.prior = prior;
1566
+ this.options = {
1567
+ ...defaultOptions,
1568
+ ...(prior == null ? void 0 : prior.options) ?? {},
1569
+ ...options ?? {}
1570
+ };
1571
+ this.changes = changes ?? {
1572
+ addedNodes: [],
1573
+ removedNodes: [],
1574
+ addedEdges: [],
1575
+ removedEdges: []
1576
+ };
1577
+ this.changes.addedNodes.push(...nodes || []);
1578
+ this.changes.addedEdges.push(...edges || []);
1579
+ this._dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1580
+ this._reverse = this.options.orientation === "BT" || this.options.orientation === "RL";
1581
+ this._vertical = this.options.orientation === "TB" || this.options.orientation === "BT";
1582
+ this._height = this._vertical ? "height" : "width";
1583
+ this._width = this._vertical ? "width" : "height";
1584
+ this._x = this._vertical ? "x" : "y";
1585
+ this._y = this._vertical ? "y" : "x";
1586
+ this._d = {
1587
+ x: this._vertical ? 0 : this._reverse ? -1 : 1,
1588
+ y: this._vertical ? this._reverse ? -1 : 1 : 0
1589
+ };
1590
+ const natAligns = { TB: "top", BT: "bottom", LR: "left", RL: "right" };
1591
+ if (this.options.nodeAlign == "natural")
1592
+ this._natural = true;
1593
+ else
1594
+ this._natural = natAligns[this.options.orientation] == this.options.nodeAlign;
1595
+ if (this._dirty) this._update();
1596
+ }
1597
+ };
1598
+ var mixins = [
1599
+ GraphNodes,
1600
+ GraphEdges,
1601
+ GraphLayers,
1602
+ GraphAPI,
1603
+ GraphMutate,
1604
+ GraphCycle,
1605
+ GraphDummy,
1606
+ GraphPos
1607
+ ];
1608
+ for (const mixin of mixins)
1609
+ Object.assign(Graph.prototype, mixin);
1610
+ export {
1611
+ Graph
1612
+ };