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