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