@3plate/graph-core 0.1.0 → 0.1.4

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 CHANGED
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -26,1044 +27,790 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
27
  ));
27
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
29
 
29
- // src/index.js
30
+ // src/index.ts
30
31
  var index_exports = {};
31
32
  __export(index_exports, {
32
- Graph: () => Graph
33
+ default: () => index_default,
34
+ graph: () => graph
33
35
  });
34
36
  module.exports = __toCommonJS(index_exports);
35
37
 
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
- };
38
+ // src/graph/graph.ts
39
+ var import_immutable8 = require("immutable");
56
40
 
57
- // src/graph-nodes.js
41
+ // src/graph/node.ts
58
42
  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
- }
43
+ var defNodeData = {
44
+ id: "",
45
+ data: void 0,
46
+ version: 0,
47
+ title: void 0,
48
+ text: void 0,
49
+ type: void 0,
50
+ render: void 0,
51
+ ports: { in: null, out: null },
52
+ aligned: {},
53
+ edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
54
+ segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
55
+ layerId: "",
56
+ isDummy: false,
57
+ isMerged: false,
58
+ edgeIds: [],
59
+ index: void 0,
60
+ pos: void 0,
61
+ lpos: void 0,
62
+ dims: void 0,
63
+ mutable: false
89
64
  };
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,
65
+ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
66
+ static dummyPrefix = "d:";
67
+ // get edgeId(): EdgeId {
68
+ // if (!this.isDummy)
69
+ // throw new Error(`node ${this.id} is not a dummy`)
70
+ // if (this.isMerged)
71
+ // throw new Error(`node ${this.id} is merged`)
72
+ // return this.get('edgeIds')[0]
73
+ // }
74
+ // get edgeIds(): EdgeId[] {
75
+ // if (!this.isDummy)
76
+ // throw new Error(`node ${this.id} is not a dummy`)
77
+ // if (!this.isMerged)
78
+ // throw new Error(`node ${this.id} is not merged`)
79
+ // return this.get('edgeIds')
80
+ // }
81
+ get key() {
82
+ return this.isDummy ? this.id : _Node.key(this);
83
+ }
84
+ static key(node) {
85
+ return `${node.id}:${node.version}`;
86
+ }
87
+ static isDummyId(nodeId) {
88
+ return nodeId.startsWith(_Node.dummyPrefix);
89
+ }
90
+ static addNormal(g, data) {
91
+ const layer = g.layerAt(0);
92
+ const node = new _Node({
93
+ ...data,
138
94
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
139
95
  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);
96
+ aligned: {},
97
+ edgeIds: [],
98
+ layerId: layer.id
99
+ });
100
+ layer.addNode(g, node.id);
101
+ g.nodes.set(node.id, node);
102
+ g.dirtyNodes.add(node.id);
147
103
  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(),
104
+ }
105
+ static addDummy(g, data) {
106
+ const layer = g.getLayer(data.layerId);
107
+ const node = new _Node({
108
+ ...data,
109
+ id: `${_Node.dummyPrefix}${g.nextDummyId++}`,
110
+ edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
111
+ segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
112
+ aligned: {},
160
113
  isDummy: true,
161
114
  dims: {
162
- width: this.options.dummyNodeSize,
163
- height: this.options.dummyNodeSize
115
+ w: g.options.dummyNodeSize,
116
+ h: g.options.dummyNodeSize
164
117
  }
165
118
  });
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();
119
+ layer.addNode(g, node.id);
120
+ g.nodes.set(node.id, node);
121
+ g.dirtyNodes.add(node.id);
122
+ return node;
216
123
  }
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);
124
+ static del(g, node) {
125
+ return g.getNode(node.id).delSelf(g);
126
+ }
127
+ static update(g, data) {
128
+ return g.getNode(data.id).mut(g).merge(data);
129
+ }
130
+ mut(g) {
131
+ if (this.mutable) return this;
132
+ return g.mutateNode(this);
133
+ }
134
+ final() {
135
+ if (!this.mutable) return this;
136
+ return this.merge({
137
+ edges: { in: this.edges.in.asImmutable(), out: this.edges.out.asImmutable() },
138
+ segs: { in: this.segs.in.asImmutable(), out: this.segs.out.asImmutable() },
139
+ mutable: false
140
+ }).asImmutable();
141
+ }
142
+ dirty(g) {
143
+ g.dirtyNodes.add(this.id);
144
+ return this;
145
+ }
146
+ cur(g) {
147
+ return g.getNode(this.id);
148
+ }
149
+ isUnlinked() {
150
+ return this.edges.in.size == 0 && this.edges.out.size == 0 && this.segs.in.size == 0 && this.segs.out.size == 0;
151
+ }
152
+ hasPorts() {
153
+ return !!this.ports?.in?.length || !!this.ports?.out?.length;
154
+ }
155
+ layerIndex(g) {
156
+ return this.getLayer(g).index;
157
+ }
158
+ getLayer(g) {
159
+ return g.getLayer(this.layerId);
160
+ }
161
+ setIndex(g, index) {
162
+ if (this.index == index) return this;
163
+ return this.mut(g).set("index", index);
164
+ }
165
+ setLayerPos(g, lpos) {
166
+ if (this.lpos == lpos) return this;
167
+ return this.mut(g).set("lpos", lpos);
168
+ }
169
+ setLayer(g, layerId) {
170
+ if (this.layerId == layerId) return this;
171
+ return this.mut(g).set("layerId", layerId);
172
+ }
173
+ setPos(g, pos) {
174
+ if (!this.pos || this.pos.x != pos.x || this.pos.y != pos.y)
175
+ return this.mut(g).set("pos", pos);
176
+ return this;
177
+ }
178
+ moveToLayer(g, layer) {
179
+ this.getLayer(g).delNode(g, this.id);
180
+ layer.addNode(g, this.id);
181
+ return this.setLayer(g, layer.id);
182
+ }
183
+ moveToLayerIndex(g, index) {
184
+ return this.moveToLayer(g, g.layerAt(index));
185
+ }
186
+ setAligned(g, dir, nodeId) {
187
+ if (this.aligned[dir] === nodeId) return this;
188
+ return this.mut(g).set("aligned", { ...this.aligned, [dir]: nodeId });
189
+ }
190
+ addRel(g, type, dir, relId) {
191
+ const sets = this.get(type);
192
+ const set = sets[dir];
193
+ if (set.has(relId)) return this;
194
+ return this.mut(g).set(type, { ...sets, [dir]: set.asMutable().add(relId) });
195
+ }
196
+ delRel(g, type, dir, relId) {
197
+ let sets = this.get(type);
198
+ const set = sets[dir];
199
+ if (!set.has(relId)) return this;
200
+ sets = { ...sets, [dir]: set.asMutable().remove(relId) };
201
+ const node = this.mut(g).set(type, sets);
202
+ if (node.isDummy && node.isUnlinked())
203
+ return node.delSelf(g);
204
+ return node;
205
+ }
206
+ delSelf(g) {
207
+ this.getLayer(g).delNode(g, this.id);
208
+ for (const rel of this.rels(g))
209
+ rel.delSelf(g);
210
+ g.nodes.delete(this.id);
211
+ g.dirtyNodes.delete(this.id);
212
+ g.delNodes.add(this.id);
213
+ return null;
214
+ }
215
+ addInEdge(g, edgeId) {
216
+ return this.addRel(g, "edges", "in", edgeId);
217
+ }
218
+ addOutEdge(g, edgeId) {
219
+ return this.addRel(g, "edges", "out", edgeId);
220
+ }
221
+ addInSeg(g, segId) {
222
+ return this.addRel(g, "segs", "in", segId);
223
+ }
224
+ addOutSeg(g, segId) {
225
+ return this.addRel(g, "segs", "out", segId);
226
+ }
227
+ delInEdge(g, edgeId) {
228
+ return this.delRel(g, "edges", "in", edgeId);
229
+ }
230
+ delOutEdge(g, edgeId) {
231
+ return this.delRel(g, "edges", "out", edgeId);
232
+ }
233
+ delInSeg(g, segId) {
234
+ return this.delRel(g, "segs", "in", segId);
235
+ }
236
+ delOutSeg(g, segId) {
237
+ return this.delRel(g, "segs", "out", segId);
238
+ }
239
+ *relIds(type = "both", dir = "both") {
473
240
  const types = type == "both" ? ["edges", "segs"] : [type];
474
241
  const dirs = dir == "both" ? ["in", "out"] : [dir];
475
242
  for (const type2 of types)
476
243
  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 });
244
+ yield* this.get(type2)[dir2];
245
+ }
246
+ *rels(g, type = "both", dir = "both") {
247
+ for (const relId of this.relIds(type, dir))
248
+ yield g.getRel(relId);
249
+ }
250
+ *adjIds(g, type = "both", dir = "both") {
251
+ const dirs = dir == "both" ? ["in", "out"] : [dir];
252
+ for (const dir2 of dirs) {
253
+ const side = dir2 == "in" ? "source" : "target";
254
+ for (const rel of this.rels(g, type, dir2))
255
+ yield rel[side].id;
256
+ }
257
+ }
258
+ *adjs(g, type = "both", dir = "both") {
259
+ for (const nodeId of this.adjIds(g, type, dir))
260
+ yield g.getNode(nodeId);
261
+ }
262
+ *inEdgeIds() {
263
+ yield* this.relIds("edges", "in");
264
+ }
265
+ *outEdgeIds() {
266
+ yield* this.relIds("edges", "out");
267
+ }
268
+ *inSegIds() {
269
+ yield* this.relIds("segs", "in");
270
+ }
271
+ *outSegIds() {
272
+ yield* this.relIds("segs", "out");
273
+ }
274
+ *inEdges(g) {
275
+ yield* this.rels(g, "edges", "in");
276
+ }
277
+ *outEdges(g) {
278
+ yield* this.rels(g, "edges", "out");
279
+ }
280
+ *inSegs(g) {
281
+ yield* this.rels(g, "segs", "in");
282
+ }
283
+ *outSegs(g) {
284
+ yield* this.rels(g, "segs", "out");
285
+ }
286
+ *inNodeIds(g) {
287
+ yield* this.adjIds(g, "edges", "in");
288
+ }
289
+ *outNodeIds(g) {
290
+ yield* this.adjIds(g, "edges", "out");
291
+ }
292
+ *inNodes(g) {
293
+ yield* this.adjs(g, "edges", "in");
294
+ }
295
+ *outNodes(g) {
296
+ yield* this.adjs(g, "edges", "out");
558
297
  }
559
298
  };
560
299
 
561
- // src/graph-layers.js
300
+ // src/graph/edge.ts
562
301
  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
- }
302
+
303
+ // src/log.ts
304
+ var levels = {
305
+ error: 0,
306
+ warn: 1,
307
+ info: 2,
308
+ debug: 3
309
+ };
310
+ var currentLevel = "debug";
311
+ function shouldLog(level) {
312
+ return levels[level] <= levels[currentLevel];
313
+ }
314
+ function logger(module2) {
315
+ return {
316
+ error: (msg, ...args) => shouldLog("error") && console.error(`[${module2}] ${msg}`, ...args),
317
+ warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module2}] ${msg}`, ...args),
318
+ info: (msg, ...args) => shouldLog("info") && console.info(`[${module2}] ${msg}`, ...args),
319
+ debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module2}] ${msg}`, ...args)
320
+ };
321
+ }
322
+ var log = logger("core");
323
+
324
+ // src/graph/edge.ts
325
+ var log2 = logger("edge");
326
+ var defEdgeData = {
327
+ id: "",
328
+ data: null,
329
+ label: void 0,
330
+ source: { id: "" },
331
+ target: { id: "" },
332
+ type: void 0,
333
+ style: void 0,
334
+ mutable: false,
335
+ segIds: []
336
+ };
337
+ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
338
+ static prefix = "e:";
339
+ mut(g) {
340
+ if (this.mutable) return this;
341
+ return g.mutateEdge(this);
342
+ }
343
+ final() {
344
+ if (!this.mutable) return this;
345
+ return this.merge({
346
+ mutable: false
347
+ }).asImmutable();
348
+ }
349
+ link(g) {
350
+ this.sourceNode(g).addOutEdge(g, this.id);
351
+ this.targetNode(g).addInEdge(g, this.id);
352
+ return this;
353
+ }
354
+ unlink(g) {
355
+ this.sourceNode(g).delOutEdge(g, this.id);
356
+ this.targetNode(g).delInEdge(g, this.id);
357
+ return this;
358
+ }
359
+ delSelf(g) {
360
+ for (const seg of this.segs(g))
361
+ seg.delEdgeId(g, this.id);
362
+ this.unlink(g);
363
+ g.edges.delete(this.id);
364
+ g.dirtyEdges.delete(this.id);
365
+ g.delEdges.add(this.id);
366
+ return null;
367
+ }
368
+ delSegId(g, segId) {
369
+ return this.setSegIds(g, this.segIds.filter((id) => id != segId));
370
+ }
371
+ replaceSegId(g, oldId, newId) {
372
+ return this.setSegIds(g, this.segIds.map((id) => id == oldId ? newId : id));
373
+ }
374
+ setSegIds(g, segIds) {
375
+ if (segIds.join(",") == this.segIds.join(",")) return this;
376
+ return this.mut(g).set("segIds", segIds);
377
+ }
378
+ *segs(g) {
379
+ for (const segId of this.segIds)
380
+ yield g.getSeg(segId);
381
+ }
382
+ node(g, side) {
383
+ return g.getNode(this[side].id);
384
+ }
385
+ sourceNode(g) {
386
+ return this.node(g, "source");
387
+ }
388
+ targetNode(g) {
389
+ return this.node(g, "target");
390
+ }
391
+ get str() {
392
+ return _Edge.str(this);
393
+ }
394
+ static str(edge) {
395
+ let source = edge.source?.id;
396
+ if (!source) throw new Error("edge source is undefined");
397
+ if (edge.source?.port)
398
+ source = `${source} (port ${edge.source.port})`;
399
+ let target = edge.target?.id;
400
+ if (!target) throw new Error("edge target is undefined");
401
+ if (edge.target?.port)
402
+ target = `${target} (port ${edge.target.port})`;
403
+ let str = `edge from ${source} to ${target}`;
404
+ if (edge.type) str += ` of type ${edge.type}`;
405
+ return str;
406
+ }
407
+ static key(edge, prefix = _Edge.prefix, side = "both") {
408
+ let source = "", target = "";
409
+ if (side == "source" || side == "both") {
410
+ if (!edge.source?.id) throw new Error("edge source is undefined");
411
+ source = edge.source.id;
412
+ if (edge.source?.port)
413
+ source = `${source}.${edge.source.port}`;
414
+ const marker = edge.source?.marker ?? edge.style?.marker?.source;
415
+ if (marker && marker != "none") source += `[${marker}]`;
416
+ source += "-";
650
417
  }
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
- }
418
+ if (side == "target" || side == "both") {
419
+ if (!edge.target?.id) throw new Error("edge target is undefined");
420
+ target = edge.target.id;
421
+ if (edge.target.port)
422
+ target = `${target}.${edge.target.port}`;
423
+ target = "-" + target;
424
+ const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
425
+ if (marker && marker != "none") target += `[${marker}]`;
681
426
  }
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)()
427
+ const type = edge.type || "";
428
+ return `${prefix}${source}${type}${target}`;
429
+ }
430
+ static add(g, data) {
431
+ const edge = new _Edge({
432
+ ...data,
433
+ segIds: []
726
434
  });
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
- });
435
+ edge.link(g);
436
+ g.edges.set(edge.id, edge);
437
+ g.dirtyEdges.add(edge.id);
438
+ return edge;
439
+ }
440
+ static del(g, data) {
441
+ return g.getEdge(data.id).delSelf(g);
442
+ }
443
+ static update(g, data) {
444
+ let edge = g.getEdge(data.id);
445
+ let relink = false;
446
+ if (data.source.id !== edge.source.id || data.target.id !== edge.target.id || data.source.port !== edge.source.port || data.target.port !== edge.target.port || data.type !== edge.type) {
447
+ for (const seg of edge.segs(g))
448
+ seg.delEdgeId(g, edge.id);
449
+ edge.unlink(g);
450
+ relink = true;
756
451
  }
452
+ edge = edge.mut(g).merge(data);
453
+ if (relink)
454
+ edge.link(g);
455
+ return edge;
757
456
  }
758
457
  };
759
458
 
760
- // src/mutator.js
761
- var Mutator = class {
762
- constructor() {
763
- this.changes = {
764
- addedNodes: [],
765
- removedNodes: [],
766
- addedEdges: [],
767
- removedEdges: []
768
- };
459
+ // src/graph/seg.ts
460
+ var import_immutable3 = require("immutable");
461
+ var defSegData = {
462
+ id: "",
463
+ source: { id: "" },
464
+ target: { id: "" },
465
+ type: void 0,
466
+ style: void 0,
467
+ edgeIds: (0, import_immutable3.Set)(),
468
+ trackPos: void 0,
469
+ svg: void 0,
470
+ mutable: false
471
+ };
472
+ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
473
+ static prefix = "s:";
474
+ mut(g) {
475
+ if (this.mutable) return this;
476
+ return g.mutateSeg(this);
769
477
  }
770
- addNode(node) {
771
- this.changes.addedNodes.push(node);
478
+ final() {
479
+ if (!this.mutable) return this;
480
+ return this.merge({
481
+ edgeIds: this.edgeIds.asImmutable(),
482
+ mutable: false
483
+ }).asImmutable();
772
484
  }
773
- addNodes(...nodes) {
774
- nodes.forEach((node) => this.addNode(node));
485
+ get p1() {
486
+ return this.source.pos;
775
487
  }
776
- addEdge(edge) {
777
- this.changes.addedEdges.push(edge);
488
+ get p2() {
489
+ return this.target.pos;
778
490
  }
779
- addEdges(...edges) {
780
- edges.forEach((edge) => this.addEdge(edge));
491
+ anySameEnd(other) {
492
+ return this.sameEnd(other, "source") || this.sameEnd(other, "target");
781
493
  }
782
- removeNode(node) {
783
- if (typeof node == "string")
784
- this.changes.removedNodes.push({ id: node });
785
- else
786
- this.changes.removedNodes.push(node);
494
+ sameEnd(other, side) {
495
+ const mine = this[side];
496
+ const yours = other[side];
497
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
787
498
  }
788
- removeNodes(...nodes) {
789
- nodes.forEach((node) => this.removeNode(node));
499
+ setPos(g, source, target) {
500
+ return this.mut(g).merge({
501
+ source: { ...this.source, pos: source },
502
+ target: { ...this.target, pos: target }
503
+ });
790
504
  }
791
- removeEdge(edge) {
792
- this.changes.removedEdges.push(edge);
505
+ setTrackPos(g, trackPos) {
506
+ if (this.trackPos == trackPos) return this;
507
+ return this.mut(g).set("trackPos", trackPos);
793
508
  }
794
- removeEdges(...edges) {
795
- edges.forEach((edge) => this.removeEdge(edge));
509
+ setSVG(g, svg) {
510
+ if (this.svg == svg) return this;
511
+ return this.mut(g).set("svg", svg);
796
512
  }
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);
513
+ link(g) {
514
+ this.sourceNode(g).addOutSeg(g, this.id);
515
+ this.targetNode(g).addInSeg(g, this.id);
516
+ return this;
517
+ }
518
+ unlink(g) {
519
+ this.sourceNode(g).delOutSeg(g, this.id);
520
+ this.targetNode(g).delInSeg(g, this.id);
521
+ return this;
522
+ }
523
+ delSelf(g) {
524
+ this.unlink(g);
525
+ g.segs.delete(this.id);
526
+ g.dirtySegs.delete(this.id);
527
+ g.delSegs.add(this.id);
528
+ return null;
529
+ }
530
+ *edges(g) {
531
+ for (const edgeId of this.edgeIds)
532
+ yield g.getEdge(edgeId);
533
+ }
534
+ node(g, side) {
535
+ return g.getNode(this[side].id);
536
+ }
537
+ sourceNode(g) {
538
+ return this.node(g, "source");
539
+ }
540
+ targetNode(g) {
541
+ return this.node(g, "target");
542
+ }
543
+ addEdgeId(g, edgeId) {
544
+ if (this.edgeIds.has(edgeId)) return this;
545
+ return this.mut(g).set("edgeIds", this.edgeIds.asMutable().add(edgeId));
546
+ }
547
+ delEdgeId(g, edgeId) {
548
+ if (!this.edgeIds.has(edgeId)) return this;
549
+ if (this.edgeIds.size == 1) {
550
+ this.delSelf(g);
551
+ return null;
552
+ }
553
+ return this.mut(g).set("edgeIds", this.edgeIds.asMutable().remove(edgeId));
554
+ }
555
+ static add(g, data) {
556
+ const seg = new _Seg({
557
+ ...data,
558
+ id: Edge.key(data, _Seg.prefix)
879
559
  });
560
+ seg.link(g);
561
+ g.segs.set(seg.id, seg);
562
+ g.dirtySegs.add(seg.id);
563
+ return seg;
880
564
  }
881
565
  };
882
566
 
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
- });
567
+ // src/graph/layer.ts
568
+ var import_immutable4 = require("immutable");
569
+ var log3 = logger("layer");
570
+ var defLayerData = {
571
+ id: "",
572
+ index: 0,
573
+ nodeIds: (0, import_immutable4.Set)(),
574
+ sorted: [],
575
+ tracks: [],
576
+ size: 0,
577
+ pos: 0,
578
+ isSorted: false,
579
+ mutable: false
580
+ };
581
+ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
582
+ static prefix = "l:";
583
+ mut(g) {
584
+ if (this.mutable) return this;
585
+ return g.mutateLayer(this);
586
+ }
587
+ final() {
588
+ if (!this.mutable) return this;
589
+ return this.merge({
590
+ nodeIds: this.nodeIds.asImmutable(),
591
+ mutable: false
592
+ }).asImmutable();
593
+ }
594
+ get size() {
595
+ return this.nodeIds.size;
596
+ }
597
+ *nodes(g) {
598
+ for (const nodeId of this.nodeIds.values())
599
+ yield g.getNode(nodeId);
600
+ }
601
+ hasSortOrder(order) {
602
+ return order.length == this.sorted.length && this.sorted.every((nodeId, i) => order[i] == nodeId);
603
+ }
604
+ canCrush(g) {
605
+ for (const node of this.nodes(g))
606
+ if (!node.isDummy)
607
+ return false;
608
+ return true;
609
+ }
610
+ crush(g) {
611
+ g.layerList.remove(this.index);
612
+ g.layers.delete(this.id);
613
+ g.dirtyLayers.delete(this.id);
614
+ for (let i = this.index; i < g.layerList.size; i++)
615
+ g.getLayer(g.layerList.get(i)).setIndex(g, i);
616
+ for (const node of this.nodes(g))
617
+ if (node.isDummy) node.delSelf(g);
618
+ return null;
619
+ }
620
+ setIndex(g, index) {
621
+ if (this.index == index) return this;
622
+ return this.mut(g).set("index", index);
623
+ }
624
+ setTracks(g, tracks) {
625
+ if (this.tracks == tracks) return this;
626
+ return this.mut(g).set("tracks", tracks);
627
+ }
628
+ setSize(g, size) {
629
+ if (this.size == size) return this;
630
+ return this.mut(g).set("size", size);
631
+ }
632
+ setPos(g, pos) {
633
+ if (this.pos == pos) return this;
634
+ return this.mut(g).set("pos", pos);
635
+ }
636
+ addNode(g, nodeId) {
637
+ if (this.nodeIds.has(nodeId)) return this;
638
+ return this.mut(g).set("nodeIds", this.nodeIds.asMutable().add(nodeId));
639
+ }
640
+ willCrush(g, nodeId) {
641
+ for (const node of this.nodes(g))
642
+ if (!node.isDummy && node.id != nodeId)
643
+ return false;
644
+ return true;
645
+ }
646
+ reindex(g, nodeId) {
647
+ if (!this.isSorted) return void 0;
648
+ const sorted = this.sorted.filter((id) => id != nodeId);
649
+ const idx = this.sorted.findIndex((id) => id == nodeId);
650
+ for (let i = idx; i < sorted.length; i++)
651
+ g.getNode(sorted[i]).setIndex(g, i);
652
+ return sorted;
653
+ }
654
+ delNode(g, nodeId) {
655
+ if (!this.nodeIds.has(nodeId)) return this;
656
+ if (this.willCrush(g, nodeId)) return this.crush(g);
657
+ const nodeIds = this.nodeIds.asMutable().remove(nodeId);
658
+ const sorted = this.reindex(g, nodeId);
659
+ return this.mut(g).merge({ nodeIds, sorted });
660
+ }
661
+ setSorted(g, nodeIds) {
662
+ if (this.hasSortOrder(nodeIds)) return this;
663
+ nodeIds.forEach((nodeId, i) => g.getNode(nodeId).setIndex(g, i));
664
+ return this.mut(g).merge({ sorted: nodeIds, isSorted: true });
665
+ }
666
+ *outEdges(g) {
667
+ for (const node of this.nodes(g))
668
+ yield* node.outEdges(g);
669
+ }
670
+ };
671
+
672
+ // src/graph/mutator.ts
673
+ var Mutator = class {
674
+ changes;
675
+ constructor() {
676
+ this.changes = {
677
+ addedNodes: [],
678
+ removedNodes: [],
679
+ updatedNodes: [],
680
+ addedEdges: [],
681
+ removedEdges: [],
682
+ updatedEdges: []
908
683
  };
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));
684
+ }
685
+ describe(description) {
686
+ this.changes.description = description;
687
+ }
688
+ addNode(node) {
689
+ this.changes.addedNodes.push(node);
690
+ }
691
+ addNodes(...nodes) {
692
+ nodes.forEach((node) => this.addNode(node));
693
+ }
694
+ removeNode(node) {
695
+ this.changes.removedNodes.push(node);
696
+ }
697
+ removeNodes(...nodes) {
698
+ nodes.forEach((node) => this.removeNode(node));
699
+ }
700
+ updateNode(node) {
701
+ this.changes.updatedNodes.push(node);
702
+ }
703
+ updateNodes(...nodes) {
704
+ nodes.forEach((node) => this.updateNode(node));
705
+ }
706
+ addEdge(edge) {
707
+ this.changes.addedEdges.push(edge);
708
+ }
709
+ addEdges(...edges) {
710
+ edges.forEach((edge) => this.addEdge(edge));
711
+ }
712
+ removeEdge(edge) {
713
+ this.changes.removedEdges.push(edge);
714
+ }
715
+ removeEdges(...edges) {
716
+ edges.forEach((edge) => this.removeEdge(edge));
717
+ }
718
+ updateEdge(edge) {
719
+ this.changes.updatedEdges.push(edge);
720
+ }
721
+ updateEdges(...edges) {
722
+ edges.forEach((edge) => this.updateEdge(edge));
949
723
  }
950
724
  };
951
725
 
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;
726
+ // src/graph/services/cycles.ts
727
+ var Cycles = class _Cycles {
728
+ static info(g, node) {
729
+ return node.id;
730
+ }
731
+ static checkCycles(g) {
732
+ const totalNodes = g.nodes.size;
733
+ const newStuff = g.changes.addedNodes.length + g.changes.addedEdges.length;
971
734
  const changeRatio = newStuff / totalNodes;
972
735
  if (changeRatio > 0.2 || totalNodes < 20)
973
- this._checkCyclesFull();
736
+ _Cycles.checkCyclesFull(g);
974
737
  else
975
- this._checkCyclesIncremental();
976
- },
977
- /**
978
- * Use a graph traversal algorithm to check for cycles.
979
- */
980
- _checkCyclesFull() {
738
+ _Cycles.checkCyclesIncremental(g);
739
+ }
740
+ static checkCyclesFull(g) {
981
741
  const colorMap = /* @__PURE__ */ new Map();
982
742
  const parentMap = /* @__PURE__ */ new Map();
983
- const white = 0, gray = 1, black = 2;
984
743
  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) {
744
+ const white = 0, gray = 1, black = 2;
745
+ const visit = (node2) => {
746
+ colorMap.set(node2, gray);
747
+ for (const next of node2.outNodes(g)) {
748
+ switch (colorMap.get(next) ?? white) {
989
749
  case gray:
990
- start = nextId;
991
- end = nodeId2;
750
+ start = next;
751
+ end = node2;
992
752
  return true;
993
753
  case white:
994
- parentMap.set(nextId, nodeId2);
995
- if (visit(nextId)) return true;
754
+ parentMap.set(next, node2);
755
+ if (visit(next)) return true;
996
756
  }
997
757
  }
998
- colorMap.set(nodeId2, black);
758
+ colorMap.set(node2, black);
999
759
  return false;
1000
760
  };
1001
- for (const nodeId2 of this._nodeIds())
1002
- if ((colorMap.get(nodeId2) ?? white) == white) {
1003
- if (visit(nodeId2)) break;
761
+ for (const node2 of g.getNodes())
762
+ if ((colorMap.get(node2) ?? white) == white) {
763
+ if (visit(node2)) break;
1004
764
  }
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);
765
+ if (!start || !end) return;
766
+ const cycle = [start];
767
+ let node = end;
768
+ while (node != start) {
769
+ cycle.push(node);
770
+ node = parentMap.get(node);
1011
771
  }
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);
772
+ _Cycles.throwCycle(g, cycle);
773
+ }
774
+ static checkCyclesIncremental(g) {
775
+ for (const edge of g.changes.addedEdges) {
776
+ const source = g.getNode(edge.source.id);
777
+ const target = g.getNode(edge.target.id);
778
+ const layer1 = source.layerIndex(g);
779
+ const layer2 = target.layerIndex(g);
1028
780
  if (layer1 < layer2) continue;
1029
- const route = this._findRoute(edge.target.id, edge.source.id);
781
+ const route = _Cycles.findRoute(g, target, source);
1030
782
  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;
783
+ _Cycles.throwCycle(g, route);
1036
784
  }
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) {
785
+ }
786
+ static throwCycle(g, cycle) {
787
+ cycle.push(cycle[0]);
788
+ cycle.reverse();
789
+ const info = cycle.map((node) => _Cycles.info(g, node));
790
+ throw new Error(`Cycle detected: ${info.join(" \u2192 ")}`);
791
+ }
792
+ static findRoute(g, source, target) {
1046
793
  const parentMap = /* @__PURE__ */ new Map();
1047
- const queue = [sourceId];
1048
- const visited = /* @__PURE__ */ new Set([sourceId]);
794
+ const queue = [source];
795
+ const visited = /* @__PURE__ */ new Set([source]);
1049
796
  while (queue.length > 0) {
1050
- const nodeId = queue.shift();
1051
- if (nodeId == targetId) {
797
+ const node = queue.shift();
798
+ if (node == target) {
1052
799
  const route = [];
1053
- let currId = targetId;
1054
- while (currId != sourceId) {
1055
- route.push(currId);
1056
- currId = parentMap.get(currId);
800
+ let currNode = target;
801
+ while (currNode != source) {
802
+ route.push(currNode);
803
+ currNode = parentMap.get(currNode);
1057
804
  }
1058
- route.push(sourceId);
805
+ route.push(source);
1059
806
  route.reverse();
1060
807
  return route;
1061
808
  }
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);
809
+ for (const next of node.outNodes(g)) {
810
+ if (!visited.has(next)) {
811
+ visited.add(next);
812
+ parentMap.set(next, node);
813
+ queue.push(next);
1067
814
  }
1068
815
  }
1069
816
  }
@@ -1071,51 +818,42 @@ var GraphCycle = {
1071
818
  }
1072
819
  };
1073
820
 
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);
821
+ // src/graph/services/dummy.ts
822
+ var import_immutable5 = require("immutable");
823
+ var log4 = logger("dummy");
824
+ var Dummy = class _Dummy {
825
+ static updateDummies(g) {
826
+ for (const edgeId of g.dirtyEdges) {
827
+ const edge = g.getEdge(edgeId);
828
+ const { type, style } = edge;
829
+ const sourceLayer = edge.sourceNode(g).layerIndex(g);
830
+ const targetLayer = edge.targetNode(g).layerIndex(g);
1091
831
  let segIndex = 0;
1092
832
  let changed = false;
1093
833
  let source = edge.source;
1094
- const segs = edge.segs;
834
+ const segs = edge.segIds;
1095
835
  for (let layerIndex = sourceLayer + 1; layerIndex <= targetLayer; layerIndex++) {
1096
- const layer = this._layerAtIndex(layerIndex);
836
+ const layer = g.layerAt(layerIndex);
1097
837
  while (true) {
1098
838
  const segId = segs[segIndex];
1099
- let seg = segId ? this.getSeg(segId) : null;
1100
- const segLayer = seg ? this._nodeLayerIndex(seg.target.id) : null;
839
+ let seg = segId ? g.getSeg(segId) : null;
840
+ const segLayer = seg ? seg.targetNode(g).layerIndex(g) : null;
1101
841
  if (segIndex == segs.length || segLayer > layerIndex) {
1102
842
  let target;
1103
843
  if (layerIndex == targetLayer) {
1104
844
  target = edge.target;
1105
845
  } else {
1106
- const dummy = this._addDummy({
1107
- edgeId,
846
+ const dummy = Node.addDummy(g, {
847
+ edgeIds: [edgeId],
1108
848
  layerId: layer.id
1109
849
  });
1110
- target = { ...target, id: dummy.id };
850
+ target = { id: dummy.id };
1111
851
  }
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}`);
852
+ seg = Seg.add(g, { source, target, type, style, edgeIds: (0, import_immutable5.Set)([edgeId]) });
1114
853
  segs.splice(segIndex, 0, seg.id);
1115
854
  changed = true;
1116
855
  } 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);
856
+ seg = seg.delEdgeId(g, edgeId);
1119
857
  segs.splice(segIndex, 1);
1120
858
  changed = true;
1121
859
  continue;
@@ -1126,68 +864,67 @@ var GraphDummy = {
1126
864
  }
1127
865
  }
1128
866
  while (segIndex < segs.length) {
1129
- log5.debug(`edge ${edgeId}: removing trailing segment ${segs[segIndex]}`);
1130
- this._segDeleteEdge(segs[segIndex], edgeId);
867
+ g.getSeg(segs[segIndex]).delEdgeId(g, edgeId);
1131
868
  segs.splice(segIndex, 1);
1132
869
  changed = true;
1133
870
  segIndex++;
1134
871
  }
1135
872
  if (changed) {
1136
- log5.debug(`edge ${edgeId}: updated segments to ${segs.join(", ")}`);
1137
- this.edges.set(edgeId, { ...edge, segs });
873
+ edge.setSegIds(g, segs);
1138
874
  }
1139
875
  }
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();
876
+ }
877
+ static mergeDummies(g) {
878
+ for (const side of g.options.mergeOrder)
879
+ _Dummy.mergeScan(g, side);
880
+ }
881
+ static mergeScan(g, side) {
882
+ let layerIds = [...g.layerList].filter((layerId) => g.dirtyLayers.has(layerId));
883
+ if (side == "target") layerIds.reverse();
1148
884
  const dir = side == "source" ? "in" : "out";
1149
885
  const altSide = side == "source" ? "target" : "source";
1150
886
  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);
887
+ for (const layerId of layerIds) {
888
+ let layer = g.getLayer(layerId);
1154
889
  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);
890
+ for (const nodeId of layer.nodeIds) {
891
+ if (!Node.isDummyId(nodeId)) continue;
892
+ const node = g.getNode(nodeId);
893
+ if (node.isMerged) continue;
894
+ const edge = g.getEdge(node.edgeIds[0]);
895
+ const key = Edge.key(edge, "k:", side);
1161
896
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
1162
897
  groups.get(key).add(node);
1163
898
  }
1164
899
  for (const [key, group] of groups) {
1165
900
  if (group.size == 1) continue;
1166
901
  const edgeIds = [...group].map((node) => node.edgeId);
1167
- const dummy = this._addDummy({ edgeIds, layerId, merged: true });
902
+ const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
1168
903
  let seg;
1169
904
  for (const old of group) {
1170
- for (const segId of this._relIds(old.id, "segs", dir)) {
905
+ let edge = g.getEdge(old.edgeIds[0]);
906
+ for (const segId of old.relIds("segs", dir)) {
1171
907
  if (!seg) {
1172
- const example = this.getSeg(segId);
1173
- seg = this._addSeg({
908
+ const example = g.getSeg(segId);
909
+ seg = Seg.add(g, {
1174
910
  ...example,
1175
- edges: (0, import_immutable3.Set)([old.edgeId]),
911
+ edgeIds: (0, import_immutable5.Set)([old.edgeId]),
1176
912
  [altSide]: { ...example[altSide], id: dummy.id }
1177
913
  });
1178
914
  }
1179
- this._edgeReplaceSeg(old.edgeId, segId, seg.id);
915
+ edge = edge.replaceSegId(g, segId, seg.id);
1180
916
  }
1181
917
  }
1182
918
  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({
919
+ let edge = g.getEdge(old.edgeIds[0]);
920
+ for (const segId of old.relIds("segs", altDir)) {
921
+ const example = g.getSeg(segId);
922
+ const seg2 = Seg.add(g, {
1186
923
  ...example,
1187
- edges: (0, import_immutable3.Set)([old.edgeId]),
924
+ edgeIds: (0, import_immutable5.Set)([old.edgeId]),
1188
925
  [side]: { ...example[side], id: dummy.id }
1189
926
  });
1190
- this._edgeReplaceSeg(old.edgeId, segId, seg2.id);
927
+ edge = edge.replaceSegId(g, segId, seg2.id);
1191
928
  }
1192
929
  }
1193
930
  }
@@ -1195,209 +932,191 @@ var GraphDummy = {
1195
932
  }
1196
933
  };
1197
934
 
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"));
935
+ // src/graph/services/layers.ts
936
+ var import_immutable6 = require("immutable");
937
+ var log5 = logger("layers");
938
+ var Layers = class {
939
+ static updateLayers(g) {
940
+ const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
941
+ const phase2 = new Set(stack);
942
+ const moved = /* @__PURE__ */ new Set();
943
+ while (stack.length > 0) {
944
+ let node = g.getNode(stack.pop());
945
+ const curLayer = node.layerIndex(g);
946
+ let correctLayer = 0;
947
+ const parents = node.inNodes(g);
948
+ for (const parent of parents) {
949
+ const pidx = parent.layerIndex(g);
950
+ if (pidx >= correctLayer) correctLayer = pidx + 1;
951
+ }
952
+ if (curLayer != correctLayer) {
953
+ node = node.moveToLayerIndex(g, correctLayer);
954
+ stack.push(...node.outNodeIds(g));
955
+ moved.add(node.id);
956
+ for (const parent of parents)
957
+ phase2.add(parent.id);
958
+ }
959
+ }
960
+ const byLayer = /* @__PURE__ */ new Map();
961
+ const addParent = (nodeId) => {
962
+ let set;
963
+ const layerId = g.getNode(nodeId).layerId;
964
+ if (!byLayer.has(layerId)) {
965
+ set = /* @__PURE__ */ new Set();
966
+ byLayer.set(layerId, set);
967
+ } else {
968
+ set = byLayer.get(layerId);
969
+ }
970
+ set.add(nodeId);
971
+ };
972
+ for (const id of phase2) addParent(id);
973
+ const layerIds = [...byLayer.keys()].sort(
974
+ (a, b) => g.getLayer(b).index - g.getLayer(a).index
975
+ );
976
+ for (const layerId of layerIds) {
977
+ const curLayer = g.getLayer(layerId).index;
978
+ for (const parentId of byLayer.get(layerId)) {
979
+ let parent = g.getNode(parentId);
980
+ const children = [...parent.outNodes(g)];
981
+ if (children.length == 0) continue;
982
+ const minChild = (0, import_immutable6.Seq)(children).map((node) => node.layerIndex(g)).min();
983
+ const correctLayer = minChild - 1;
984
+ if (curLayer != correctLayer) {
985
+ moved.add(parentId);
986
+ parent = parent.moveToLayerIndex(g, correctLayer);
987
+ for (const gpId of parent.inNodeIds(g))
988
+ addParent(gpId);
989
+ }
990
+ }
991
+ }
992
+ for (const id of moved)
993
+ for (const edgeId of g.getNode(id).relIds("edges"))
994
+ g.dirtyEdges.add(edgeId);
995
+ }
996
+ };
997
+
998
+ // src/graph/services/layout.ts
999
+ var import_immutable7 = require("immutable");
1000
+ var log6 = logger("layout");
1001
+ var Layout = class _Layout {
1002
+ static parentIndex(g, node) {
1003
+ const parents = (0, import_immutable7.Seq)([...node.adjs(g, "segs", "in")]);
1210
1004
  const pidx = parents.map((p) => p.index).min();
1211
- log6.debug(`node ${node.id}: parent index ${pidx}`);
1212
1005
  if (pidx !== void 0) return pidx;
1213
1006
  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) {
1007
+ }
1008
+ static compareNodes(g, aId, bId, pidxs) {
1223
1009
  const ai = pidxs.get(aId);
1224
1010
  const bi = pidxs.get(bId);
1225
1011
  if (ai !== bi) return ai - bi;
1226
- const a = this.getNode(aId);
1227
- const b = this.getNode(bId);
1012
+ const a = g.getNode(aId);
1013
+ const b = g.getNode(bId);
1228
1014
  if (a.isDummy && !b.isDummy) return -1;
1229
1015
  if (!a.isDummy && b.isDummy) return 1;
1230
1016
  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();
1017
+ const minA = a.edgeId ?? (0, import_immutable7.Seq)(a.edgeIds).min();
1018
+ const minB = b.edgeId ?? (0, import_immutable7.Seq)(b.edgeIds).min();
1233
1019
  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
- }
1020
+ }
1021
+ static positionNodes(g) {
1022
+ for (const nodeId of g.dirtyNodes)
1023
+ g.dirtyLayers.add(g.getNode(nodeId).layerId);
1250
1024
  let adjustNext = false;
1251
- for (const layerId of this.layerList) {
1252
- if (!adjustNext && !this.dirtyLayers.has(layerId)) continue;
1025
+ for (const layerId of g.layerList) {
1026
+ if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
1253
1027
  adjustNext = false;
1254
- const layer = this.getLayer(layerId);
1028
+ let layer = g.getLayer(layerId);
1255
1029
  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 });
1030
+ for (const nodeId of layer.nodeIds)
1031
+ pidxs.set(nodeId, _Layout.parentIndex(g, g.getNode(nodeId)));
1032
+ const sorted = [...layer.nodeIds].sort(
1033
+ (aId, bId) => _Layout.compareNodes(g, aId, bId, pidxs)
1034
+ );
1035
+ if (layer.hasSortOrder(sorted)) continue;
1036
+ g.dirtyLayers.add(layerId);
1037
+ layer = layer.setSorted(g, sorted);
1262
1038
  adjustNext = true;
1263
1039
  let lpos = 0;
1264
1040
  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;
1041
+ let node = g.getNode(sorted[i]);
1042
+ node = node.setIndex(g, i).setLayerPos(g, lpos);
1043
+ const size = node.dims?.[g.w] ?? 0;
1044
+ lpos += size + g.options.nodeMargin;
1270
1045
  }
1271
1046
  }
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}`]();
1047
+ }
1048
+ static alignAll(g) {
1049
+ if (g.options.layoutSteps) {
1050
+ for (const step of g.options.layoutSteps)
1051
+ _Layout[step](g);
1285
1052
  } else {
1286
- for (let i = 0; i < this.options.alignIterations; i++) {
1287
- let anyChanged = this._alignChildren() || this._alignParents() || this._compact();
1053
+ for (let i = 0; i < g.options.alignIterations; i++) {
1054
+ let anyChanged = _Layout.alignChildren(g) || _Layout.alignParents(g) || _Layout.compact(g);
1288
1055
  if (!anyChanged) break;
1289
1056
  }
1290
1057
  }
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];
1058
+ }
1059
+ static alignChildren(g) {
1060
+ return _Layout.alignNodes(g, false, false, false, "in", false);
1061
+ }
1062
+ static alignParents(g) {
1063
+ return _Layout.alignNodes(g, true, true, false, "out", true);
1064
+ }
1065
+ static alignNodes(g, reverseLayers, reverseNodes, reverseMove, dir, conservative) {
1066
+ let layerIds = [...g.layerList];
1329
1067
  let anyChanged = false;
1330
1068
  if (reverseLayers) layerIds.reverse();
1331
1069
  let adjustNext = false;
1332
1070
  for (const layerId of layerIds) {
1333
- if (!adjustNext && !this.dirtyLayers.has(layerId)) continue;
1071
+ if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
1334
1072
  adjustNext = false;
1073
+ let iterations = 0;
1335
1074
  while (true) {
1075
+ if (++iterations > 10) {
1076
+ log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1077
+ break;
1078
+ }
1336
1079
  let changed = false;
1337
- const nodeIds = this._sortLayer(layerId, reverseNodes);
1080
+ const nodeIds = _Layout.sortLayer(g, layerId, reverseNodes);
1338
1081
  for (const nodeId of nodeIds) {
1339
- const { isAligned, pos: newPos, nodeId: otherId } = this._nearestNode(nodeId, dir, reverseMove, !reverseMove);
1082
+ const {
1083
+ isAligned,
1084
+ pos: newPos,
1085
+ nodeId: otherId
1086
+ } = _Layout.nearestNode(g, nodeId, dir, reverseMove, !reverseMove);
1340
1087
  if (isAligned || newPos === void 0) continue;
1341
- if (this._shiftNode(nodeId, otherId, dir, newPos, reverseMove, conservative)) {
1088
+ if (_Layout.shiftNode(g, nodeId, otherId, dir, newPos, reverseMove, conservative)) {
1342
1089
  changed = true;
1343
1090
  anyChanged = true;
1344
1091
  break;
1345
1092
  }
1346
1093
  }
1347
1094
  if (!changed) break;
1348
- this.dirtyLayers.add(layerId);
1095
+ g.dirtyLayers.add(layerId);
1349
1096
  adjustNext = true;
1350
1097
  }
1351
1098
  }
1352
1099
  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
- }
1100
+ }
1101
+ static sortLayer(g, layerId, reverseNodes) {
1102
+ const layer = g.getLayer(layerId);
1103
+ const sorted = [...layer.nodeIds];
1104
+ sorted.sort((a, b) => g.getNode(a).lpos - g.getNode(b).lpos);
1105
+ layer.setSorted(g, sorted);
1375
1106
  if (reverseNodes)
1376
1107
  return sorted.toReversed();
1377
1108
  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);
1109
+ }
1110
+ static nearestNode(g, nodeId, dir, allowLeft, allowRight) {
1111
+ const node = g.getNode(nodeId);
1393
1112
  let minDist = Infinity;
1394
1113
  let bestPos, bestNodeId;
1395
1114
  const mySide = dir == "in" ? "target" : "source";
1396
1115
  const altSide = dir == "in" ? "source" : "target";
1397
- for (const seg of this._rels(nodeId, "segs", dir)) {
1116
+ for (const seg of node.rels(g, "segs", dir)) {
1398
1117
  const altId = seg[altSide].id;
1399
- const myPos = this._anchorPos(seg, mySide)[this._x];
1400
- const altPos = this._anchorPos(seg, altSide)[this._x];
1118
+ const myPos = _Layout.anchorPos(g, seg, mySide)[g.x];
1119
+ const altPos = _Layout.anchorPos(g, seg, altSide)[g.x];
1401
1120
  const diff = altPos - myPos;
1402
1121
  if (diff == 0) return { nodeId: altId, isAligned: true };
1403
1122
  if (diff < 0 && !allowLeft) continue;
@@ -1410,117 +1129,98 @@ var GraphPos = {
1410
1129
  }
1411
1130
  }
1412
1131
  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;
1132
+ }
1133
+ static anchorPos(g, seg, side) {
1424
1134
  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;
1135
+ const node = g.getNode(nodeId);
1136
+ let p = { x: 0, y: 0, [g.x]: node.lpos, ...node.pos || {} };
1137
+ let w = node.dims?.[g.w] ?? 0;
1138
+ let h = node.dims?.[g.h] ?? 0;
1429
1139
  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;
1140
+ return {
1141
+ [g.x]: p[g.x] + w / 2,
1142
+ [g.y]: p[g.y] + h / 2
1143
+ };
1144
+ p[g.x] += _Layout.nodePortOffset(g, nodeId, seg, side);
1145
+ if (side == "target" == g.r)
1146
+ p[g.y] += h;
1434
1147
  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);
1148
+ }
1149
+ static nodePortOffset(g, nodeId, seg, side) {
1150
+ const node = g.getNode(nodeId);
1151
+ const dir = side == "source" ? "out" : "in";
1152
+ const portId = seg[side].port;
1153
+ let min = 0, size = node.dims?.[g.w] ?? 0;
1154
+ if (portId) {
1155
+ const ports = node.ports?.[dir];
1156
+ const port = ports?.find((p) => p.id === portId);
1157
+ if (port?.offset !== void 0) {
1158
+ min = port.offset;
1159
+ size = port.size ?? 0;
1160
+ }
1161
+ }
1162
+ const alt = side == "source" ? "target" : "source";
1163
+ let segs = [];
1164
+ const keyOf = (seg2) => `${seg2.type ?? ""}:${seg2[side].marker ?? ""}`;
1165
+ for (const segId of node.segs[dir])
1166
+ segs.push(g.getSeg(segId));
1167
+ if (portId) segs = segs.filter((s) => s[side].port == portId);
1168
+ const groups = Object.groupBy(segs, (s) => keyOf(s));
1169
+ const posMap = /* @__PURE__ */ new Map();
1170
+ for (const [key, segs2] of Object.entries(groups)) {
1171
+ let pos = Infinity;
1172
+ for (const seg2 of segs2) pos = Math.min(pos, seg2.node(g, alt).lpos);
1173
+ posMap.set(key, pos);
1174
+ }
1175
+ const keys = [...posMap.keys()].sort((a, b) => posMap.get(a) - posMap.get(b));
1176
+ const gap = size / (keys.length + 1);
1177
+ const index = keys.indexOf(keyOf(seg));
1178
+ return min + (index + 1) * gap;
1179
+ }
1180
+ static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1181
+ const node = g.getNode(nodeId);
1182
+ log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1463
1183
  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;
1184
+ _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1185
+ const space = g.options.nodeMargin;
1186
+ const nodeWidth = node.dims?.[g.w] ?? 0;
1467
1187
  const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1468
1188
  repeat:
1469
- for (const otherId of this.getLayer(node.layerId).nodes) {
1189
+ for (const otherId of node.getLayer(g).nodeIds) {
1470
1190
  if (otherId == nodeId) continue;
1471
- const other = this.getNode(otherId);
1191
+ const other = g.getNode(otherId);
1472
1192
  const opos = other.lpos;
1473
- const otherWidth = ((_b = other.dims) == null ? void 0 : _b[this._width]) ?? 0;
1193
+ const otherWidth = other.dims?.[g.w] ?? 0;
1474
1194
  const bMin = opos, bMax = opos + otherWidth;
1475
1195
  if (aMin < bMax && bMin < aMax) {
1476
1196
  if (conservative) return false;
1477
1197
  const safePos = reverseMove ? aMin - otherWidth : aMax;
1478
- this._shiftNode(otherId, void 0, dir, safePos, reverseMove, conservative);
1198
+ _Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
1479
1199
  continue repeat;
1480
1200
  }
1481
1201
  }
1482
1202
  if (conservative)
1483
- this._markAligned(nodeId, alignId, dir, lpos);
1203
+ _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1484
1204
  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);
1205
+ }
1206
+ static markAligned(g, nodeId, otherId, dir, lpos) {
1207
+ const node = g.getNode(nodeId);
1496
1208
  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) {
1209
+ if (node.aligned[dir])
1210
+ g.getNode(node.aligned[dir]).setAligned(g, alt, void 0);
1211
+ if (otherId)
1212
+ g.getNode(otherId).setAligned(g, alt, nodeId);
1213
+ node.setAligned(g, dir, otherId).setLayerPos(g, lpos);
1214
+ }
1215
+ static *aligned(g, nodeId, dir) {
1516
1216
  const visit = function* (node2, dir2) {
1517
1217
  const otherId = node2.aligned[dir2];
1518
1218
  if (!otherId) return;
1519
- const other = this.getNode(otherId);
1219
+ const other = g.getNode(otherId);
1520
1220
  yield other;
1521
- yield* visit.call(this, other, dir2);
1522
- }.bind(this);
1523
- const node = this.getNode(nodeId);
1221
+ yield* visit(other, dir2);
1222
+ };
1223
+ const node = g.getNode(nodeId);
1524
1224
  yield node;
1525
1225
  if (dir == "both") {
1526
1226
  yield* visit(node, "in");
@@ -1528,121 +1228,1786 @@ var GraphPos = {
1528
1228
  } else {
1529
1229
  yield* visit(node, dir);
1530
1230
  }
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) {
1231
+ }
1232
+ static leftOf(g, node) {
1539
1233
  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);
1234
+ return node.getLayer(g).sorted[node.index - 1];
1235
+ }
1236
+ static rightOf(g, node) {
1237
+ const layer = node.getLayer(g);
1550
1238
  if (node.index == layer.sorted.length - 1) return null;
1551
1239
  return layer.sorted[node.index + 1];
1552
- },
1553
- /**
1554
- * Compact tries to eliminate empty space between nodes
1555
- */
1556
- _compact() {
1557
- var _a;
1240
+ }
1241
+ static compact(g) {
1558
1242
  let anyChanged = false;
1559
- for (const layerId of this.layerList) {
1560
- const layer = this.getLayer(layerId);
1243
+ for (const layerId of g.layerList) {
1244
+ const layer = g.getLayer(layerId);
1561
1245
  if (layer.sorted.length < 2) continue;
1562
1246
  for (const nodeId of layer.sorted) {
1563
- const node = this.getNode(nodeId);
1247
+ const node = g.getNode(nodeId);
1564
1248
  if (node.index == 0) continue;
1565
1249
  let minGap = Infinity;
1566
1250
  const stack = [];
1567
- for (const right of this._aligned(nodeId, "both")) {
1251
+ for (const right of _Layout.aligned(g, nodeId, "both")) {
1568
1252
  stack.push(right);
1569
- const leftId = this._leftOf(right);
1253
+ const leftId = _Layout.leftOf(g, right);
1570
1254
  if (!leftId) return;
1571
- const left = this.getNode(leftId);
1572
- const leftWidth = ((_a = left.dims) == null ? void 0 : _a[this._width]) ?? 0;
1255
+ const left = g.getNode(leftId);
1256
+ const leftWidth = left.dims?.[g.w] ?? 0;
1573
1257
  const gap = right.lpos - left.lpos - leftWidth;
1574
1258
  if (gap < minGap) minGap = gap;
1575
1259
  }
1576
- const delta = minGap - this.options.nodeMargin;
1260
+ const delta = minGap - g.options.nodeMargin;
1577
1261
  if (delta <= 0) continue;
1578
1262
  anyChanged = true;
1579
1263
  for (const right of stack)
1580
- this.nodes.set(right.id, { ...right, lpos: right.lpos - delta });
1264
+ right.setLayerPos(g, right.lpos - delta);
1581
1265
  }
1582
1266
  }
1583
1267
  return anyChanged;
1584
1268
  }
1269
+ static getCoords(g) {
1270
+ let pos = 0;
1271
+ const dir = g.r ? -1 : 1;
1272
+ const trackSep = Math.max(
1273
+ g.options.edgeSpacing,
1274
+ g.options.turnRadius
1275
+ );
1276
+ const marginSep = Math.max(
1277
+ g.options.edgeSpacing,
1278
+ g.options.layerMargin,
1279
+ g.options.turnRadius + g.options.markerSize
1280
+ );
1281
+ for (const layerId of g.layerList) {
1282
+ let layer = g.getLayer(layerId);
1283
+ let height;
1284
+ if (g.dirtyLayers.has(layerId)) {
1285
+ height = (0, import_immutable7.Seq)(layer.nodes(g)).map((node) => node.dims?.[g.h] ?? 0).max() ?? 0;
1286
+ layer = layer.setSize(g, height);
1287
+ } else height = layer.size;
1288
+ for (const node of layer.nodes(g)) {
1289
+ if (!g.dirtyNodes.has(node.id) && pos == layer.pos) continue;
1290
+ const npos = { [g.x]: node.lpos, [g.y]: pos };
1291
+ if (!g.n) npos[g.y] += dir * height;
1292
+ if (g.r == g.n) npos[g.y] -= node.dims?.[g.h] ?? 0;
1293
+ node.setPos(g, npos);
1294
+ }
1295
+ layer = layer.setPos(g, pos);
1296
+ pos += dir * (height + marginSep);
1297
+ for (const track of layer.tracks) {
1298
+ for (const segId of track)
1299
+ g.getSeg(segId).setTrackPos(g, pos);
1300
+ pos += dir * trackSep;
1301
+ }
1302
+ pos += dir * (marginSep - trackSep);
1303
+ }
1304
+ }
1585
1305
  };
1586
1306
 
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 ?? {}
1307
+ // src/canvas/marker.tsx
1308
+ var import_marker = __toESM(require("./marker.css?raw"), 1);
1309
+ var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1310
+ function arrow(size, classPrefix, reverse = false) {
1311
+ const h = size / 1.5;
1312
+ const w = size;
1313
+ const ry = h / 2;
1314
+ const suffix = reverse ? "-reverse" : "";
1315
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1316
+ "marker",
1317
+ {
1318
+ id: `g3p-marker-arrow${suffix}`,
1319
+ className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1320
+ markerWidth: size,
1321
+ markerHeight: size,
1322
+ refX: "2",
1323
+ refY: ry,
1324
+ orient: reverse ? "auto-start-reverse" : "auto",
1325
+ markerUnits: "userSpaceOnUse",
1326
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M0,0 L0,${h} L${w},${ry} z` })
1327
+ }
1328
+ );
1329
+ }
1330
+ function circle(size, classPrefix, reverse = false) {
1331
+ const r = size / 3;
1332
+ const cy = size / 2;
1333
+ const suffix = reverse ? "-reverse" : "";
1334
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1335
+ "marker",
1336
+ {
1337
+ id: `g3p-marker-circle${suffix}`,
1338
+ className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1339
+ markerWidth: size,
1340
+ markerHeight: size,
1341
+ refX: "2",
1342
+ refY: cy,
1343
+ orient: reverse ? "auto-start-reverse" : "auto",
1344
+ markerUnits: "userSpaceOnUse",
1345
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: r + 2, cy, r })
1346
+ }
1347
+ );
1348
+ }
1349
+ function diamond(size, classPrefix, reverse = false) {
1350
+ const w = size * 0.7;
1351
+ const h = size / 2;
1352
+ const cy = size / 2;
1353
+ const suffix = reverse ? "-reverse" : "";
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ "marker",
1356
+ {
1357
+ id: `g3p-marker-diamond${suffix}`,
1358
+ className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1359
+ markerWidth: size,
1360
+ markerHeight: size,
1361
+ refX: "2",
1362
+ refY: cy,
1363
+ orient: reverse ? "auto-start-reverse" : "auto",
1364
+ markerUnits: "userSpaceOnUse",
1365
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M2,${cy} L${2 + w / 2},${cy - h / 2} L${2 + w},${cy} L${2 + w / 2},${cy + h / 2} z` })
1366
+ }
1367
+ );
1368
+ }
1369
+ function bar(size, classPrefix, reverse = false) {
1370
+ const h = size * 0.6;
1371
+ const cy = size / 2;
1372
+ const suffix = reverse ? "-reverse" : "";
1373
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1374
+ "marker",
1375
+ {
1376
+ id: `g3p-marker-bar${suffix}`,
1377
+ className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1378
+ markerWidth: size,
1379
+ markerHeight: size,
1380
+ refX: "2",
1381
+ refY: cy,
1382
+ orient: reverse ? "auto-start-reverse" : "auto",
1383
+ markerUnits: "userSpaceOnUse",
1384
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: cy - h / 2, x2: "2", y2: cy + h / 2, "stroke-width": "2" })
1385
+ }
1386
+ );
1387
+ }
1388
+ function none(size, classPrefix, reverse = false) {
1389
+ return void 0;
1390
+ }
1391
+ function normalize(data) {
1392
+ let source = data.source?.marker ?? data.style?.marker?.source;
1393
+ let target = data.target?.marker ?? data.style?.marker?.target ?? "arrow";
1394
+ if (source == "none") source = void 0;
1395
+ if (target == "none") target = void 0;
1396
+ return { source, target };
1397
+ }
1398
+ var markerDefs = {
1399
+ arrow,
1400
+ circle,
1401
+ diamond,
1402
+ bar,
1403
+ none
1404
+ };
1405
+
1406
+ // src/graph/services/lines.ts
1407
+ var log7 = logger("lines");
1408
+ var Lines = class _Lines {
1409
+ static layoutSeg(g, seg) {
1410
+ const sourcePos = Layout.anchorPos(g, seg, "source");
1411
+ const targetPos = Layout.anchorPos(g, seg, "target");
1412
+ return seg.setPos(g, sourcePos[g.x], targetPos[g.x]);
1413
+ }
1414
+ static layerSegs(g, layer) {
1415
+ const segs = [];
1416
+ for (const node of layer.nodes(g))
1417
+ for (const seg of node.outSegs(g))
1418
+ segs.push(_Lines.layoutSeg(g, seg));
1419
+ return segs;
1420
+ }
1421
+ static trackEdges(g) {
1422
+ for (const segId of g.dirtySegs.values())
1423
+ g.dirtyLayers.add(g.getSeg(segId).sourceNode(g).layerId);
1424
+ const minLength = g.options.turnRadius * 2;
1425
+ for (const layerId of g.dirtyLayers.values()) {
1426
+ const layer = g.getLayer(layerId);
1427
+ const leftTracks = [];
1428
+ const rightTracks = [];
1429
+ const allTracks = [];
1430
+ const segs = _Lines.layerSegs(g, layer).sort((a, b) => a.p1 - b.p1);
1431
+ for (const seg of segs) {
1432
+ if (Math.abs(seg.p1 - seg.p2) < minLength) {
1433
+ seg.setTrackPos(g, void 0);
1434
+ continue;
1435
+ }
1436
+ let trackSet;
1437
+ if (!g.options.separateTrackSets)
1438
+ trackSet = allTracks;
1439
+ else if (seg.p1 < seg.p2)
1440
+ trackSet = rightTracks;
1441
+ else
1442
+ trackSet = leftTracks;
1443
+ let validTrack;
1444
+ for (let i = trackSet.length - 1; i >= 0; i--) {
1445
+ const track = trackSet[i];
1446
+ let overlap = false;
1447
+ for (const other of track) {
1448
+ if (seg.anySameEnd(other)) {
1449
+ track.push(seg);
1450
+ validTrack = track;
1451
+ break;
1452
+ }
1453
+ if (other.p1 < seg.p2 && seg.p1 < other.p2) {
1454
+ overlap = true;
1455
+ break;
1456
+ }
1457
+ }
1458
+ if (!overlap) {
1459
+ validTrack = track;
1460
+ break;
1461
+ }
1462
+ }
1463
+ if (validTrack)
1464
+ validTrack.push(seg);
1465
+ else
1466
+ trackSet.push([seg]);
1467
+ }
1468
+ const tracks = [];
1469
+ const all = leftTracks.concat(rightTracks).concat(allTracks);
1470
+ for (const track of all)
1471
+ tracks.push(track.map((seg) => seg.id));
1472
+ layer.setTracks(g, tracks);
1473
+ }
1474
+ return this;
1475
+ }
1476
+ static pathEdges(g) {
1477
+ for (const seg of g.segs.values()) {
1478
+ if (!g.dirtySegs.has(seg.id)) continue;
1479
+ const radius = g.options.turnRadius;
1480
+ const p1 = Layout.anchorPos(g, seg, "source");
1481
+ const p2 = Layout.anchorPos(g, seg, "target");
1482
+ const source = seg.sourceNode(g);
1483
+ const target = seg.targetNode(g);
1484
+ const marker = normalize(seg);
1485
+ if (source.isDummy) marker.source = void 0;
1486
+ if (target.isDummy) marker.target = void 0;
1487
+ const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius, marker) : _Lines.createDirectPath(g, p1, p2, radius, marker);
1488
+ const svg = _Lines.pathToSVG(path);
1489
+ seg.setSVG(g, svg);
1490
+ }
1491
+ return this;
1492
+ }
1493
+ static pathLine(p1, p2, type, radius) {
1494
+ if (p2.x === void 0) p2.x = p1.x;
1495
+ if (p2.y === void 0) p2.y = p1.y;
1496
+ if (p2.s === void 0) p2.s = p1.s;
1497
+ const line = {
1498
+ type,
1499
+ x1: p1.x,
1500
+ y1: p1.y,
1501
+ x2: p2.x,
1502
+ y2: p2.y
1605
1503
  };
1606
- this.changes = changes ?? {
1607
- addedNodes: [],
1608
- removedNodes: [],
1609
- addedEdges: [],
1610
- removedEdges: []
1504
+ p1.x = p2.x;
1505
+ p1.y = p2.y;
1506
+ p1.s = p2.s;
1507
+ if (type == "arc") {
1508
+ line.radius = radius;
1509
+ line.sweep = p1.s;
1510
+ }
1511
+ return line;
1512
+ }
1513
+ static pathBuilder(g, start, end, trackPos, radius, marker) {
1514
+ const { x, y } = g;
1515
+ const lr = end[x] > start[x];
1516
+ const d = lr ? 1 : -1;
1517
+ const o = g.r ? -1 : 1;
1518
+ const rd = radius * d;
1519
+ const ro = radius * o;
1520
+ const t = trackPos;
1521
+ let s = 0;
1522
+ if (g.r) s = 1 - s;
1523
+ if (!lr) s = 1 - s;
1524
+ if (!g.v) s = 1 - s;
1525
+ if (marker.source) start[y] += o * (g.options.markerSize - 1);
1526
+ if (marker.target) end[y] -= o * (g.options.markerSize - 1);
1527
+ const p = { ...start, s };
1528
+ const path = [];
1529
+ const advance = (p2, type) => {
1530
+ path.push(this.pathLine(p, p2, type, radius));
1611
1531
  };
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
1532
+ return { x, y, lr, d, o, rd, ro, t, s, p, path, advance };
1533
+ }
1534
+ // Create a railroad-style path with two 90-degree turns
1535
+ static createRailroadPath(g, start, end, trackPos, radius, marker) {
1536
+ const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius, marker);
1537
+ advance({ [y]: t - ro }, "line");
1538
+ advance({ [x]: p[x] + rd, [y]: t }, "arc");
1539
+ advance({ [x]: end[x] - rd }, "line");
1540
+ advance({ [x]: end[x], [y]: t + ro, s: 1 - s }, "arc");
1541
+ advance({ [y]: end[y] }, "line");
1542
+ return path;
1543
+ }
1544
+ // Create a mostly-vertical path with optional S-curve
1545
+ static createDirectPath(g, start, end, radius, marker) {
1546
+ const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius, marker);
1547
+ const dx = Math.abs(end.x - start.x);
1548
+ const dy = Math.abs(end.y - start.y);
1549
+ const d_ = { x: dx, y: dy };
1550
+ if (dx < 0.1) {
1551
+ advance({ ...end }, "line");
1552
+ return path;
1553
+ }
1554
+ const curve = _Lines.calculateSCurve(d_[x], d_[y], radius);
1555
+ if (!curve || curve.Ly == 0) {
1556
+ advance({ ...end }, "line");
1557
+ return path;
1558
+ }
1559
+ const m = {
1560
+ [x]: d * (d_[x] - curve.Lx) / 2,
1561
+ [y]: o * (d_[y] - curve.Ly) / 2
1562
+ };
1563
+ advance({ [x]: p[x] + m[x], [y]: p[y] + m[y] }, "arc");
1564
+ advance({ [x]: end[x] - m[x], [y]: end[y] - m[y] }, "line");
1565
+ advance({ [x]: end[x], [y]: end[y], s: 1 - s }, "arc");
1566
+ return path;
1567
+ }
1568
+ static solveSCurveAngle(dx, dy, r) {
1569
+ const f = (alpha2) => {
1570
+ return dx * Math.cos(alpha2) - dy * Math.sin(alpha2) - 2 * r * Math.cos(alpha2) + 2 * r;
1571
+ };
1572
+ const fPrime = (alpha2) => {
1573
+ return -dx * Math.sin(alpha2) - dy * Math.cos(alpha2) + 2 * r * Math.sin(alpha2);
1574
+ };
1575
+ let alpha = Math.min(0.1, Math.abs(dx) / dy);
1576
+ const maxIterations = 20;
1577
+ const tolerance = 1e-6;
1578
+ for (let i = 0; i < maxIterations; i++) {
1579
+ const fVal = f(alpha);
1580
+ const fPrimeVal = fPrime(alpha);
1581
+ if (Math.abs(fVal) < tolerance) {
1582
+ const Ly = dy - 2 * r * Math.sin(alpha);
1583
+ if (Ly >= 0 && alpha > 0 && alpha < Math.PI / 2) {
1584
+ return alpha;
1585
+ }
1586
+ return null;
1587
+ }
1588
+ if (Math.abs(fPrimeVal) < 1e-10) {
1589
+ break;
1590
+ }
1591
+ const nextAlpha = alpha - fVal / fPrimeVal;
1592
+ if (nextAlpha < 0 || nextAlpha > Math.PI / 2) {
1593
+ break;
1594
+ }
1595
+ alpha = nextAlpha;
1596
+ }
1597
+ return null;
1598
+ }
1599
+ static calculateSCurve(dx, dy, r) {
1600
+ const alpha = _Lines.solveSCurveAngle(dx, dy, r);
1601
+ if (alpha === null) {
1602
+ return null;
1603
+ }
1604
+ const Ly = dy - 2 * r * Math.sin(alpha);
1605
+ const Lx = Ly * Math.tan(alpha);
1606
+ const L = Ly / Math.cos(alpha);
1607
+ return {
1608
+ alpha,
1609
+ // Angle of each arc (radians)
1610
+ Lx,
1611
+ // Horizontal component of straight section
1612
+ Ly,
1613
+ // Vertical component of straight section
1614
+ L
1615
+ // Length of straight section
1616
+ };
1617
+ }
1618
+ static pathToSVG(path) {
1619
+ if (!path || path.length === 0) return "";
1620
+ const lines = [];
1621
+ const first = path[0];
1622
+ lines.push(`M ${first.x1},${first.y1}`);
1623
+ for (const line of path) {
1624
+ if (line.type === "line") {
1625
+ lines.push(`L ${line.x2},${line.y2}`);
1626
+ } else if (line.type === "arc") {
1627
+ const r = line.radius;
1628
+ const largeArc = 0;
1629
+ const sweep = line.sweep || 0;
1630
+ lines.push(`A ${r},${r} 0 ${largeArc} ${sweep} ${line.x2},${line.y2}`);
1631
+ }
1632
+ }
1633
+ return lines.join(" ");
1634
+ }
1635
+ };
1636
+
1637
+ // src/graph/graph.ts
1638
+ var emptyChanges = {
1639
+ addedNodes: [],
1640
+ removedNodes: [],
1641
+ updatedNodes: [],
1642
+ addedEdges: [],
1643
+ removedEdges: [],
1644
+ updatedEdges: []
1645
+ };
1646
+ var Graph = class _Graph {
1647
+ prior;
1648
+ nodes;
1649
+ edges;
1650
+ segs;
1651
+ layers;
1652
+ layerList;
1653
+ nextLayerId;
1654
+ nextDummyId;
1655
+ options;
1656
+ changes;
1657
+ dirtyNodes;
1658
+ dirtyEdges;
1659
+ dirtyLayers;
1660
+ dirtySegs;
1661
+ dirty;
1662
+ delNodes;
1663
+ delEdges;
1664
+ delSegs;
1665
+ r;
1666
+ v;
1667
+ n;
1668
+ h;
1669
+ w;
1670
+ x;
1671
+ y;
1672
+ d;
1673
+ constructor({ prior, changes, options }) {
1674
+ this.options = prior?.options ?? options;
1675
+ this.changes = changes ?? emptyChanges;
1676
+ this.initFromPrior(prior);
1677
+ this.r = this.options.orientation === "BT" || this.options.orientation === "RL";
1678
+ this.v = this.options.orientation === "TB" || this.options.orientation === "BT";
1679
+ this.h = this.v ? "h" : "w";
1680
+ this.w = this.v ? "w" : "h";
1681
+ this.x = this.v ? "x" : "y";
1682
+ this.y = this.v ? "y" : "x";
1683
+ this.d = {
1684
+ x: this.v ? 0 : this.r ? -1 : 1,
1685
+ y: this.v ? this.r ? -1 : 1 : 0
1624
1686
  };
1625
1687
  const natAligns = { TB: "top", BT: "bottom", LR: "left", RL: "right" };
1626
1688
  if (this.options.nodeAlign == "natural")
1627
- this._natural = true;
1689
+ this.n = true;
1628
1690
  else
1629
- this._natural = natAligns[this.options.orientation] == this.options.nodeAlign;
1630
- if (this._dirty) this._update();
1691
+ this.n = natAligns[this.options.orientation] == this.options.nodeAlign;
1692
+ if (this.dirty) this.processUpdate();
1693
+ }
1694
+ processUpdate() {
1695
+ try {
1696
+ this.beginMutate();
1697
+ this.applyChanges();
1698
+ Cycles.checkCycles(this);
1699
+ Layers.updateLayers(this);
1700
+ Dummy.updateDummies(this);
1701
+ Dummy.mergeDummies(this);
1702
+ Layout.positionNodes(this);
1703
+ Layout.alignAll(this);
1704
+ Lines.trackEdges(this);
1705
+ Layout.getCoords(this);
1706
+ Lines.pathEdges(this);
1707
+ } catch (e) {
1708
+ this.initFromPrior(this.prior);
1709
+ throw e;
1710
+ } finally {
1711
+ this.endMutate();
1712
+ }
1713
+ }
1714
+ applyChanges() {
1715
+ for (const edge of this.changes.removedEdges)
1716
+ Edge.del(this, edge);
1717
+ for (const node of this.changes.removedNodes)
1718
+ Node.del(this, node);
1719
+ for (const node of this.changes.addedNodes)
1720
+ Node.addNormal(this, node);
1721
+ for (const edge of this.changes.addedEdges)
1722
+ Edge.add(this, edge);
1723
+ for (const node of this.changes.updatedNodes)
1724
+ Node.update(this, node);
1725
+ for (const edge of this.changes.updatedEdges)
1726
+ Edge.update(this, edge);
1727
+ }
1728
+ layerAt(index) {
1729
+ while (index >= this.layerList.size)
1730
+ this.addLayer();
1731
+ const layerId = this.layerList.get(index);
1732
+ return this.getLayer(layerId);
1733
+ }
1734
+ addLayer() {
1735
+ const id = `${Layer.prefix}${this.nextLayerId++}`;
1736
+ this.layers.set(id, new Layer({
1737
+ id,
1738
+ index: this.layerList.size,
1739
+ nodeIds: (0, import_immutable8.Set)()
1740
+ }));
1741
+ this.layerList.push(id);
1742
+ this.dirtyLayers.add(id);
1743
+ }
1744
+ isEdgeId(id) {
1745
+ return id.startsWith(Edge.prefix);
1746
+ }
1747
+ isSegId(id) {
1748
+ return id.startsWith(Seg.prefix);
1749
+ }
1750
+ getNode(nodeId) {
1751
+ const node = this.nodes.get(nodeId);
1752
+ if (!node) throw new Error(`cannot find node ${nodeId}`);
1753
+ return node;
1754
+ }
1755
+ getEdge(edgeId) {
1756
+ const edge = this.edges.get(edgeId);
1757
+ if (!edge) throw new Error(`cannot find edge ${edgeId}`);
1758
+ return edge;
1759
+ }
1760
+ getSeg(segId) {
1761
+ const seg = this.segs.get(segId);
1762
+ if (!seg) throw new Error(`cannot find seg ${segId}`);
1763
+ return seg;
1764
+ }
1765
+ getLayer(layerId) {
1766
+ const layer = this.layers.get(layerId);
1767
+ if (!layer) throw new Error(`cannot find layer ${layerId}`);
1768
+ return layer;
1769
+ }
1770
+ layerIndex(nodeId) {
1771
+ return this.getNode(nodeId).layerIndex(this);
1772
+ }
1773
+ getRel(relId) {
1774
+ return this.isSegId(relId) ? this.getSeg(relId) : this.getEdge(relId);
1775
+ }
1776
+ *getNodes(includeDummy = false) {
1777
+ const gen = this.nodes.values();
1778
+ for (const node of this.nodes.values())
1779
+ if (includeDummy || !node.isDummy)
1780
+ yield node;
1781
+ }
1782
+ *getEdges() {
1783
+ yield* this.edges.values();
1784
+ }
1785
+ *getSegs() {
1786
+ yield* this.segs.values();
1787
+ }
1788
+ withMutations(callback) {
1789
+ const mut = new Mutator();
1790
+ callback(mut);
1791
+ return new _Graph({ prior: this, changes: mut.changes });
1792
+ }
1793
+ addNode(node) {
1794
+ return this.withMutations((mutator) => {
1795
+ mutator.addNode(node);
1796
+ });
1797
+ }
1798
+ addNodes(...nodes) {
1799
+ return this.withMutations((mutator) => {
1800
+ nodes.forEach((node) => mutator.addNode(node));
1801
+ });
1802
+ }
1803
+ removeNodes(...nodes) {
1804
+ return this.withMutations((mutator) => {
1805
+ nodes.forEach((node) => mutator.removeNode(node));
1806
+ });
1807
+ }
1808
+ removeNode(node) {
1809
+ return this.withMutations((mutator) => {
1810
+ mutator.removeNode(node);
1811
+ });
1812
+ }
1813
+ addEdges(...edges) {
1814
+ return this.withMutations((mutator) => {
1815
+ edges.forEach((edge) => mutator.addEdge(edge));
1816
+ });
1817
+ }
1818
+ addEdge(edge) {
1819
+ return this.withMutations((mutator) => {
1820
+ mutator.addEdge(edge);
1821
+ });
1822
+ }
1823
+ removeEdges(...edges) {
1824
+ return this.withMutations((mutator) => {
1825
+ edges.forEach((edge) => mutator.removeEdge(edge));
1826
+ });
1827
+ }
1828
+ removeEdge(edge) {
1829
+ return this.withMutations((mutator) => {
1830
+ mutator.removeEdge(edge);
1831
+ });
1832
+ }
1833
+ mutateNode(node) {
1834
+ if (node.mutable) return node;
1835
+ node = node.asMutable().set("mutable", true);
1836
+ this.nodes.set(node.id, node);
1837
+ this.dirtyNodes.add(node.id);
1838
+ return node;
1839
+ }
1840
+ mutateEdge(edge) {
1841
+ if (edge.mutable) return edge;
1842
+ edge = edge.asMutable().set("mutable", true);
1843
+ this.edges.set(edge.id, edge);
1844
+ this.dirtyEdges.add(edge.id);
1845
+ return edge;
1846
+ }
1847
+ mutateLayer(layer) {
1848
+ if (layer.mutable) return layer;
1849
+ layer = layer.asMutable().set("mutable", true);
1850
+ this.layers.set(layer.id, layer);
1851
+ this.dirtyLayers.add(layer.id);
1852
+ return layer;
1853
+ }
1854
+ mutateSeg(seg) {
1855
+ if (seg.mutable) return seg;
1856
+ seg = seg.asMutable().set("mutable", true);
1857
+ this.segs.set(seg.id, seg);
1858
+ this.dirtySegs.add(seg.id);
1859
+ return seg;
1860
+ }
1861
+ initFromPrior(prior) {
1862
+ this.nodes = prior?.nodes ?? (0, import_immutable8.Map)();
1863
+ this.edges = prior?.edges ?? (0, import_immutable8.Map)();
1864
+ this.layers = prior?.layers ?? (0, import_immutable8.Map)();
1865
+ this.layerList = prior?.layerList ?? (0, import_immutable8.List)();
1866
+ this.segs = prior?.segs ?? (0, import_immutable8.Map)();
1867
+ this.nextLayerId = prior?.nextLayerId ?? 0;
1868
+ this.nextDummyId = prior?.nextDummyId ?? 0;
1869
+ this.prior = prior;
1870
+ this.dirtyNodes = /* @__PURE__ */ new Set();
1871
+ this.dirtyEdges = /* @__PURE__ */ new Set();
1872
+ this.dirtyLayers = /* @__PURE__ */ new Set();
1873
+ this.dirtySegs = /* @__PURE__ */ new Set();
1874
+ this.delNodes = /* @__PURE__ */ new Set();
1875
+ this.delEdges = /* @__PURE__ */ new Set();
1876
+ this.delSegs = /* @__PURE__ */ new Set();
1877
+ this.dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.updatedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1878
+ }
1879
+ beginMutate() {
1880
+ this.nodes = this.nodes.asMutable();
1881
+ this.edges = this.edges.asMutable();
1882
+ this.layers = this.layers.asMutable();
1883
+ this.layerList = this.layerList.asMutable();
1884
+ this.segs = this.segs.asMutable();
1885
+ }
1886
+ endMutate() {
1887
+ for (const nodeId of this.dirtyNodes)
1888
+ if (this.nodes.has(nodeId))
1889
+ this.nodes.set(nodeId, this.nodes.get(nodeId).final());
1890
+ for (const edgeId of this.dirtyEdges)
1891
+ if (this.edges.has(edgeId))
1892
+ this.edges.set(edgeId, this.edges.get(edgeId).final());
1893
+ for (const segId of this.dirtySegs)
1894
+ if (this.segs.has(segId))
1895
+ this.segs.set(segId, this.segs.get(segId).final());
1896
+ for (const layerId of this.dirtyLayers)
1897
+ if (this.layers.has(layerId))
1898
+ this.layers.set(layerId, this.layers.get(layerId).final());
1899
+ this.nodes = this.nodes.asImmutable();
1900
+ this.edges = this.edges.asImmutable();
1901
+ this.layers = this.layers.asImmutable();
1902
+ this.layerList = this.layerList.asImmutable();
1903
+ this.segs = this.segs.asImmutable();
1904
+ }
1905
+ };
1906
+
1907
+ // src/common.ts
1908
+ var screenPos = (x, y) => ({ x, y });
1909
+ var canvasPos = (x, y) => ({ x, y });
1910
+ var graphPos = (x, y) => ({ x, y });
1911
+
1912
+ // src/canvas/node.tsx
1913
+ var import_node3 = __toESM(require("./node.css?raw"), 1);
1914
+
1915
+ // src/canvas/styler.ts
1916
+ var injected = {};
1917
+ function styler(name, styles4, prefix) {
1918
+ if (prefix === "g3p" && !injected[name]) {
1919
+ const style = document.createElement("style");
1920
+ style.textContent = styles4;
1921
+ document.head.appendChild(style);
1922
+ injected[name] = true;
1923
+ }
1924
+ return (str, condition) => {
1925
+ if (!(condition ?? true)) return "";
1926
+ const parts = str.split(/\s+/);
1927
+ const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
1928
+ return fixed.join(" ");
1929
+ };
1930
+ }
1931
+
1932
+ // src/canvas/node.tsx
1933
+ var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
1934
+ var log8 = logger("canvas");
1935
+ var Node2 = class {
1936
+ selected;
1937
+ hovered;
1938
+ container;
1939
+ content;
1940
+ canvas;
1941
+ data;
1942
+ classPrefix;
1943
+ isDummy;
1944
+ pos;
1945
+ constructor(canvas, data, isDummy = false) {
1946
+ this.canvas = canvas;
1947
+ this.data = data;
1948
+ this.selected = false;
1949
+ this.hovered = false;
1950
+ this.classPrefix = canvas.classPrefix;
1951
+ this.isDummy = isDummy;
1952
+ if (this.isDummy) {
1953
+ const size = canvas.dummyNodeSize;
1954
+ } else {
1955
+ const render = data.render ?? canvas.renderNode;
1956
+ this.content = this.renderContent(render(data.data));
1957
+ }
1958
+ }
1959
+ remove() {
1960
+ this.container.remove();
1961
+ }
1962
+ append() {
1963
+ console.log("append", this);
1964
+ this.canvas.group.appendChild(this.container);
1965
+ }
1966
+ needsContentSize() {
1967
+ return !this.isDummy && this.content instanceof HTMLElement;
1968
+ }
1969
+ needsContainerSize() {
1970
+ return !this.isDummy;
1971
+ }
1972
+ handleClick(e) {
1973
+ e.stopPropagation();
1974
+ }
1975
+ handleMouseEnter(e) {
1976
+ }
1977
+ handleMouseLeave(e) {
1978
+ }
1979
+ handleContextMenu(e) {
1980
+ }
1981
+ handleMouseDown(e) {
1982
+ }
1983
+ handleMouseUp(e) {
1984
+ }
1985
+ setPos(pos) {
1986
+ this.pos = pos;
1987
+ const { x, y } = pos;
1988
+ this.container.setAttribute("transform", `translate(${x}, ${y})`);
1989
+ }
1990
+ hasPorts() {
1991
+ return !!this.data?.ports?.in?.length || !!this.data?.ports?.out?.length;
1992
+ }
1993
+ renderContent(el) {
1994
+ const hasPorts = this.hasPorts();
1995
+ el = this.renderBorder(el);
1996
+ if (hasPorts)
1997
+ el = this.renderOutsidePorts(el);
1998
+ return el;
1999
+ }
2000
+ renderContainer() {
2001
+ const c = styler("node", import_node3.default, this.classPrefix);
2002
+ const hasPorts = this.hasPorts();
2003
+ const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
2004
+ const nodeType = this.data?.type;
2005
+ const typeClass = nodeType ? `g3p-node-type-${nodeType}` : "";
2006
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2007
+ "g",
2008
+ {
2009
+ className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
2010
+ onClick: (e) => this.handleClick(e),
2011
+ onMouseEnter: (e) => this.handleMouseEnter(e),
2012
+ onMouseLeave: (e) => this.handleMouseLeave(e),
2013
+ onContextMenu: (e) => this.handleContextMenu(e),
2014
+ onMouseDown: (e) => this.handleMouseDown(e),
2015
+ onMouseUp: (e) => this.handleMouseUp(e),
2016
+ style: { cursor: "pointer" },
2017
+ children: inner
2018
+ }
2019
+ );
2020
+ }
2021
+ renderForeign() {
2022
+ const { w, h } = this.data.dims;
2023
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
2024
+ }
2025
+ renderDummy() {
2026
+ const c = styler("node", import_node3.default, this.classPrefix);
2027
+ let w = this.canvas.dummyNodeSize;
2028
+ let h = this.canvas.dummyNodeSize;
2029
+ w /= 2;
2030
+ h /= 2;
2031
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
2032
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2033
+ "ellipse",
2034
+ {
2035
+ cx: w,
2036
+ cy: h,
2037
+ rx: w,
2038
+ ry: h,
2039
+ className: c("background")
2040
+ }
2041
+ ),
2042
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2043
+ "ellipse",
2044
+ {
2045
+ cx: w,
2046
+ cy: h,
2047
+ rx: w,
2048
+ ry: h,
2049
+ fill: "none",
2050
+ className: c("border")
2051
+ }
2052
+ )
2053
+ ] });
2054
+ }
2055
+ measure(isVertical) {
2056
+ const rect = this.content.getBoundingClientRect();
2057
+ const data = this.data;
2058
+ data.dims = { w: rect.width, h: rect.height };
2059
+ for (const dir of ["in", "out"]) {
2060
+ const ports = data.ports?.[dir];
2061
+ if (!ports) continue;
2062
+ for (const port of ports) {
2063
+ const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2064
+ if (!el) continue;
2065
+ const portRect = el.getBoundingClientRect();
2066
+ if (isVertical) {
2067
+ port.offset = portRect.left - rect.left;
2068
+ port.size = portRect.width;
2069
+ } else {
2070
+ port.offset = portRect.top - rect.top;
2071
+ port.size = portRect.height;
2072
+ }
2073
+ }
2074
+ }
2075
+ }
2076
+ getPortPosition(dir) {
2077
+ const o = this.canvas.orientation;
2078
+ if (dir === "in") {
2079
+ if (o === "TB") return "top";
2080
+ if (o === "BT") return "bottom";
2081
+ if (o === "LR") return "left";
2082
+ return "right";
2083
+ } else {
2084
+ if (o === "TB") return "bottom";
2085
+ if (o === "BT") return "top";
2086
+ if (o === "LR") return "right";
2087
+ return "left";
2088
+ }
2089
+ }
2090
+ isVerticalOrientation() {
2091
+ const o = this.canvas.orientation;
2092
+ return o === "TB" || o === "BT";
2093
+ }
2094
+ isReversedOrientation() {
2095
+ const o = this.canvas.orientation;
2096
+ return o === "BT" || o === "RL";
2097
+ }
2098
+ renderPortRow(dir, inout) {
2099
+ const ports = this.data?.ports?.[dir];
2100
+ if (!ports?.length) return null;
2101
+ const c = styler("node", import_node3.default, this.classPrefix);
2102
+ const pos = this.getPortPosition(dir);
2103
+ const isVertical = this.isVerticalOrientation();
2104
+ const layoutClass = isVertical ? "row" : "col";
2105
+ const rotateLabels = false;
2106
+ const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2107
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2108
+ "div",
2109
+ {
2110
+ id: `g3p-port-${this.data.id}-${port.id}`,
2111
+ className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2112
+ children: port.label ?? port.id
2113
+ }
2114
+ )) });
2115
+ }
2116
+ renderInsidePorts(el) {
2117
+ const c = styler("node", import_node3.default, this.classPrefix);
2118
+ const isVertical = this.isVerticalOrientation();
2119
+ const isReversed = this.isReversedOrientation();
2120
+ let inPorts = this.renderPortRow("in", "in");
2121
+ let outPorts = this.renderPortRow("out", "in");
2122
+ if (!inPorts && !outPorts) return el;
2123
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2124
+ const wrapperClass = isVertical ? "v" : "h";
2125
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2126
+ inPorts,
2127
+ el,
2128
+ outPorts
2129
+ ] });
2130
+ }
2131
+ renderOutsidePorts(el) {
2132
+ const c = styler("node", import_node3.default, this.classPrefix);
2133
+ const isVertical = this.isVerticalOrientation();
2134
+ const isReversed = this.isReversedOrientation();
2135
+ let inPorts = this.renderPortRow("in", "out");
2136
+ let outPorts = this.renderPortRow("out", "out");
2137
+ if (!inPorts && !outPorts) return el;
2138
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2139
+ const wrapperClass = isVertical ? "v" : "h";
2140
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2141
+ inPorts,
2142
+ el,
2143
+ outPorts
2144
+ ] });
2145
+ }
2146
+ renderBorder(el) {
2147
+ const c = styler("node", import_node3.default, this.classPrefix);
2148
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: c("border"), children: el });
2149
+ }
2150
+ };
2151
+
2152
+ // src/canvas/seg.tsx
2153
+ var import_seg3 = __toESM(require("./seg.css?raw"), 1);
2154
+ var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
2155
+ var log9 = logger("canvas");
2156
+ var Seg2 = class {
2157
+ id;
2158
+ selected;
2159
+ hovered;
2160
+ canvas;
2161
+ classPrefix;
2162
+ type;
2163
+ svg;
2164
+ el;
2165
+ source;
2166
+ target;
2167
+ constructor(canvas, data, g) {
2168
+ this.id = data.id;
2169
+ this.canvas = canvas;
2170
+ this.selected = false;
2171
+ this.hovered = false;
2172
+ this.svg = data.svg;
2173
+ this.classPrefix = canvas.classPrefix;
2174
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2175
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2176
+ this.type = data.type;
2177
+ this.el = this.render();
2178
+ }
2179
+ handleClick(e) {
2180
+ e.stopPropagation();
2181
+ }
2182
+ handleMouseEnter(e) {
2183
+ }
2184
+ handleMouseLeave(e) {
2185
+ }
2186
+ handleContextMenu(e) {
2187
+ }
2188
+ append() {
2189
+ this.canvas.group.appendChild(this.el);
2190
+ }
2191
+ remove() {
2192
+ this.el.remove();
2193
+ }
2194
+ update(data) {
2195
+ this.svg = data.svg;
2196
+ this.type = data.type;
2197
+ this.source = data.source;
2198
+ this.target = data.target;
2199
+ this.remove();
2200
+ this.el = this.render();
2201
+ this.append();
2202
+ }
2203
+ render() {
2204
+ const c = styler("seg", import_seg3.default, this.classPrefix);
2205
+ let { source, target } = normalize(this);
2206
+ if (this.source.isDummy) source = void 0;
2207
+ if (this.target.isDummy) target = void 0;
2208
+ const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
2209
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2210
+ "g",
2211
+ {
2212
+ ref: (el) => this.el = el,
2213
+ id: `g3p-seg-${this.id}`,
2214
+ className: `${c("container")} ${typeClass}`.trim(),
2215
+ onClick: this.handleClick.bind(this),
2216
+ onMouseEnter: this.handleMouseEnter.bind(this),
2217
+ onMouseLeave: this.handleMouseLeave.bind(this),
2218
+ onContextMenu: this.handleContextMenu.bind(this),
2219
+ children: [
2220
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2221
+ "path",
2222
+ {
2223
+ d: this.svg,
2224
+ fill: "none",
2225
+ className: c("line"),
2226
+ markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2227
+ markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2228
+ }
2229
+ ),
2230
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2231
+ "path",
2232
+ {
2233
+ d: this.svg,
2234
+ stroke: "transparent",
2235
+ fill: "none",
2236
+ className: c("hitbox"),
2237
+ style: { cursor: "pointer" }
2238
+ }
2239
+ )
2240
+ ]
2241
+ }
2242
+ );
2243
+ }
2244
+ };
2245
+
2246
+ // src/canvas/canvas.tsx
2247
+ var import_canvas = __toESM(require("./canvas.css?raw"), 1);
2248
+ var import_zoom = __toESM(require("./zoom.css?raw"), 1);
2249
+ var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2250
+ var log10 = logger("canvas");
2251
+ var themeVarMap = {
2252
+ // Canvas
2253
+ bg: "--g3p-bg",
2254
+ shadow: "--g3p-shadow",
2255
+ // Node
2256
+ border: "--g3p-border",
2257
+ borderHover: "--g3p-border-hover",
2258
+ borderSelected: "--g3p-border-selected",
2259
+ text: "--g3p-text",
2260
+ textMuted: "--g3p-text-muted",
2261
+ // Port
2262
+ bgHover: "--g3p-port-bg-hover",
2263
+ // Edge
2264
+ color: "--g3p-edge-color"
2265
+ };
2266
+ function themeToCSS(theme, selector, prefix = "") {
2267
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2268
+ if (!entries.length) return "";
2269
+ let css = `${selector} {
2270
+ `;
2271
+ for (const [key, value] of entries) {
2272
+ let cssVar = themeVarMap[key];
2273
+ if (key === "bg" && prefix === "node") {
2274
+ cssVar = "--g3p-bg-node";
2275
+ } else if (key === "bg" && prefix === "port") {
2276
+ cssVar = "--g3p-port-bg";
2277
+ }
2278
+ if (cssVar) {
2279
+ css += ` ${cssVar}: ${value};
2280
+ `;
2281
+ }
2282
+ }
2283
+ css += "}\n";
2284
+ return css;
2285
+ }
2286
+ var Canvas = class {
2287
+ container;
2288
+ root;
2289
+ group;
2290
+ transform;
2291
+ bounds;
2292
+ measurement;
2293
+ allNodes;
2294
+ curNodes;
2295
+ curSegs;
2296
+ updating;
2297
+ // Pan-zoom state
2298
+ isPanning = false;
2299
+ panStart = null;
2300
+ transformStart = null;
2301
+ panScale = null;
2302
+ zoomControls;
2303
+ constructor(options) {
2304
+ Object.assign(this, options);
2305
+ this.allNodes = /* @__PURE__ */ new Map();
2306
+ this.curNodes = /* @__PURE__ */ new Map();
2307
+ this.curSegs = /* @__PURE__ */ new Map();
2308
+ this.updating = false;
2309
+ this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2310
+ this.transform = { x: 0, y: 0, scale: 1 };
2311
+ this.createMeasurementContainer();
2312
+ this.createCanvasContainer();
2313
+ if (this.panZoom) this.setupPanZoom();
2314
+ }
2315
+ createMeasurementContainer() {
2316
+ this.measurement = document.createElement("div");
2317
+ this.measurement.style.cssText = `
2318
+ position: absolute;
2319
+ left: -9999px;
2320
+ top: -9999px;
2321
+ visibility: hidden;
2322
+ pointer-events: none;
2323
+ `;
2324
+ document.body.appendChild(this.measurement);
2325
+ }
2326
+ getNode(key) {
2327
+ const node = this.allNodes.get(key);
2328
+ if (!node) throw new Error(`node not found: ${key}`);
2329
+ return node;
2330
+ }
2331
+ update() {
2332
+ let bx0 = Infinity, by0 = Infinity;
2333
+ let bx1 = -Infinity, by1 = -Infinity;
2334
+ for (const node of this.curNodes.values()) {
2335
+ const { x, y } = node.pos;
2336
+ const { w, h } = node.data.dims;
2337
+ const nx0 = x, nx1 = x + w;
2338
+ const ny0 = y, ny1 = y + h;
2339
+ bx0 = Math.min(bx0, nx0);
2340
+ by0 = Math.min(by0, ny0);
2341
+ bx1 = Math.max(bx1, nx1);
2342
+ by1 = Math.max(by1, ny1);
2343
+ }
2344
+ this.bounds = { min: { x: bx0, y: by0 }, max: { x: bx1, y: by1 } };
2345
+ this.root.setAttribute("viewBox", this.viewBox());
2346
+ }
2347
+ addNode(gnode) {
2348
+ if (this.curNodes.has(gnode.id))
2349
+ throw new Error("node already exists");
2350
+ const { key } = gnode;
2351
+ let node;
2352
+ if (gnode.isDummy) {
2353
+ node = new Node2(this, gnode, true);
2354
+ node.renderContainer();
2355
+ node.setPos(gnode.pos);
2356
+ this.allNodes.set(key, node);
2357
+ } else {
2358
+ if (!this.allNodes.has(key))
2359
+ throw new Error("node has not been measured");
2360
+ node = this.getNode(key);
2361
+ }
2362
+ this.curNodes.set(gnode.id, node);
2363
+ node.append();
2364
+ }
2365
+ updateNode(gnode) {
2366
+ if (gnode.isDummy) throw new Error("dummy node cannot be updated");
2367
+ const node = this.getNode(gnode.key);
2368
+ const cur = this.curNodes.get(gnode.id);
2369
+ if (cur) cur.remove();
2370
+ this.curNodes.set(gnode.id, node);
2371
+ node.append();
2372
+ }
2373
+ deleteNode(gnode) {
2374
+ const node = this.getNode(gnode.key);
2375
+ this.curNodes.delete(gnode.id);
2376
+ node.remove();
2377
+ }
2378
+ addSeg(gseg, g) {
2379
+ if (this.curSegs.has(gseg.id))
2380
+ throw new Error("seg already exists");
2381
+ const seg = new Seg2(this, gseg, g);
2382
+ this.curSegs.set(gseg.id, seg);
2383
+ seg.append();
2384
+ }
2385
+ updateSeg(gseg) {
2386
+ const seg = this.curSegs.get(gseg.id);
2387
+ if (!seg) throw new Error("seg not found");
2388
+ seg.update(gseg);
2389
+ }
2390
+ deleteSeg(gseg) {
2391
+ const seg = this.curSegs.get(gseg.id);
2392
+ if (!seg) throw new Error("seg not found");
2393
+ this.curSegs.delete(gseg.id);
2394
+ seg.remove();
2395
+ }
2396
+ async measureNodes(nodes) {
2397
+ const newNodes = /* @__PURE__ */ new Map();
2398
+ for (const data of nodes) {
2399
+ const node = new Node2(this, data);
2400
+ newNodes.set(data.data, node);
2401
+ this.measurement.appendChild(node.content);
2402
+ }
2403
+ await new Promise(requestAnimationFrame);
2404
+ const isVertical = this.orientation === "TB" || this.orientation === "BT";
2405
+ for (const node of newNodes.values()) {
2406
+ node.measure(isVertical);
2407
+ const { id, version } = node.data;
2408
+ const key = `${id}:${version}`;
2409
+ this.allNodes.set(key, node);
2410
+ node.renderContainer();
2411
+ }
2412
+ this.measurement.innerHTML = "";
2413
+ return newNodes;
2414
+ }
2415
+ onClick(e) {
2416
+ console.log("click", e);
2417
+ }
2418
+ onContextMenu(e) {
2419
+ console.log("context menu", e);
2420
+ }
2421
+ groupTransform() {
2422
+ return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2423
+ }
2424
+ viewBox() {
2425
+ const p = this.padding;
2426
+ const x = this.bounds.min.x - p;
2427
+ const y = this.bounds.min.y - p;
2428
+ const w = this.bounds.max.x - this.bounds.min.x + p * 2;
2429
+ const h = this.bounds.max.y - this.bounds.min.y + p * 2;
2430
+ return `${x} ${y} ${w} ${h}`;
2431
+ }
2432
+ generateDynamicStyles() {
2433
+ let css = "";
2434
+ const prefix = this.classPrefix;
2435
+ css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
2436
+ for (const [type, vars] of Object.entries(this.nodeTypes)) {
2437
+ css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
2438
+ }
2439
+ for (const [type, vars] of Object.entries(this.edgeTypes)) {
2440
+ css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
2441
+ }
2442
+ return css;
2443
+ }
2444
+ createCanvasContainer() {
2445
+ const markerStyleEl = document.createElement("style");
2446
+ markerStyleEl.textContent = import_marker.default;
2447
+ document.head.appendChild(markerStyleEl);
2448
+ const zoomStyleEl = document.createElement("style");
2449
+ zoomStyleEl.textContent = import_zoom.default;
2450
+ document.head.appendChild(zoomStyleEl);
2451
+ const dynamicStyles = this.generateDynamicStyles();
2452
+ if (dynamicStyles) {
2453
+ const themeStyleEl = document.createElement("style");
2454
+ themeStyleEl.textContent = dynamicStyles;
2455
+ document.head.appendChild(themeStyleEl);
2456
+ }
2457
+ const c = styler("canvas", import_canvas.default, this.classPrefix);
2458
+ const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2459
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2460
+ "div",
2461
+ {
2462
+ className: `${c("container")} ${colorModeClass}`.trim(),
2463
+ ref: (el) => this.container = el,
2464
+ onContextMenu: this.onContextMenu.bind(this),
2465
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2466
+ "svg",
2467
+ {
2468
+ ref: (el) => this.root = el,
2469
+ className: c("root"),
2470
+ width: this.width,
2471
+ height: this.height,
2472
+ viewBox: this.viewBox(),
2473
+ preserveAspectRatio: "xMidYMid meet",
2474
+ onClick: this.onClick.bind(this),
2475
+ children: [
2476
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2477
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2478
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
2479
+ ] }),
2480
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2481
+ "g",
2482
+ {
2483
+ ref: (el) => this.group = el,
2484
+ transform: this.groupTransform()
2485
+ }
2486
+ )
2487
+ ]
2488
+ }
2489
+ )
2490
+ }
2491
+ );
2492
+ }
2493
+ // ==================== Pan-Zoom ====================
2494
+ setupPanZoom() {
2495
+ this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false });
2496
+ this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2497
+ document.addEventListener("mousemove", this.onMouseMove.bind(this));
2498
+ document.addEventListener("mouseup", this.onMouseUp.bind(this));
2499
+ this.createZoomControls();
2500
+ }
2501
+ /** Convert screen coordinates to canvas-relative coordinates */
2502
+ screenToCanvas(screen) {
2503
+ const rect = this.container.getBoundingClientRect();
2504
+ return canvasPos(screen.x - rect.left, screen.y - rect.top);
2505
+ }
2506
+ /**
2507
+ * Get the effective scale from canvas pixels to graph units,
2508
+ * accounting for preserveAspectRatio="xMidYMid meet" which uses
2509
+ * the smaller scale (to fit) and centers the content.
2510
+ */
2511
+ getEffectiveScale() {
2512
+ const vb = this.currentViewBox();
2513
+ const rect = this.container.getBoundingClientRect();
2514
+ const scaleX = vb.w / rect.width;
2515
+ const scaleY = vb.h / rect.height;
2516
+ const scale = Math.max(scaleX, scaleY);
2517
+ const actualW = rect.width * scale;
2518
+ const actualH = rect.height * scale;
2519
+ const offsetX = (actualW - vb.w) / 2;
2520
+ const offsetY = (actualH - vb.h) / 2;
2521
+ return { scale, offsetX, offsetY };
2522
+ }
2523
+ /** Convert canvas coordinates to graph coordinates */
2524
+ canvasToGraph(canvas) {
2525
+ const vb = this.currentViewBox();
2526
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
2527
+ return graphPos(
2528
+ vb.x - offsetX + canvas.x * scale,
2529
+ vb.y - offsetY + canvas.y * scale
2530
+ );
2531
+ }
2532
+ /** Get current viewBox as an object */
2533
+ currentViewBox() {
2534
+ const p = this.padding;
2535
+ const t = this.transform;
2536
+ const baseX = this.bounds.min.x - p;
2537
+ const baseY = this.bounds.min.y - p;
2538
+ const baseW = this.bounds.max.x - this.bounds.min.x + p * 2;
2539
+ const baseH = this.bounds.max.y - this.bounds.min.y + p * 2;
2540
+ const cx = baseX + baseW / 2;
2541
+ const cy = baseY + baseH / 2;
2542
+ const w = baseW / t.scale;
2543
+ const h = baseH / t.scale;
2544
+ const x = cx - w / 2 - t.x;
2545
+ const y = cy - h / 2 - t.y;
2546
+ return { x, y, w, h };
2547
+ }
2548
+ onWheel(e) {
2549
+ e.preventDefault();
2550
+ const zoomFactor = 1.1;
2551
+ const delta = e.deltaY > 0 ? 1 / zoomFactor : zoomFactor;
2552
+ const screenCursor = screenPos(e.clientX, e.clientY);
2553
+ const canvasCursor = this.screenToCanvas(screenCursor);
2554
+ const graphCursor = this.canvasToGraph(canvasCursor);
2555
+ const oldScale = this.transform.scale;
2556
+ const newScale = Math.max(0.1, Math.min(10, oldScale * delta));
2557
+ this.transform.scale = newScale;
2558
+ const newGraphCursor = this.canvasToGraph(canvasCursor);
2559
+ this.transform.x += newGraphCursor.x - graphCursor.x;
2560
+ this.transform.y += newGraphCursor.y - graphCursor.y;
2561
+ this.applyTransform();
2562
+ }
2563
+ onMouseDown(e) {
2564
+ if (e.button !== 0) return;
2565
+ if (e.target.closest(".g3p-zoom-controls")) return;
2566
+ this.isPanning = true;
2567
+ this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2568
+ this.transformStart = { ...this.transform };
2569
+ const { scale } = this.getEffectiveScale();
2570
+ this.panScale = { x: scale, y: scale };
2571
+ this.container.style.cursor = "grabbing";
2572
+ e.preventDefault();
2573
+ }
2574
+ onMouseMove(e) {
2575
+ if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
2576
+ const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2577
+ const dx = current.x - this.panStart.x;
2578
+ const dy = current.y - this.panStart.y;
2579
+ this.transform.x = this.transformStart.x + dx * this.panScale.x;
2580
+ this.transform.y = this.transformStart.y + dy * this.panScale.y;
2581
+ this.applyTransform();
2582
+ }
2583
+ onMouseUp(e) {
2584
+ if (!this.isPanning) return;
2585
+ this.isPanning = false;
2586
+ this.panStart = null;
2587
+ this.transformStart = null;
2588
+ this.panScale = null;
2589
+ this.container.style.cursor = "";
2590
+ }
2591
+ applyTransform() {
2592
+ const vb = this.currentViewBox();
2593
+ this.root.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
2594
+ this.updateZoomLevel();
2595
+ }
2596
+ createZoomControls() {
2597
+ const c = styler("zoom", import_zoom.default, this.classPrefix);
2598
+ this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: c("controls"), children: [
2599
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2602
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
2603
+ ] });
2604
+ this.container.appendChild(this.zoomControls);
2605
+ }
2606
+ updateZoomLevel() {
2607
+ const level = this.container.querySelector("#g3p-zoom-level");
2608
+ if (level) {
2609
+ level.textContent = `${Math.round(this.transform.scale * 100)}%`;
2610
+ }
2611
+ }
2612
+ zoomIn() {
2613
+ this.transform.scale = Math.min(10, this.transform.scale * 1.2);
2614
+ this.applyTransform();
2615
+ }
2616
+ zoomOut() {
2617
+ this.transform.scale = Math.max(0.1, this.transform.scale / 1.2);
2618
+ this.applyTransform();
2619
+ }
2620
+ zoomReset() {
2621
+ this.transform = { x: 0, y: 0, scale: 1 };
2622
+ this.applyTransform();
2623
+ }
2624
+ };
2625
+
2626
+ // src/canvas/render-node.tsx
2627
+ var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2628
+ function renderNode(node) {
2629
+ if (typeof node == "string") node = { id: node };
2630
+ const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
2631
+ const detail = node?.detail ?? node?.description ?? node?.subtitle;
2632
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-node-default", children: [
2633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-title", children: title }),
2634
+ detail && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-detail", children: detail })
2635
+ ] });
2636
+ }
2637
+
2638
+ // src/api/defaults.ts
2639
+ function applyDefaults(options) {
2640
+ const { graph: graph2, canvas, props } = defaults();
2641
+ return {
2642
+ graph: { ...graph2, ...options?.graph },
2643
+ canvas: { ...canvas, ...options?.canvas },
2644
+ props: { ...props, ...options?.props }
2645
+ };
2646
+ }
2647
+ function defaults() {
2648
+ return {
2649
+ graph: {
2650
+ mergeOrder: ["target", "source"],
2651
+ nodeMargin: 15,
2652
+ dummyNodeSize: 15,
2653
+ nodeAlign: "natural",
2654
+ edgeSpacing: 10,
2655
+ turnRadius: 10,
2656
+ orientation: "TB",
2657
+ layerMargin: 5,
2658
+ alignIterations: 5,
2659
+ alignThreshold: 10,
2660
+ separateTrackSets: true,
2661
+ markerSize: 10,
2662
+ layoutSteps: null
2663
+ },
2664
+ canvas: {
2665
+ renderNode,
2666
+ classPrefix: "g3p",
2667
+ width: "100%",
2668
+ height: "100%",
2669
+ padding: 20,
2670
+ editable: false,
2671
+ panZoom: true,
2672
+ markerSize: 10,
2673
+ colorMode: "system",
2674
+ theme: {},
2675
+ nodeTypes: {},
2676
+ edgeTypes: {}
2677
+ },
2678
+ props: {}
2679
+ };
2680
+ }
2681
+
2682
+ // src/api/updater.ts
2683
+ var Updater = class _Updater {
2684
+ update;
2685
+ constructor() {
2686
+ this.update = {
2687
+ addNodes: [],
2688
+ removeNodes: [],
2689
+ updateNodes: [],
2690
+ addEdges: [],
2691
+ removeEdges: [],
2692
+ updateEdges: []
2693
+ };
2694
+ }
2695
+ describe(desc) {
2696
+ this.update.description = desc;
2697
+ return this;
2698
+ }
2699
+ addNode(node) {
2700
+ this.update.addNodes.push(node);
2701
+ return this;
2702
+ }
2703
+ deleteNode(node) {
2704
+ this.update.removeNodes.push(node);
2705
+ return this;
2706
+ }
2707
+ updateNode(node) {
2708
+ this.update.updateNodes.push(node);
2709
+ return this;
2710
+ }
2711
+ addEdge(edge) {
2712
+ this.update.addEdges.push(edge);
2713
+ return this;
2714
+ }
2715
+ deleteEdge(edge) {
2716
+ this.update.removeEdges.push(edge);
2717
+ return this;
2718
+ }
2719
+ updateEdge(edge) {
2720
+ this.update.updateEdges.push(edge);
2721
+ return this;
2722
+ }
2723
+ static add(nodes, edges) {
2724
+ const updater = new _Updater();
2725
+ updater.update.addNodes = nodes;
2726
+ updater.update.addEdges = edges;
2727
+ return updater;
2728
+ }
2729
+ };
2730
+
2731
+ // src/api/api.ts
2732
+ var log11 = logger("api");
2733
+ var API = class {
2734
+ state;
2735
+ seq;
2736
+ index;
2737
+ canvas;
2738
+ options;
2739
+ history;
2740
+ nodeIds;
2741
+ edgeIds;
2742
+ nodeVersions;
2743
+ nextNodeId;
2744
+ nextEdgeId;
2745
+ root;
2746
+ constructor(args) {
2747
+ this.root = args.root;
2748
+ this.options = applyDefaults(args.options);
2749
+ let graph2 = new Graph({ options: this.options.graph });
2750
+ this.state = { graph: graph2, update: null };
2751
+ this.seq = [this.state];
2752
+ this.index = 0;
2753
+ this.nodeIds = /* @__PURE__ */ new Map();
2754
+ this.edgeIds = /* @__PURE__ */ new Map();
2755
+ this.nodeVersions = /* @__PURE__ */ new Map();
2756
+ this.nextNodeId = 1;
2757
+ this.nextEdgeId = 1;
2758
+ this.canvas = new Canvas({
2759
+ ...this.options.canvas,
2760
+ dummyNodeSize: this.options.graph.dummyNodeSize,
2761
+ orientation: this.options.graph.orientation
2762
+ });
2763
+ if (args.history) {
2764
+ this.history = args.history;
2765
+ } else if (args.nodes) {
2766
+ this.history = [Updater.add(args.nodes, args.edges || []).update];
2767
+ } else {
2768
+ this.history = [];
2769
+ }
2770
+ }
2771
+ async init() {
2772
+ const root = document.getElementById(this.root);
2773
+ if (!root) throw new Error("root element not found");
2774
+ root.appendChild(this.canvas.container);
2775
+ for (const update of this.history)
2776
+ await this.applyUpdate(update);
2777
+ }
2778
+ nav(nav) {
2779
+ let newIndex;
2780
+ switch (nav) {
2781
+ case "first":
2782
+ newIndex = 0;
2783
+ break;
2784
+ case "last":
2785
+ newIndex = this.seq.length - 1;
2786
+ break;
2787
+ case "prev":
2788
+ newIndex = this.index - 1;
2789
+ break;
2790
+ case "next":
2791
+ newIndex = this.index + 1;
2792
+ break;
2793
+ }
2794
+ if (newIndex < 0 || newIndex >= this.seq.length || newIndex == this.index)
2795
+ return;
2796
+ this.applyDiff(this.index, newIndex);
2797
+ this.index = newIndex;
2798
+ this.state = this.seq[this.index];
2799
+ }
2800
+ applyDiff(oldIndex, newIndex) {
2801
+ const oldGraph = this.seq[oldIndex].graph;
2802
+ const newGraph = this.seq[newIndex].graph;
2803
+ for (const oldNode of oldGraph.nodes.values()) {
2804
+ const newNode = newGraph.nodes.get(oldNode.id);
2805
+ if (!newNode) this.canvas.deleteNode(oldNode);
2806
+ }
2807
+ for (const newNode of newGraph.nodes.values()) {
2808
+ if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
2809
+ }
2810
+ for (const oldSeg of oldGraph.segs.values()) {
2811
+ const newSeg = newGraph.segs.get(oldSeg.id);
2812
+ if (!newSeg)
2813
+ this.canvas.deleteSeg(oldSeg);
2814
+ else if (oldSeg.svg != newSeg.svg)
2815
+ this.canvas.updateSeg(newSeg);
2816
+ }
2817
+ for (const newSeg of newGraph.segs.values()) {
2818
+ if (!oldGraph.segs.has(newSeg.id))
2819
+ this.canvas.addSeg(newSeg, newGraph);
2820
+ }
2821
+ this.canvas.update();
2822
+ }
2823
+ async addNode(node) {
2824
+ await this.update((update) => update.addNode(node));
2825
+ }
2826
+ async deleteNode(node) {
2827
+ await this.update((update) => update.deleteNode(node));
2828
+ }
2829
+ async updateNode(node) {
2830
+ await this.update((update) => update.updateNode(node));
2831
+ }
2832
+ async addEdge(edge) {
2833
+ await this.update((update) => update.addEdge(edge));
2834
+ }
2835
+ async deleteEdge(edge) {
2836
+ await this.update((update) => update.deleteEdge(edge));
2837
+ }
2838
+ async applyUpdate(update) {
2839
+ log11.info("applyUpdate", update);
2840
+ const nodes = await this.measureNodes(update);
2841
+ const graph2 = this.state.graph.withMutations((mut) => {
2842
+ for (const edge of update.removeEdges ?? [])
2843
+ this._removeEdge(edge, mut);
2844
+ for (const node of update.removeNodes ?? [])
2845
+ this._removeNode(node, mut);
2846
+ for (const node of update.addNodes ?? [])
2847
+ this._addNode(nodes.get(node), mut);
2848
+ for (const node of update.updateNodes ?? [])
2849
+ this._updateNode(nodes.get(node), mut);
2850
+ for (const edge of update.addEdges ?? [])
2851
+ this._addEdge(edge, mut);
2852
+ for (const edge of update.updateEdges ?? [])
2853
+ this._updateEdge(edge, mut);
2854
+ });
2855
+ this.state = { graph: graph2, update };
2856
+ this.setNodePositions();
2857
+ this.seq.splice(this.index + 1);
2858
+ this.seq.push(this.state);
2859
+ this.nav("last");
2860
+ }
2861
+ setNodePositions() {
2862
+ const { graph: graph2 } = this.state;
2863
+ for (const nodeId of graph2.dirtyNodes) {
2864
+ const node = graph2.getNode(nodeId);
2865
+ if (!node.isDummy)
2866
+ this.canvas.getNode(node.key).setPos(node.pos);
2867
+ }
2868
+ }
2869
+ async update(callback) {
2870
+ const updater = new Updater();
2871
+ callback(updater);
2872
+ await this.applyUpdate(updater.update);
2873
+ }
2874
+ async measureNodes(update) {
2875
+ const data = [];
2876
+ for (const set of [update.updateNodes, update.addNodes])
2877
+ for (const node of set ?? [])
2878
+ data.push(this.parseNode(node, true));
2879
+ return await this.canvas.measureNodes(data);
2880
+ }
2881
+ parseNode(data, bumpVersion = false) {
2882
+ const get = this.options.props.node;
2883
+ let props;
2884
+ if (get) props = get(data);
2885
+ else if (!data) throw new Error(`invalid node ${data}`);
2886
+ else if (typeof data == "string") props = { id: data };
2887
+ else if (typeof data == "object") props = data;
2888
+ else throw new Error(`invalid node ${data}`);
2889
+ let { id, title, text, type, render } = props;
2890
+ id ??= this.getNodeId(data);
2891
+ const ports = this.parsePorts(props.ports);
2892
+ let version = this.nodeVersions.get(data);
2893
+ if (!version) version = 1;
2894
+ else if (bumpVersion) version++;
2895
+ this.nodeVersions.set(data, version);
2896
+ return { id, data, ports, title, text, type, render, version };
2897
+ }
2898
+ parseEdge(data) {
2899
+ const get = this.options.props.edge;
2900
+ let props;
2901
+ if (get) props = get(data);
2902
+ else if (!data) throw new Error(`invalid edge ${data}`);
2903
+ else if (typeof data == "string") props = this.parseStringEdge(data);
2904
+ else if (typeof data == "object") props = data;
2905
+ else throw new Error(`invalid edge ${data}`);
2906
+ let { id, source, target, type } = props;
2907
+ id ??= this.getEdgeId(data);
2908
+ source = this.parseEdgeEnd(source);
2909
+ target = this.parseEdgeEnd(target);
2910
+ const edge = { id, source, target, type, data };
2911
+ return edge;
2912
+ }
2913
+ parseEdgeEnd(end) {
2914
+ if (!end) throw new Error(`edge has an undefined source or target`);
2915
+ if (typeof end == "string") return { id: end };
2916
+ if (typeof end == "object") {
2917
+ const keys = Object.keys(end);
2918
+ const pidx = keys.indexOf("port");
2919
+ if (pidx != -1) {
2920
+ if (typeof end.port != "string") return end;
2921
+ keys.splice(pidx, 1);
2922
+ }
2923
+ if (keys.length != 1) return end;
2924
+ if (keys[0] == "id") return end;
2925
+ if (keys[0] != "node") return end;
2926
+ const id = this.nodeIds.get(end.node);
2927
+ if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
2928
+ return { id, port: end.port };
2929
+ }
2930
+ throw new Error(`invalid edge end ${end}`);
2931
+ }
2932
+ parseStringEdge(str) {
2933
+ const [source, target] = str.split(/\s*(?::|-+>?)\s*/);
2934
+ return { source, target };
2935
+ }
2936
+ parsePorts(ports) {
2937
+ const fixed = { in: null, out: null };
2938
+ for (const key of ["in", "out"]) {
2939
+ if (ports?.[key] && ports[key].length > 0)
2940
+ fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2941
+ }
2942
+ return fixed;
2943
+ }
2944
+ getNodeId(node) {
2945
+ let id = this.nodeIds.get(node);
2946
+ if (!id) {
2947
+ id = `n${this.nextNodeId++}`;
2948
+ this.nodeIds.set(node, id);
2949
+ }
2950
+ return id;
2951
+ }
2952
+ getEdgeId(edge) {
2953
+ let id = this.edgeIds.get(edge);
2954
+ if (!id) {
2955
+ id = `e${this.nextEdgeId++}`;
2956
+ this.edgeIds.set(edge, id);
2957
+ }
2958
+ return id;
2959
+ }
2960
+ _addNode(node, mut) {
2961
+ const { data, id: newId } = node.data;
2962
+ const oldId = this.nodeIds.get(data);
2963
+ console.log("addNode", node, oldId, newId);
2964
+ if (oldId && oldId != newId)
2965
+ throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2966
+ this.nodeIds.set(data, newId);
2967
+ mut.addNode(node.data);
2968
+ }
2969
+ _removeNode(node, mut) {
2970
+ const id = this.nodeIds.get(node);
2971
+ if (!id) throw new Error(`removing node ${node} which does not exist`);
2972
+ mut.removeNode({ id });
2973
+ }
2974
+ _updateNode(node, mut) {
2975
+ const { data, id: newId } = node.data;
2976
+ const oldId = this.nodeIds.get(data);
2977
+ if (!oldId) throw new Error(`updating unknown node ${node}`);
2978
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
2979
+ mut.updateNode(node.data);
2980
+ }
2981
+ _addEdge(edge, mut) {
2982
+ const data = this.parseEdge(edge);
2983
+ const id = this.edgeIds.get(edge);
2984
+ if (id && id != data.id)
2985
+ throw new Error(`edge id changed from ${id} to ${data.id}`);
2986
+ this.edgeIds.set(edge, data.id);
2987
+ mut.addEdge(data);
2988
+ }
2989
+ _removeEdge(edge, mut) {
2990
+ const id = this.edgeIds.get(edge);
2991
+ if (!id) throw new Error(`removing edge ${edge} which does not exist`);
2992
+ mut.removeEdge(this.parseEdge(edge));
2993
+ }
2994
+ _updateEdge(edge, mut) {
2995
+ const id = this.edgeIds.get(edge);
2996
+ if (!id) throw new Error(`updating unknown edge ${edge}`);
2997
+ const data = this.parseEdge(edge);
2998
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
2999
+ mut.updateEdge(data);
1631
3000
  }
1632
3001
  };
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);
3002
+
3003
+ // src/index.ts
3004
+ async function graph(args = { root: "app" }) {
3005
+ const api = new API(args);
3006
+ await api.init();
3007
+ return api;
3008
+ }
3009
+ var index_default = graph;
1645
3010
  // Annotate the CommonJS export names for ESM import in node:
1646
3011
  0 && (module.exports = {
1647
- Graph
3012
+ graph
1648
3013
  });