@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/LICENSE +674 -0
- package/README.md +42 -0
- package/dist/index.cjs +1648 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1612 -0
- package/dist/index.mjs +323 -0
- package/package.json +36 -0
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
|
+
};
|