@3plate/graph-core 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -25,23 +35,87 @@ __export(index_exports, {
25
35
  });
26
36
  module.exports = __toCommonJS(index_exports);
27
37
 
28
- // src/canvas/render-node.tsx
29
- var import_jsx_runtime = require("jsx-dom/jsx-runtime");
30
- function renderNode(node) {
31
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: node?.id || "" });
32
- }
33
-
34
- // src/graph/types/graph.ts
38
+ // src/graph/graph.ts
35
39
  var import_immutable8 = require("immutable");
36
40
 
37
- // src/graph/types/node.ts
41
+ // src/graph/node.ts
38
42
  var import_immutable = require("immutable");
39
- var defaultNodeProps = {
43
+
44
+ // src/log.ts
45
+ var import_pino = __toESM(require("pino"), 1);
46
+ var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
47
+ var browserOpts = { asObject: true };
48
+ browserOpts.transmit = {
49
+ level: resolvedLevel,
50
+ send: (level, log12) => {
51
+ try {
52
+ const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
53
+ if (!endpoint || typeof window === "undefined") {
54
+ try {
55
+ console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
56
+ } catch {
57
+ }
58
+ return;
59
+ }
60
+ const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
61
+ try {
62
+ console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
63
+ } catch {
64
+ }
65
+ fetch(endpoint, {
66
+ method: "POST",
67
+ mode: "no-cors",
68
+ credentials: "omit",
69
+ body: line,
70
+ keepalive: true
71
+ }).then(() => {
72
+ try {
73
+ console.debug("[graph-core] transmit fetch ok");
74
+ } catch {
75
+ }
76
+ }).catch((err) => {
77
+ try {
78
+ console.debug("[graph-core] transmit fetch error", err?.message || err);
79
+ } catch {
80
+ }
81
+ });
82
+ } catch (e) {
83
+ try {
84
+ console.debug("[graph-core] transmit error", e?.message || e);
85
+ } catch {
86
+ }
87
+ }
88
+ }
89
+ };
90
+ var base = (0, import_pino.default)({
91
+ level: resolvedLevel,
92
+ browser: browserOpts
93
+ });
94
+ function logger(module2) {
95
+ const child = base.child({ module: module2 });
96
+ return {
97
+ error: (msg, ...args) => child.error({ args }, msg),
98
+ warn: (msg, ...args) => child.warn({ args }, msg),
99
+ info: (msg, ...args) => child.info({ args }, msg),
100
+ debug: (msg, ...args) => child.debug({ args }, msg)
101
+ };
102
+ }
103
+ var log = logger("core");
104
+
105
+ // src/graph/node.ts
106
+ var log2 = logger("node");
107
+ var defNodeData = {
40
108
  id: "",
109
+ data: void 0,
110
+ version: 0,
111
+ title: void 0,
112
+ text: void 0,
113
+ type: void 0,
114
+ render: void 0,
115
+ ports: {},
41
116
  aligned: {},
42
117
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
43
118
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
44
- ports: { in: [], out: [] },
45
119
  layerId: "",
46
120
  isDummy: false,
47
121
  isMerged: false,
@@ -52,44 +126,49 @@ var defaultNodeProps = {
52
126
  dims: void 0,
53
127
  mutable: false
54
128
  };
55
- var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
129
+ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
56
130
  static dummyPrefix = "d:";
57
- get edgeId() {
58
- if (!this.isDummy)
59
- throw new Error(`node ${this.id} is not a dummy`);
60
- if (this.isMerged)
61
- throw new Error(`node ${this.id} is merged`);
62
- return this.get("edgeIds")[0];
63
- }
64
- get edgeIds() {
65
- if (!this.isDummy)
66
- throw new Error(`node ${this.id} is not a dummy`);
67
- if (!this.isMerged)
68
- throw new Error(`node ${this.id} is not merged`);
69
- return this.get("edgeIds");
70
- }
71
- static isDummyId(nodeId) {
72
- return nodeId.startsWith(_Node.dummyPrefix);
73
- }
74
- static addNormal(g, props) {
131
+ // get edgeId(): EdgeId {
132
+ // if (!this.isDummy)
133
+ // throw new Error(`node ${this.id} is not a dummy`)
134
+ // if (this.isMerged)
135
+ // throw new Error(`node ${this.id} is merged`)
136
+ // return this.get('edgeIds')[0]
137
+ // }
138
+ // get edgeIds(): EdgeId[] {
139
+ // if (!this.isDummy)
140
+ // throw new Error(`node ${this.id} is not a dummy`)
141
+ // if (!this.isMerged)
142
+ // throw new Error(`node ${this.id} is not merged`)
143
+ // return this.get('edgeIds')
144
+ // }
145
+ get key() {
146
+ return this.isDummy ? this.id : _Node.key(this);
147
+ }
148
+ static key(node) {
149
+ return `k:${node.id}:${node.version}`;
150
+ }
151
+ static addNormal(g, data) {
75
152
  const layer = g.layerAt(0);
76
153
  const node = new _Node({
77
- ...props,
154
+ ...data,
78
155
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
79
156
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
80
157
  aligned: {},
81
158
  edgeIds: [],
82
- layerId: layer.id
159
+ layerId: layer.id,
160
+ lpos: void 0,
161
+ pos: void 0
83
162
  });
84
163
  layer.addNode(g, node.id);
85
164
  g.nodes.set(node.id, node);
86
165
  g.dirtyNodes.add(node.id);
87
166
  return node;
88
167
  }
89
- static addDummy(g, props) {
90
- const layer = g.getLayer(props.layerId);
168
+ static addDummy(g, data) {
169
+ const layer = g.getLayer(data.layerId);
91
170
  const node = new _Node({
92
- ...props,
171
+ ...data,
93
172
  id: `${_Node.dummyPrefix}${g.nextDummyId++}`,
94
173
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
95
174
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
@@ -105,6 +184,12 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
105
184
  g.dirtyNodes.add(node.id);
106
185
  return node;
107
186
  }
187
+ static del(g, node) {
188
+ return g.getNode(node.id).delSelf(g);
189
+ }
190
+ static update(g, data) {
191
+ return g.getNode(data.id).mut(g).merge(data);
192
+ }
108
193
  mut(g) {
109
194
  if (this.mutable) return this;
110
195
  return g.mutateNode(this);
@@ -127,19 +212,32 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
127
212
  isUnlinked() {
128
213
  return this.edges.in.size == 0 && this.edges.out.size == 0 && this.segs.in.size == 0 && this.segs.out.size == 0;
129
214
  }
215
+ hasPorts() {
216
+ return !!this.ports?.in?.length || !!this.ports?.out?.length;
217
+ }
130
218
  layerIndex(g) {
131
219
  return this.getLayer(g).index;
132
220
  }
133
221
  getLayer(g) {
134
222
  return g.getLayer(this.layerId);
135
223
  }
224
+ margin(g) {
225
+ return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
226
+ }
227
+ marginWith(g, other) {
228
+ return Math.max(this.margin(g), other.margin(g));
229
+ }
230
+ width(g) {
231
+ return this.dims?.[g.w] ?? 0;
232
+ }
233
+ right(g) {
234
+ return this.lpos + this.width(g);
235
+ }
136
236
  setIndex(g, index) {
137
237
  if (this.index == index) return this;
138
- console.log(`set index of ${this.id} to ${index}`);
139
238
  return this.mut(g).set("index", index);
140
239
  }
141
240
  setLayerPos(g, lpos) {
142
- console.log("setLayerPos", this.id, lpos);
143
241
  if (this.lpos == lpos) return this;
144
242
  return this.mut(g).set("lpos", lpos);
145
243
  }
@@ -153,7 +251,6 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
153
251
  return this;
154
252
  }
155
253
  moveToLayer(g, layer) {
156
- console.log("moveToLayer", this, this.getLayer(g), layer);
157
254
  this.getLayer(g).delNode(g, this.id);
158
255
  layer.addNode(g, this.id);
159
256
  return this.setLayer(g, layer.id);
@@ -182,9 +279,22 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
182
279
  return node;
183
280
  }
184
281
  delSelf(g) {
282
+ if (this.aligned.in)
283
+ g.getNode(this.aligned.in).setAligned(g, "out", void 0);
284
+ if (this.aligned.out)
285
+ g.getNode(this.aligned.out).setAligned(g, "in", void 0);
185
286
  this.getLayer(g).delNode(g, this.id);
186
- for (const rel of this.rels(g))
187
- rel.delSelf(g);
287
+ for (const edge of this.rels(g, "edges", "both"))
288
+ edge.delSelf(g);
289
+ const remainingSegIds = [...this.segs.in, ...this.segs.out];
290
+ if (remainingSegIds.length > 0) {
291
+ for (const segId of remainingSegIds) {
292
+ if (g.segs?.has?.(segId)) {
293
+ g.getSeg(segId).delSelf(g);
294
+ } else {
295
+ }
296
+ }
297
+ }
188
298
  g.nodes.delete(this.id);
189
299
  g.dirtyNodes.delete(this.id);
190
300
  g.delNodes.add(this.id);
@@ -275,16 +385,20 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
275
385
  }
276
386
  };
277
387
 
278
- // src/graph/types/edge.ts
388
+ // src/graph/edge.ts
279
389
  var import_immutable2 = require("immutable");
280
- var defaultEdgeProps = {
390
+ var log3 = logger("edge");
391
+ var defEdgeData = {
281
392
  id: "",
393
+ data: null,
394
+ label: void 0,
282
395
  source: { id: "" },
283
396
  target: { id: "" },
397
+ type: void 0,
284
398
  mutable: false,
285
399
  segIds: []
286
400
  };
287
- var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
401
+ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
288
402
  static prefix = "e:";
289
403
  mut(g) {
290
404
  if (this.mutable) return this;
@@ -342,37 +456,44 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
342
456
  return _Edge.str(this);
343
457
  }
344
458
  static str(edge) {
345
- let source = edge.source.id;
346
- if (edge.source.port)
459
+ let source = edge.source?.id;
460
+ if (!source) throw new Error("edge source is undefined");
461
+ if (edge.source?.port)
347
462
  source = `${source} (port ${edge.source.port})`;
348
- let target = edge.target.id;
349
- if (edge.target.port)
463
+ let target = edge.target?.id;
464
+ if (!target) throw new Error("edge target is undefined");
465
+ if (edge.target?.port)
350
466
  target = `${target} (port ${edge.target.port})`;
351
467
  let str = `edge from ${source} to ${target}`;
352
468
  if (edge.type) str += ` of type ${edge.type}`;
353
469
  return str;
354
470
  }
355
- static id(edge, prefix = _Edge.prefix, side = "both") {
471
+ static key(edge, prefix = _Edge.prefix, side = "both") {
356
472
  let source = "", target = "";
357
473
  if (side == "source" || side == "both") {
474
+ if (!edge.source?.id) throw new Error("edge source is undefined");
358
475
  source = edge.source.id;
359
- if (edge.source.port)
476
+ if (edge.source?.port)
360
477
  source = `${source}.${edge.source.port}`;
478
+ const marker = edge.source?.marker;
479
+ if (marker && marker != "none") source += `[${marker}]`;
361
480
  source += "-";
362
481
  }
363
482
  if (side == "target" || side == "both") {
483
+ if (!edge.target?.id) throw new Error("edge target is undefined");
364
484
  target = edge.target.id;
365
485
  if (edge.target.port)
366
486
  target = `${target}.${edge.target.port}`;
367
487
  target = "-" + target;
488
+ const marker = edge.target?.marker ?? "arrow";
489
+ if (marker && marker != "none") target += `[${marker}]`;
368
490
  }
369
491
  const type = edge.type || "";
370
492
  return `${prefix}${source}${type}${target}`;
371
493
  }
372
- static add(g, props) {
494
+ static add(g, data) {
373
495
  const edge = new _Edge({
374
- ...props,
375
- id: _Edge.id(props),
496
+ ...data,
376
497
  segIds: []
377
498
  });
378
499
  edge.link(g);
@@ -380,18 +501,38 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
380
501
  g.dirtyEdges.add(edge.id);
381
502
  return edge;
382
503
  }
504
+ static del(g, data) {
505
+ return g.getEdge(data.id).delSelf(g);
506
+ }
507
+ static update(g, data) {
508
+ let edge = g.getEdge(data.id);
509
+ let relink = false;
510
+ 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) {
511
+ for (const seg of edge.segs(g))
512
+ seg.delEdgeId(g, edge.id);
513
+ edge.unlink(g);
514
+ relink = true;
515
+ }
516
+ edge = edge.mut(g).merge(data);
517
+ if (relink)
518
+ edge.link(g);
519
+ return edge;
520
+ }
383
521
  };
384
522
 
385
- // src/graph/types/seg.ts
523
+ // src/graph/seg.ts
386
524
  var import_immutable3 = require("immutable");
387
- var defaultSegProps = {
525
+ var defSegData = {
388
526
  id: "",
389
527
  source: { id: "" },
390
528
  target: { id: "" },
529
+ type: void 0,
391
530
  edgeIds: (0, import_immutable3.Set)(),
531
+ trackPos: void 0,
532
+ svg: void 0,
392
533
  mutable: false
393
534
  };
394
- var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
535
+ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
395
536
  static prefix = "s:";
396
537
  mut(g) {
397
538
  if (this.mutable) return this;
@@ -416,7 +557,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
416
557
  sameEnd(other, side) {
417
558
  const mine = this[side];
418
559
  const yours = other[side];
419
- return mine.id === yours.id && mine.port === yours.port;
560
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
420
561
  }
421
562
  setPos(g, source, target) {
422
563
  return this.mut(g).merge({
@@ -474,10 +615,10 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
474
615
  }
475
616
  return this.mut(g).set("edgeIds", this.edgeIds.asMutable().remove(edgeId));
476
617
  }
477
- static add(g, props) {
618
+ static add(g, data) {
478
619
  const seg = new _Seg({
479
- ...props,
480
- id: Edge.id(props, _Seg.prefix)
620
+ ...data,
621
+ id: Edge.key(data, _Seg.prefix)
481
622
  });
482
623
  seg.link(g);
483
624
  g.segs.set(seg.id, seg);
@@ -486,33 +627,10 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
486
627
  }
487
628
  };
488
629
 
489
- // src/graph/types/layer.ts
630
+ // src/graph/layer.ts
490
631
  var import_immutable4 = require("immutable");
491
-
492
- // src/log.ts
493
- var levels = {
494
- error: 0,
495
- warn: 1,
496
- info: 2,
497
- debug: 3
498
- };
499
- var currentLevel = "debug";
500
- function shouldLog(level) {
501
- return levels[level] <= levels[currentLevel];
502
- }
503
- function logger(module2) {
504
- return {
505
- error: (msg, ...args) => shouldLog("error") && console.error(`[${module2}] ${msg}`, ...args),
506
- warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module2}] ${msg}`, ...args),
507
- info: (msg, ...args) => shouldLog("info") && console.info(`[${module2}] ${msg}`, ...args),
508
- debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module2}] ${msg}`, ...args)
509
- };
510
- }
511
- var log = logger("core");
512
-
513
- // src/graph/types/layer.ts
514
- var log2 = logger("layer");
515
- var defaultLayerProps = {
632
+ var log4 = logger("layer");
633
+ var defLayerData = {
516
634
  id: "",
517
635
  index: 0,
518
636
  nodeIds: (0, import_immutable4.Set)(),
@@ -523,7 +641,7 @@ var defaultLayerProps = {
523
641
  isSorted: false,
524
642
  mutable: false
525
643
  };
526
- var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
644
+ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
527
645
  static prefix = "l:";
528
646
  mut(g) {
529
647
  if (this.mutable) return this;
@@ -536,7 +654,7 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
536
654
  mutable: false
537
655
  }).asImmutable();
538
656
  }
539
- get size() {
657
+ get nodeCount() {
540
658
  return this.nodeIds.size;
541
659
  }
542
660
  *nodes(g) {
@@ -591,9 +709,8 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
591
709
  reindex(g, nodeId) {
592
710
  if (!this.isSorted) return void 0;
593
711
  const sorted = this.sorted.filter((id) => id != nodeId);
594
- const idx = this.sorted.findIndex((id) => id == nodeId);
595
- for (let i = idx; i < sorted.length; i++)
596
- g.getNode(sorted[i]).setIndex(g, i);
712
+ for (const [i, id] of this.sorted.entries())
713
+ g.getNode(id).setIndex(g, i);
597
714
  return sorted;
598
715
  }
599
716
  delNode(g, nodeId) {
@@ -605,7 +722,6 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
605
722
  }
606
723
  setSorted(g, nodeIds) {
607
724
  if (this.hasSortOrder(nodeIds)) return this;
608
- console.log(`setting sorted for layer ${this.id}`);
609
725
  nodeIds.forEach((nodeId, i) => g.getNode(nodeId).setIndex(g, i));
610
726
  return this.mut(g).merge({ sorted: nodeIds, isSorted: true });
611
727
  }
@@ -615,7 +731,7 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
615
731
  }
616
732
  };
617
733
 
618
- // src/graph/types/mutator.ts
734
+ // src/graph/mutator.ts
619
735
  var Mutator = class {
620
736
  changes;
621
737
  constructor() {
@@ -624,20 +740,27 @@ var Mutator = class {
624
740
  removedNodes: [],
625
741
  updatedNodes: [],
626
742
  addedEdges: [],
627
- removedEdges: []
743
+ removedEdges: [],
744
+ updatedEdges: []
628
745
  };
629
746
  }
747
+ describe(description) {
748
+ this.changes.description = description;
749
+ }
630
750
  addNode(node) {
631
751
  this.changes.addedNodes.push(node);
632
752
  }
633
753
  addNodes(...nodes) {
634
754
  nodes.forEach((node) => this.addNode(node));
635
755
  }
756
+ removeNode(node) {
757
+ this.changes.removedNodes.push(node);
758
+ }
759
+ removeNodes(...nodes) {
760
+ nodes.forEach((node) => this.removeNode(node));
761
+ }
636
762
  updateNode(node) {
637
- if (typeof node === "string")
638
- this.changes.updatedNodes.push({ id: node });
639
- else
640
- this.changes.updatedNodes.push(node);
763
+ this.changes.updatedNodes.push(node);
641
764
  }
642
765
  updateNodes(...nodes) {
643
766
  nodes.forEach((node) => this.updateNode(node));
@@ -648,21 +771,18 @@ var Mutator = class {
648
771
  addEdges(...edges) {
649
772
  edges.forEach((edge) => this.addEdge(edge));
650
773
  }
651
- removeNode(node) {
652
- if (typeof node === "string")
653
- this.changes.removedNodes.push({ id: node });
654
- else
655
- this.changes.removedNodes.push(node);
656
- }
657
- removeNodes(...nodes) {
658
- nodes.forEach((node) => this.removeNode(node));
659
- }
660
774
  removeEdge(edge) {
661
775
  this.changes.removedEdges.push(edge);
662
776
  }
663
777
  removeEdges(...edges) {
664
778
  edges.forEach((edge) => this.removeEdge(edge));
665
779
  }
780
+ updateEdge(edge) {
781
+ this.changes.updatedEdges.push(edge);
782
+ }
783
+ updateEdges(...edges) {
784
+ edges.forEach((edge) => this.updateEdge(edge));
785
+ }
666
786
  };
667
787
 
668
788
  // src/graph/services/cycles.ts
@@ -762,12 +882,10 @@ var Cycles = class _Cycles {
762
882
 
763
883
  // src/graph/services/dummy.ts
764
884
  var import_immutable5 = require("immutable");
765
- var log3 = logger("dummy");
885
+ var log5 = logger("dummy");
766
886
  var Dummy = class _Dummy {
767
887
  static updateDummies(g) {
768
- log3.debug(`updating dummies:`, [...g.dirtyEdges]);
769
888
  for (const edgeId of g.dirtyEdges) {
770
- log3.debug(`updating dummies of edge ${edgeId}`);
771
889
  const edge = g.getEdge(edgeId);
772
890
  const { type } = edge;
773
891
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
@@ -794,11 +912,9 @@ var Dummy = class _Dummy {
794
912
  target = { id: dummy.id };
795
913
  }
796
914
  seg = Seg.add(g, { source, target, type, edgeIds: (0, import_immutable5.Set)([edgeId]) });
797
- log3.debug(`edge ${edgeId}: adding segment ${seg.id} from ${source.id} at layer ${layerIndex - 1} to ${target.id} at layer ${layerIndex}`);
798
915
  segs.splice(segIndex, 0, seg.id);
799
916
  changed = true;
800
917
  } 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)) {
801
- log3.debug(`edge ${edgeId}: removing segment ${seg.id} from layer ${layerIndex - 1} to layer ${layerIndex}`);
802
918
  seg = seg.delEdgeId(g, edgeId);
803
919
  segs.splice(segIndex, 1);
804
920
  changed = true;
@@ -810,14 +926,12 @@ var Dummy = class _Dummy {
810
926
  }
811
927
  }
812
928
  while (segIndex < segs.length) {
813
- log3.debug(`edge ${edgeId}: removing trailing segment ${segs[segIndex]}`);
814
929
  g.getSeg(segs[segIndex]).delEdgeId(g, edgeId);
815
930
  segs.splice(segIndex, 1);
816
931
  changed = true;
817
932
  segIndex++;
818
933
  }
819
934
  if (changed) {
820
- log3.debug(`edge ${edgeId}: updated segments to ${segs.join(", ")}`);
821
935
  edge.setSegIds(g, segs);
822
936
  }
823
937
  }
@@ -832,22 +946,20 @@ var Dummy = class _Dummy {
832
946
  const dir = side == "source" ? "in" : "out";
833
947
  const altSide = side == "source" ? "target" : "source";
834
948
  const altDir = altSide == "source" ? "in" : "out";
835
- log3.debug(`merging dummies by ${side}`);
836
949
  for (const layerId of layerIds) {
837
950
  let layer = g.getLayer(layerId);
838
951
  const groups = /* @__PURE__ */ new Map();
839
952
  for (const nodeId of layer.nodeIds) {
840
- if (!Node.isDummyId(nodeId)) continue;
841
953
  const node = g.getNode(nodeId);
842
- if (node.isMerged) continue;
954
+ if (!node.isDummy || node.isMerged) continue;
843
955
  const edge = g.getEdge(node.edgeIds[0]);
844
- const key = Edge.id(edge, "k:", side);
956
+ const key = Edge.key(edge, "k:", side);
845
957
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
846
958
  groups.get(key).add(node);
847
959
  }
848
960
  for (const [key, group] of groups) {
849
961
  if (group.size == 1) continue;
850
- const edgeIds = [...group].map((node) => node.edgeId);
962
+ const edgeIds = [...group].map((node) => node.edgeIds[0]);
851
963
  const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
852
964
  let seg;
853
965
  for (const old of group) {
@@ -857,8 +969,9 @@ var Dummy = class _Dummy {
857
969
  const example = g.getSeg(segId);
858
970
  seg = Seg.add(g, {
859
971
  ...example,
860
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
861
- [altSide]: { ...example[altSide], id: dummy.id }
972
+ edgeIds: (0, import_immutable5.Set)(edgeIds),
973
+ [side]: { ...example[side] },
974
+ [altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
862
975
  });
863
976
  }
864
977
  edge = edge.replaceSegId(g, segId, seg.id);
@@ -870,12 +983,15 @@ var Dummy = class _Dummy {
870
983
  const example = g.getSeg(segId);
871
984
  const seg2 = Seg.add(g, {
872
985
  ...example,
873
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
874
- [side]: { ...example[side], id: dummy.id }
986
+ edgeIds: (0, import_immutable5.Set)([old.edgeIds[0]]),
987
+ [altSide]: { ...example[altSide] },
988
+ [side]: { ...example[side], id: dummy.id, port: void 0 }
875
989
  });
876
990
  edge = edge.replaceSegId(g, segId, seg2.id);
877
991
  }
878
992
  }
993
+ for (const old of group)
994
+ old.delSelf(g);
879
995
  }
880
996
  }
881
997
  }
@@ -883,7 +999,7 @@ var Dummy = class _Dummy {
883
999
 
884
1000
  // src/graph/services/layers.ts
885
1001
  var import_immutable6 = require("immutable");
886
- var log4 = logger("layers");
1002
+ var log6 = logger("layers");
887
1003
  var Layers = class {
888
1004
  static updateLayers(g) {
889
1005
  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);
@@ -946,13 +1062,11 @@ var Layers = class {
946
1062
 
947
1063
  // src/graph/services/layout.ts
948
1064
  var import_immutable7 = require("immutable");
949
- var log5 = logger("layout");
1065
+ var log7 = logger("layout");
950
1066
  var Layout = class _Layout {
951
1067
  static parentIndex(g, node) {
952
1068
  const parents = (0, import_immutable7.Seq)([...node.adjs(g, "segs", "in")]);
953
- console.log(`parents of ${node.id}:`, [...parents], [...parents.map((p) => p.index)]);
954
1069
  const pidx = parents.map((p) => p.index).min();
955
- log5.debug(`node ${node.id}: parent index ${pidx}`);
956
1070
  if (pidx !== void 0) return pidx;
957
1071
  return node.isDummy ? -Infinity : Infinity;
958
1072
  }
@@ -965,12 +1079,11 @@ var Layout = class _Layout {
965
1079
  if (a.isDummy && !b.isDummy) return -1;
966
1080
  if (!a.isDummy && b.isDummy) return 1;
967
1081
  if (!a.isDummy) return a.id.localeCompare(b.id);
968
- const minA = a.edgeId ?? (0, import_immutable7.Seq)(a.edgeIds).min();
969
- const minB = b.edgeId ?? (0, import_immutable7.Seq)(b.edgeIds).min();
1082
+ const minA = (0, import_immutable7.Seq)(a.edgeIds).min();
1083
+ const minB = (0, import_immutable7.Seq)(b.edgeIds).min();
970
1084
  return minA.localeCompare(minB);
971
1085
  }
972
1086
  static positionNodes(g) {
973
- console.log("positionNodes", g.dirtyNodes);
974
1087
  for (const nodeId of g.dirtyNodes)
975
1088
  g.dirtyLayers.add(g.getNode(nodeId).layerId);
976
1089
  let adjustNext = false;
@@ -978,15 +1091,12 @@ var Layout = class _Layout {
978
1091
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
979
1092
  adjustNext = false;
980
1093
  let layer = g.getLayer(layerId);
981
- console.log(`positioning layer ${layerId} at ${layer.index}`);
982
1094
  const pidxs = /* @__PURE__ */ new Map();
983
1095
  for (const nodeId of layer.nodeIds)
984
1096
  pidxs.set(nodeId, _Layout.parentIndex(g, g.getNode(nodeId)));
985
- console.log("pidxs", pidxs);
986
1097
  const sorted = [...layer.nodeIds].sort(
987
1098
  (aId, bId) => _Layout.compareNodes(g, aId, bId, pidxs)
988
1099
  );
989
- console.log(`sorted:`, sorted);
990
1100
  if (layer.hasSortOrder(sorted)) continue;
991
1101
  g.dirtyLayers.add(layerId);
992
1102
  layer = layer.setSorted(g, sorted);
@@ -994,10 +1104,14 @@ var Layout = class _Layout {
994
1104
  let lpos = 0;
995
1105
  for (let i = 0; i < sorted.length; i++) {
996
1106
  let node = g.getNode(sorted[i]);
997
- log5.debug(`node ${node.id}: final index ${i}`);
998
1107
  node = node.setIndex(g, i).setLayerPos(g, lpos);
999
1108
  const size = node.dims?.[g.w] ?? 0;
1000
- lpos += size + g.options.nodeMargin;
1109
+ let margin = node.margin(g);
1110
+ if (i + 1 < sorted.length) {
1111
+ const next = g.getNode(sorted[i + 1]);
1112
+ margin = node.marginWith(g, next);
1113
+ }
1114
+ lpos += size + margin;
1001
1115
  }
1002
1116
  }
1003
1117
  }
@@ -1026,7 +1140,12 @@ var Layout = class _Layout {
1026
1140
  for (const layerId of layerIds) {
1027
1141
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
1028
1142
  adjustNext = false;
1143
+ let iterations = 0;
1029
1144
  while (true) {
1145
+ if (++iterations > 10) {
1146
+ log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1147
+ break;
1148
+ }
1030
1149
  let changed = false;
1031
1150
  const nodeIds = _Layout.sortLayer(g, layerId, reverseNodes);
1032
1151
  for (const nodeId of nodeIds) {
@@ -1084,7 +1203,7 @@ var Layout = class _Layout {
1084
1203
  static anchorPos(g, seg, side) {
1085
1204
  const nodeId = seg[side].id;
1086
1205
  const node = g.getNode(nodeId);
1087
- let p = { x: 0, y: 0, [g.x]: node.lpos, ...node.pos || {} };
1206
+ let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
1088
1207
  let w = node.dims?.[g.w] ?? 0;
1089
1208
  let h = node.dims?.[g.h] ?? 0;
1090
1209
  if (node.isDummy)
@@ -1092,32 +1211,56 @@ var Layout = class _Layout {
1092
1211
  [g.x]: p[g.x] + w / 2,
1093
1212
  [g.y]: p[g.y] + h / 2
1094
1213
  };
1095
- p[g.x] += _Layout.nodePortOffset(g, nodeId, seg[side].port);
1096
- if (side == "source" == g.r)
1214
+ p[g.x] += _Layout.nodePortOffset(g, nodeId, seg, side);
1215
+ if (side == "target" == g.r)
1097
1216
  p[g.y] += h;
1098
1217
  return p;
1099
1218
  }
1100
- static nodePortOffset(g, nodeId, port) {
1101
- if (!port) return g.options.defaultPortOffset;
1102
- return g.options.defaultPortOffset;
1219
+ static nodePortOffset(g, nodeId, seg, side) {
1220
+ const node = g.getNode(nodeId);
1221
+ const dir = side == "source" ? "out" : "in";
1222
+ const portId = seg[side].port;
1223
+ let min = 0, size = node.dims?.[g.w] ?? 0;
1224
+ if (portId) {
1225
+ const ports = node.ports?.[dir];
1226
+ const port = ports?.find((p) => p.id === portId);
1227
+ if (port?.offset !== void 0) {
1228
+ min = port.offset;
1229
+ size = port.size ?? 0;
1230
+ }
1231
+ }
1232
+ const alt = side == "source" ? "target" : "source";
1233
+ let segs = [];
1234
+ const keyOf = (seg2) => `${seg2.type ?? ""}:${seg2[side].marker ?? ""}`;
1235
+ for (const segId of node.segs[dir])
1236
+ segs.push(g.getSeg(segId));
1237
+ if (portId) segs = segs.filter((s) => s[side].port == portId);
1238
+ const groups = Object.groupBy(segs, (s) => keyOf(s));
1239
+ const posMap = /* @__PURE__ */ new Map();
1240
+ for (const [key, segs2] of Object.entries(groups)) {
1241
+ let pos = Infinity;
1242
+ for (const seg2 of segs2) pos = Math.min(pos, seg2.node(g, alt).lpos);
1243
+ posMap.set(key, pos);
1244
+ }
1245
+ const keys = [...posMap.keys()].sort((a, b) => posMap.get(a) - posMap.get(b));
1246
+ const gap = size / (keys.length + 1);
1247
+ const index = keys.indexOf(keyOf(seg));
1248
+ return min + (index + 1) * gap;
1103
1249
  }
1104
1250
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1105
1251
  const node = g.getNode(nodeId);
1106
1252
  if (!conservative)
1107
1253
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1108
- const space = g.options.nodeMargin;
1109
- const nodeWidth = node.dims?.[g.w] ?? 0;
1110
- const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1254
+ const nodeRight = lpos + node.width(g);
1111
1255
  repeat:
1112
1256
  for (const otherId of node.getLayer(g).nodeIds) {
1113
1257
  if (otherId == nodeId) continue;
1114
1258
  const other = g.getNode(otherId);
1115
- const opos = other.lpos;
1116
- const otherWidth = other.dims?.[g.w] ?? 0;
1117
- const bMin = opos, bMax = opos + otherWidth;
1118
- if (aMin < bMax && bMin < aMax) {
1259
+ const margin = node.marginWith(g, other);
1260
+ const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
1261
+ if (gap < margin) {
1119
1262
  if (conservative) return false;
1120
- const safePos = reverseMove ? aMin - otherWidth : aMax;
1263
+ const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
1121
1264
  _Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
1122
1265
  continue repeat;
1123
1266
  }
@@ -1133,7 +1276,7 @@ var Layout = class _Layout {
1133
1276
  g.getNode(node.aligned[dir]).setAligned(g, alt, void 0);
1134
1277
  if (otherId)
1135
1278
  g.getNode(otherId).setAligned(g, alt, nodeId);
1136
- node.setAligned(g, dir, otherId);
1279
+ node.setAligned(g, dir, otherId).setLayerPos(g, lpos);
1137
1280
  }
1138
1281
  static *aligned(g, nodeId, dir) {
1139
1282
  const visit = function* (node2, dir2) {
@@ -1166,11 +1309,12 @@ var Layout = class _Layout {
1166
1309
  for (const layerId of g.layerList) {
1167
1310
  const layer = g.getLayer(layerId);
1168
1311
  if (layer.sorted.length < 2) continue;
1169
- for (const nodeId of layer.sorted) {
1312
+ for (const [i, nodeId] of layer.sorted.entries()) {
1170
1313
  const node = g.getNode(nodeId);
1171
1314
  if (node.index == 0) continue;
1172
1315
  let minGap = Infinity;
1173
1316
  const stack = [];
1317
+ let maxMargin = 0;
1174
1318
  for (const right of _Layout.aligned(g, nodeId, "both")) {
1175
1319
  stack.push(right);
1176
1320
  const leftId = _Layout.leftOf(g, right);
@@ -1179,8 +1323,10 @@ var Layout = class _Layout {
1179
1323
  const leftWidth = left.dims?.[g.w] ?? 0;
1180
1324
  const gap = right.lpos - left.lpos - leftWidth;
1181
1325
  if (gap < minGap) minGap = gap;
1326
+ const margin = right.marginWith(g, left);
1327
+ if (margin > maxMargin) maxMargin = margin;
1182
1328
  }
1183
- const delta = minGap - g.options.nodeMargin;
1329
+ const delta = minGap - maxMargin;
1184
1330
  if (delta <= 0) continue;
1185
1331
  anyChanged = true;
1186
1332
  for (const right of stack)
@@ -1193,39 +1339,140 @@ var Layout = class _Layout {
1193
1339
  let pos = 0;
1194
1340
  const dir = g.r ? -1 : 1;
1195
1341
  const trackSep = Math.max(
1196
- g.options.layerMargin,
1197
1342
  g.options.edgeSpacing,
1198
1343
  g.options.turnRadius
1199
1344
  );
1345
+ const marginSep = Math.max(
1346
+ g.options.edgeSpacing,
1347
+ g.options.layerMargin,
1348
+ g.options.turnRadius + g.options.markerSize
1349
+ );
1200
1350
  for (const layerId of g.layerList) {
1201
1351
  let layer = g.getLayer(layerId);
1202
1352
  let height;
1203
- console.log(`getCoords: layer = ${layerId} at ${layer.index}`);
1204
1353
  if (g.dirtyLayers.has(layerId)) {
1205
1354
  height = (0, import_immutable7.Seq)(layer.nodes(g)).map((node) => node.dims?.[g.h] ?? 0).max() ?? 0;
1206
1355
  layer = layer.setSize(g, height);
1207
1356
  } else height = layer.size;
1208
- console.log(`getCoords: layer = ${layerId}: pos = ${pos}, height = ${height}`);
1209
1357
  for (const node of layer.nodes(g)) {
1210
1358
  if (!g.dirtyNodes.has(node.id) && pos == layer.pos) continue;
1211
1359
  const npos = { [g.x]: node.lpos, [g.y]: pos };
1212
1360
  if (!g.n) npos[g.y] += dir * height;
1213
1361
  if (g.r == g.n) npos[g.y] -= node.dims?.[g.h] ?? 0;
1214
- console.log(`getCoords: node = ${node.id}: pos:`, npos);
1215
1362
  node.setPos(g, npos);
1216
1363
  }
1217
1364
  layer = layer.setPos(g, pos);
1218
- pos += dir * (height + trackSep);
1365
+ pos += dir * (height + marginSep);
1219
1366
  for (const track of layer.tracks) {
1220
1367
  for (const segId of track)
1221
1368
  g.getSeg(segId).setTrackPos(g, pos);
1222
- pos += dir * g.options.edgeSpacing;
1369
+ pos += dir * trackSep;
1223
1370
  }
1371
+ pos += dir * (marginSep - trackSep);
1224
1372
  }
1225
1373
  }
1226
1374
  };
1227
1375
 
1376
+ // src/canvas/marker.tsx
1377
+ var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1378
+ function arrow(size, reverse = false) {
1379
+ const h = size / 1.5;
1380
+ const w = size;
1381
+ const ry = h / 2;
1382
+ const suffix = reverse ? "-reverse" : "";
1383
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1384
+ "marker",
1385
+ {
1386
+ id: `g3p-marker-arrow${suffix}`,
1387
+ className: "g3p-marker g3p-marker-arrow",
1388
+ markerWidth: size,
1389
+ markerHeight: size,
1390
+ refX: "2",
1391
+ refY: ry,
1392
+ orient: reverse ? "auto-start-reverse" : "auto",
1393
+ markerUnits: "userSpaceOnUse",
1394
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M0,0 L0,${h} L${w},${ry} z` })
1395
+ }
1396
+ );
1397
+ }
1398
+ function circle(size, reverse = false) {
1399
+ const r = size / 3;
1400
+ const cy = size / 2;
1401
+ const suffix = reverse ? "-reverse" : "";
1402
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1403
+ "marker",
1404
+ {
1405
+ id: `g3p-marker-circle${suffix}`,
1406
+ className: "g3p-marker g3p-marker-circle",
1407
+ markerWidth: size,
1408
+ markerHeight: size,
1409
+ refX: "2",
1410
+ refY: cy,
1411
+ orient: reverse ? "auto-start-reverse" : "auto",
1412
+ markerUnits: "userSpaceOnUse",
1413
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: r + 2, cy, r })
1414
+ }
1415
+ );
1416
+ }
1417
+ function diamond(size, reverse = false) {
1418
+ const w = size * 0.7;
1419
+ const h = size / 2;
1420
+ const cy = size / 2;
1421
+ const suffix = reverse ? "-reverse" : "";
1422
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1423
+ "marker",
1424
+ {
1425
+ id: `g3p-marker-diamond${suffix}`,
1426
+ className: "g3p-marker g3p-marker-diamond",
1427
+ markerWidth: size,
1428
+ markerHeight: size,
1429
+ refX: "2",
1430
+ refY: cy,
1431
+ orient: reverse ? "auto-start-reverse" : "auto",
1432
+ markerUnits: "userSpaceOnUse",
1433
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M2,${cy} L${2 + w / 2},${cy - h / 2} L${2 + w},${cy} L${2 + w / 2},${cy + h / 2} z` })
1434
+ }
1435
+ );
1436
+ }
1437
+ function bar(size, reverse = false) {
1438
+ const h = size * 0.6;
1439
+ const cy = size / 2;
1440
+ const suffix = reverse ? "-reverse" : "";
1441
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1442
+ "marker",
1443
+ {
1444
+ id: `g3p-marker-bar${suffix}`,
1445
+ className: "g3p-marker g3p-marker-bar",
1446
+ markerWidth: size,
1447
+ markerHeight: size,
1448
+ refX: "2",
1449
+ refY: cy,
1450
+ orient: reverse ? "auto-start-reverse" : "auto",
1451
+ markerUnits: "userSpaceOnUse",
1452
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: cy - h / 2, x2: "2", y2: cy + h / 2, "stroke-width": "2" })
1453
+ }
1454
+ );
1455
+ }
1456
+ function none(size, reverse = false) {
1457
+ return void 0;
1458
+ }
1459
+ function normalize(data) {
1460
+ let source = data.source?.marker ?? data.style?.marker?.source;
1461
+ let target = data.target?.marker ?? data.style?.marker?.target ?? "arrow";
1462
+ if (source == "none") source = void 0;
1463
+ if (target == "none") target = void 0;
1464
+ return { source, target };
1465
+ }
1466
+ var markerDefs = {
1467
+ arrow,
1468
+ circle,
1469
+ diamond,
1470
+ bar,
1471
+ none
1472
+ };
1473
+
1228
1474
  // src/graph/services/lines.ts
1475
+ var log8 = logger("lines");
1229
1476
  var Lines = class _Lines {
1230
1477
  static layoutSeg(g, seg) {
1231
1478
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1271,7 +1518,11 @@ var Lines = class _Lines {
1271
1518
  validTrack = track;
1272
1519
  break;
1273
1520
  }
1274
- if (other.p1 < seg.p2 && seg.p1 < other.p2) {
1521
+ const minA = Math.min(seg.p1, seg.p2);
1522
+ const maxA = Math.max(seg.p1, seg.p2);
1523
+ const minB = Math.min(other.p1, other.p2);
1524
+ const maxB = Math.max(other.p1, other.p2);
1525
+ if (minA < maxB && minB < maxA) {
1275
1526
  overlap = true;
1276
1527
  break;
1277
1528
  }
@@ -1286,6 +1537,21 @@ var Lines = class _Lines {
1286
1537
  else
1287
1538
  trackSet.push([seg]);
1288
1539
  }
1540
+ const midpoint = (s) => (s.p1 + s.p2) / 2;
1541
+ const sortTracks = (tracks2, goingRight) => {
1542
+ tracks2.sort((a, b) => {
1543
+ const midA = Math.max(...a.map(midpoint));
1544
+ const midB = Math.max(...b.map(midpoint));
1545
+ return goingRight ? midB - midA : midA - midB;
1546
+ });
1547
+ };
1548
+ sortTracks(rightTracks, true);
1549
+ sortTracks(leftTracks, false);
1550
+ allTracks.sort((a, b) => {
1551
+ const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
1552
+ const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
1553
+ return avgB - avgA;
1554
+ });
1289
1555
  const tracks = [];
1290
1556
  const all = leftTracks.concat(rightTracks).concat(allTracks);
1291
1557
  for (const track of all)
@@ -1300,7 +1566,12 @@ var Lines = class _Lines {
1300
1566
  const radius = g.options.turnRadius;
1301
1567
  const p1 = Layout.anchorPos(g, seg, "source");
1302
1568
  const p2 = Layout.anchorPos(g, seg, "target");
1303
- const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius) : _Lines.createDirectPath(g, p1, p2, radius);
1569
+ const source = seg.sourceNode(g);
1570
+ const target = seg.targetNode(g);
1571
+ const marker = normalize(seg);
1572
+ if (source.isDummy) marker.source = void 0;
1573
+ if (target.isDummy) marker.target = void 0;
1574
+ const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius, marker) : _Lines.createDirectPath(g, p1, p2, radius, marker);
1304
1575
  const svg = _Lines.pathToSVG(path);
1305
1576
  seg.setSVG(g, svg);
1306
1577
  }
@@ -1326,7 +1597,7 @@ var Lines = class _Lines {
1326
1597
  }
1327
1598
  return line;
1328
1599
  }
1329
- static pathBuilder(g, start, end, trackPos, radius) {
1600
+ static pathBuilder(g, start, end, trackPos, radius, marker) {
1330
1601
  const { x, y } = g;
1331
1602
  const lr = end[x] > start[x];
1332
1603
  const d = lr ? 1 : -1;
@@ -1338,6 +1609,8 @@ var Lines = class _Lines {
1338
1609
  if (g.r) s = 1 - s;
1339
1610
  if (!lr) s = 1 - s;
1340
1611
  if (!g.v) s = 1 - s;
1612
+ if (marker.source) start[y] += o * (g.options.markerSize - 1);
1613
+ if (marker.target) end[y] -= o * (g.options.markerSize - 1);
1341
1614
  const p = { ...start, s };
1342
1615
  const path = [];
1343
1616
  const advance = (p2, type) => {
@@ -1346,8 +1619,8 @@ var Lines = class _Lines {
1346
1619
  return { x, y, lr, d, o, rd, ro, t, s, p, path, advance };
1347
1620
  }
1348
1621
  // Create a railroad-style path with two 90-degree turns
1349
- static createRailroadPath(g, start, end, trackPos, radius) {
1350
- const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius);
1622
+ static createRailroadPath(g, start, end, trackPos, radius, marker) {
1623
+ const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius, marker);
1351
1624
  advance({ [y]: t - ro }, "line");
1352
1625
  advance({ [x]: p[x] + rd, [y]: t }, "arc");
1353
1626
  advance({ [x]: end[x] - rd }, "line");
@@ -1356,8 +1629,8 @@ var Lines = class _Lines {
1356
1629
  return path;
1357
1630
  }
1358
1631
  // Create a mostly-vertical path with optional S-curve
1359
- static createDirectPath(g, start, end, radius) {
1360
- const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius);
1632
+ static createDirectPath(g, start, end, radius, marker) {
1633
+ const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius, marker);
1361
1634
  const dx = Math.abs(end.x - start.x);
1362
1635
  const dy = Math.abs(end.y - start.y);
1363
1636
  const d_ = { x: dx, y: dy };
@@ -1448,21 +1721,15 @@ var Lines = class _Lines {
1448
1721
  }
1449
1722
  };
1450
1723
 
1451
- // src/graph/types/graph.ts
1452
- var defaultOptions = {
1453
- mergeOrder: ["target", "source"],
1454
- nodeMargin: 15,
1455
- dummyNodeSize: 15,
1456
- defaultPortOffset: 20,
1457
- nodeAlign: "natural",
1458
- edgeSpacing: 10,
1459
- turnRadius: 10,
1460
- orientation: "TB",
1461
- layerMargin: 5,
1462
- alignIterations: 5,
1463
- alignThreshold: 10,
1464
- separateTrackSets: true,
1465
- layoutSteps: null
1724
+ // src/graph/graph.ts
1725
+ var log9 = logger("graph");
1726
+ var emptyChanges = {
1727
+ addedNodes: [],
1728
+ removedNodes: [],
1729
+ updatedNodes: [],
1730
+ addedEdges: [],
1731
+ removedEdges: [],
1732
+ updatedEdges: []
1466
1733
  };
1467
1734
  var Graph = class _Graph {
1468
1735
  prior;
@@ -1491,30 +1758,10 @@ var Graph = class _Graph {
1491
1758
  x;
1492
1759
  y;
1493
1760
  d;
1494
- constructor({ prior, changes, options, nodes, edges } = {}) {
1761
+ constructor({ prior, changes, options }) {
1762
+ this.options = prior?.options ?? options;
1763
+ this.changes = changes ?? emptyChanges;
1495
1764
  this.initFromPrior(prior);
1496
- this.dirtyNodes = /* @__PURE__ */ new Set();
1497
- this.dirtyEdges = /* @__PURE__ */ new Set();
1498
- this.dirtyLayers = /* @__PURE__ */ new Set();
1499
- this.dirtySegs = /* @__PURE__ */ new Set();
1500
- this.delNodes = /* @__PURE__ */ new Set();
1501
- this.delEdges = /* @__PURE__ */ new Set();
1502
- this.delSegs = /* @__PURE__ */ new Set();
1503
- this.options = {
1504
- ...defaultOptions,
1505
- ...prior?.options,
1506
- ...options
1507
- };
1508
- this.changes = changes ?? {
1509
- addedNodes: [],
1510
- removedNodes: [],
1511
- updatedNodes: [],
1512
- addedEdges: [],
1513
- removedEdges: []
1514
- };
1515
- this.changes.addedNodes.push(...nodes || []);
1516
- this.changes.addedEdges.push(...edges || []);
1517
- this.dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1518
1765
  this.r = this.options.orientation === "BT" || this.options.orientation === "RL";
1519
1766
  this.v = this.options.orientation === "TB" || this.options.orientation === "BT";
1520
1767
  this.h = this.v ? "h" : "w";
@@ -1530,38 +1777,41 @@ var Graph = class _Graph {
1530
1777
  this.n = true;
1531
1778
  else
1532
1779
  this.n = natAligns[this.options.orientation] == this.options.nodeAlign;
1533
- if (this.dirty) {
1534
- try {
1535
- this.beginMutate();
1536
- this.applyChanges();
1537
- Cycles.checkCycles(this);
1538
- Layers.updateLayers(this);
1539
- Dummy.updateDummies(this);
1540
- Dummy.mergeDummies(this);
1541
- Layout.positionNodes(this);
1542
- Layout.alignAll(this);
1543
- Lines.trackEdges(this);
1544
- Layout.getCoords(this);
1545
- Lines.pathEdges(this);
1546
- } catch (e) {
1547
- this.initFromPrior(this.prior);
1548
- throw e;
1549
- } finally {
1550
- this.endMutate();
1551
- }
1780
+ if (this.dirty) this.processUpdate();
1781
+ }
1782
+ processUpdate() {
1783
+ try {
1784
+ this.beginMutate();
1785
+ this.applyChanges();
1786
+ Cycles.checkCycles(this);
1787
+ Layers.updateLayers(this);
1788
+ Dummy.updateDummies(this);
1789
+ Dummy.mergeDummies(this);
1790
+ Layout.positionNodes(this);
1791
+ Layout.alignAll(this);
1792
+ Lines.trackEdges(this);
1793
+ Layout.getCoords(this);
1794
+ Lines.pathEdges(this);
1795
+ } catch (e) {
1796
+ this.initFromPrior(this.prior);
1797
+ throw e;
1798
+ } finally {
1799
+ this.endMutate();
1552
1800
  }
1553
1801
  }
1554
1802
  applyChanges() {
1555
1803
  for (const edge of this.changes.removedEdges)
1556
- this.getEdge(Edge.id(edge)).delSelf(this);
1804
+ Edge.del(this, edge);
1557
1805
  for (const node of this.changes.removedNodes)
1558
- this.getNode(node.id).delSelf(this);
1806
+ Node.del(this, node);
1559
1807
  for (const node of this.changes.addedNodes)
1560
1808
  Node.addNormal(this, node);
1561
1809
  for (const edge of this.changes.addedEdges)
1562
1810
  Edge.add(this, edge);
1563
1811
  for (const node of this.changes.updatedNodes)
1564
- this.dirtyNodes.add(node.id);
1812
+ Node.update(this, node);
1813
+ for (const edge of this.changes.updatedEdges)
1814
+ Edge.update(this, edge);
1565
1815
  }
1566
1816
  layerAt(index) {
1567
1817
  while (index >= this.layerList.size)
@@ -1638,24 +1888,24 @@ var Graph = class _Graph {
1638
1888
  nodes.forEach((node) => mutator.addNode(node));
1639
1889
  });
1640
1890
  }
1641
- addEdges(...edges) {
1891
+ removeNodes(...nodes) {
1642
1892
  return this.withMutations((mutator) => {
1643
- edges.forEach((edge) => mutator.addEdge(edge));
1893
+ nodes.forEach((node) => mutator.removeNode(node));
1644
1894
  });
1645
1895
  }
1646
- addEdge(edge) {
1896
+ removeNode(node) {
1647
1897
  return this.withMutations((mutator) => {
1648
- mutator.addEdge(edge);
1898
+ mutator.removeNode(node);
1649
1899
  });
1650
1900
  }
1651
- removeNodes(...nodes) {
1901
+ addEdges(...edges) {
1652
1902
  return this.withMutations((mutator) => {
1653
- nodes.forEach((node) => mutator.removeNode(node));
1903
+ edges.forEach((edge) => mutator.addEdge(edge));
1654
1904
  });
1655
1905
  }
1656
- removeNode(node) {
1906
+ addEdge(edge) {
1657
1907
  return this.withMutations((mutator) => {
1658
- mutator.removeNode(node);
1908
+ mutator.addEdge(edge);
1659
1909
  });
1660
1910
  }
1661
1911
  removeEdges(...edges) {
@@ -1705,6 +1955,14 @@ var Graph = class _Graph {
1705
1955
  this.nextLayerId = prior?.nextLayerId ?? 0;
1706
1956
  this.nextDummyId = prior?.nextDummyId ?? 0;
1707
1957
  this.prior = prior;
1958
+ this.dirtyNodes = /* @__PURE__ */ new Set();
1959
+ this.dirtyEdges = /* @__PURE__ */ new Set();
1960
+ this.dirtyLayers = /* @__PURE__ */ new Set();
1961
+ this.dirtySegs = /* @__PURE__ */ new Set();
1962
+ this.delNodes = /* @__PURE__ */ new Set();
1963
+ this.delEdges = /* @__PURE__ */ new Set();
1964
+ this.delSegs = /* @__PURE__ */ new Set();
1965
+ 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;
1708
1966
  }
1709
1967
  beginMutate() {
1710
1968
  this.nodes = this.nodes.asMutable();
@@ -1734,28 +1992,10 @@ var Graph = class _Graph {
1734
1992
  }
1735
1993
  };
1736
1994
 
1737
- // src/canvas/canvas.css
1738
- var canvas_default = ".g3p-canvas-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.g3p-canvas-root {\n display: block;\n width: 100%;\n height: 100%;\n user-select: none;\n background: #fafafa;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}";
1739
-
1740
- // src/canvas/styler.ts
1741
- var injected = {};
1742
- function styler(name, styles, prefix) {
1743
- if (prefix === "g3p" && !injected[name]) {
1744
- const style = document.createElement("style");
1745
- style.textContent = styles;
1746
- document.head.appendChild(style);
1747
- injected[name] = true;
1748
- }
1749
- return (str, condition) => {
1750
- if (!(condition ?? true)) return "";
1751
- const parts = str.split(/\s+/);
1752
- const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
1753
- return fixed.join(" ");
1754
- };
1755
- }
1756
-
1757
- // src/canvas/node.css
1758
- var node_default = ".g3p-node-container {\n transition: opacity 0.2s ease;\n}\n\n.g3p-node-background {\n fill: var(--graph-node-bg, #ffffff);\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));\n}\n\n.g3p-node-border {\n stroke: var(--graph-node-border, #cbd5e1);\n transition: stroke 0.2s ease, stroke-width 0.2s ease;\n}\n\n.g3p-node-background.hovered {\n fill: var(--graph-node-hover, #f1f5f9);\n}\n\n.g3p-node-border.hovered {\n stroke: #94a3b8;\n}\n\n.g3p-node-border.selected {\n stroke: var(--graph-node-border-selected, #3b82f6);\n stroke-width: 3;\n}\n\n.g3p-node-content-wrapper {\n pointer-events: none;\n}\n\n.g3p-node-content {\n pointer-events: auto;\n box-sizing: border-box;\n}\n\n.g3p-node-content>div {\n width: 100%;\n height: 100%;\n}\n\n/* Dummy node styles */\n.g3p-node-dummy .g3p-node-background {\n fill: var(--graph-dummy-node-bg, #f8fafc);\n opacity: 0.8;\n}\n\n.g3p-node-dummy .g3p-node-border {\n stroke: var(--graph-dummy-node-border, #cbd5e1);\n stroke-dasharray: 3, 3;\n}";
1995
+ // src/common.ts
1996
+ var screenPos = (x, y) => ({ x, y });
1997
+ var canvasPos = (x, y) => ({ x, y });
1998
+ var graphPos = (x, y) => ({ x, y });
1759
1999
 
1760
2000
  // src/canvas/node.tsx
1761
2001
  var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
@@ -1763,89 +2003,75 @@ var Node2 = class {
1763
2003
  selected;
1764
2004
  hovered;
1765
2005
  container;
1766
- dims;
1767
2006
  content;
1768
- measured;
2007
+ canvas;
2008
+ data;
1769
2009
  isDummy;
1770
- constructor(options) {
1771
- this.isDummy = false;
1772
- Object.assign(this, {
1773
- selected: false,
1774
- hovered: false,
1775
- renderNode: () => null,
1776
- onClick: () => null,
1777
- onMouseEnter: () => null,
1778
- onMouseLeave: () => null,
1779
- onContextMenu: () => null,
1780
- onMouseDown: () => null,
1781
- onMouseUp: () => null,
1782
- classPrefix: "g3p",
1783
- ...options
1784
- });
1785
- if (!this.isDummy) {
1786
- this.content = this.renderNode(this.data);
1787
- this.measured = false;
2010
+ pos;
2011
+ constructor(canvas, data, isDummy = false) {
2012
+ this.canvas = canvas;
2013
+ this.data = data;
2014
+ this.selected = false;
2015
+ this.hovered = false;
2016
+ this.isDummy = isDummy;
2017
+ if (this.isDummy) {
2018
+ const size = canvas.dummyNodeSize;
1788
2019
  } else {
1789
- this.measured = true;
2020
+ const render = data.render ?? canvas.renderNode;
2021
+ this.content = this.renderContent(render(data.data, data));
1790
2022
  }
1791
2023
  }
1792
- getSize() {
1793
- const rect = this.content.getBoundingClientRect();
1794
- this.dims = { w: rect.width, h: rect.height };
1795
- this.measured = true;
1796
- }
1797
- handleClick(e) {
1798
- e.stopPropagation();
1799
- this.onClick?.(this.data, e);
1800
- }
1801
- handleMouseEnter(e) {
1802
- this.onMouseEnter?.(this.data, e);
2024
+ remove() {
2025
+ this.container.remove();
1803
2026
  }
1804
- handleMouseLeave(e) {
1805
- this.onMouseLeave?.(this.data, e);
2027
+ append() {
2028
+ this.canvas.group.appendChild(this.container);
1806
2029
  }
1807
- handleContextMenu(e) {
1808
- if (this.onContextMenu) {
1809
- e.stopPropagation();
1810
- this.onContextMenu(this.data, e);
1811
- }
2030
+ needsContentSize() {
2031
+ return !this.isDummy && this.content instanceof HTMLElement;
1812
2032
  }
1813
- handleMouseDown(e) {
1814
- this.onMouseDown?.(this.data, e);
1815
- }
1816
- handleMouseUp(e) {
1817
- this.onMouseUp?.(this.data, e);
2033
+ needsContainerSize() {
2034
+ return !this.isDummy;
1818
2035
  }
1819
2036
  setPos(pos) {
1820
- console.log(`setPos:`, this, pos);
1821
2037
  this.pos = pos;
1822
- this.container.setAttribute("transform", `translate(${this.pos.x}, ${this.pos.y})`);
1823
- }
1824
- // render will be called once the node is measured
1825
- render() {
1826
- const c = styler("node", node_default, this.classPrefix);
1827
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2038
+ const { x, y } = pos;
2039
+ this.container.setAttribute("transform", `translate(${x}, ${y})`);
2040
+ }
2041
+ hasPorts() {
2042
+ return !!this.data?.ports?.in?.length || !!this.data?.ports?.out?.length;
2043
+ }
2044
+ renderContent(el) {
2045
+ const hasPorts = this.hasPorts();
2046
+ el = this.renderBorder(el);
2047
+ if (hasPorts)
2048
+ el = this.renderOutsidePorts(el);
2049
+ return el;
2050
+ }
2051
+ renderContainer() {
2052
+ const hasPorts = this.hasPorts();
2053
+ const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
2054
+ const nodeType = this.data?.type;
2055
+ const typeClass = nodeType ? `g3p-node-type-${nodeType}` : "";
2056
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1828
2057
  "g",
1829
2058
  {
1830
- ref: (el) => this.container = el,
1831
- className: `${c("container")} ${c("dummy", this.isDummy)}`,
1832
- onClick: this.handleClick.bind(this),
1833
- onMouseEnter: this.handleMouseEnter.bind(this),
1834
- onMouseLeave: this.handleMouseLeave.bind(this),
1835
- onContextMenu: this.handleContextMenu.bind(this),
1836
- onMouseDown: this.handleMouseDown.bind(this),
1837
- onMouseUp: this.handleMouseUp.bind(this),
1838
- style: { cursor: "pointer" },
1839
- children: this.isDummy ? this.renderDummy() : this.renderContent()
2059
+ className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
2060
+ "data-node-id": this.data?.id,
2061
+ children: inner
1840
2062
  }
1841
2063
  );
1842
2064
  }
2065
+ renderForeign() {
2066
+ const { w, h } = this.data.dims;
2067
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
2068
+ }
1843
2069
  renderDummy() {
1844
- const c = styler("node", node_default, this.classPrefix);
1845
- let { w, h } = this.dims;
2070
+ let w = this.canvas.dummyNodeSize;
2071
+ let h = this.canvas.dummyNodeSize;
1846
2072
  w /= 2;
1847
2073
  h /= 2;
1848
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2074
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
1849
2075
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1850
2076
  "ellipse",
1851
2077
  {
@@ -1853,7 +2079,7 @@ var Node2 = class {
1853
2079
  cy: h,
1854
2080
  rx: w,
1855
2081
  ry: h,
1856
- className: c("background")
2082
+ className: "g3p-node-background"
1857
2083
  }
1858
2084
  ),
1859
2085
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -1864,160 +2090,176 @@ var Node2 = class {
1864
2090
  rx: w,
1865
2091
  ry: h,
1866
2092
  fill: "none",
1867
- className: c("border"),
1868
- strokeWidth: "2"
2093
+ className: "g3p-node-border"
1869
2094
  }
1870
2095
  )
1871
2096
  ] });
1872
2097
  }
1873
- renderContent() {
1874
- const c = styler("node", node_default, this.classPrefix);
1875
- const { w, h } = this.dims;
1876
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1877
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1878
- "rect",
1879
- {
1880
- className: c("background"),
1881
- width: w,
1882
- height: h,
1883
- rx: 8,
1884
- ry: 8
1885
- }
1886
- ),
1887
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1888
- "rect",
1889
- {
1890
- className: c("border"),
1891
- width: w,
1892
- height: h,
1893
- rx: 8,
1894
- ry: 8,
1895
- fill: "none",
1896
- strokeWidth: "2"
1897
- }
1898
- ),
1899
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1900
- "foreignObject",
1901
- {
1902
- width: w,
1903
- height: h,
1904
- className: c("content-wrapper"),
1905
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1906
- "div",
1907
- {
1908
- className: c("content"),
1909
- style: {
1910
- width: `${w}px`,
1911
- height: `${h}px`,
1912
- overflow: "hidden"
1913
- },
1914
- children: this.content
1915
- }
1916
- )
2098
+ measure(isVertical) {
2099
+ const rect = this.content.getBoundingClientRect();
2100
+ const data = this.data;
2101
+ data.dims = { w: rect.width, h: rect.height };
2102
+ for (const dir of ["in", "out"]) {
2103
+ const ports = data.ports?.[dir];
2104
+ if (!ports) continue;
2105
+ for (const port of ports) {
2106
+ const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
2107
+ if (!el) continue;
2108
+ const portRect = el.getBoundingClientRect();
2109
+ if (isVertical) {
2110
+ port.offset = portRect.left - rect.left;
2111
+ port.size = portRect.width;
2112
+ } else {
2113
+ port.offset = portRect.top - rect.top;
2114
+ port.size = portRect.height;
1917
2115
  }
1918
- )
2116
+ }
2117
+ }
2118
+ }
2119
+ getPortPosition(dir) {
2120
+ const o = this.canvas.orientation;
2121
+ if (dir === "in") {
2122
+ if (o === "TB") return "top";
2123
+ if (o === "BT") return "bottom";
2124
+ if (o === "LR") return "left";
2125
+ return "right";
2126
+ } else {
2127
+ if (o === "TB") return "bottom";
2128
+ if (o === "BT") return "top";
2129
+ if (o === "LR") return "right";
2130
+ return "left";
2131
+ }
2132
+ }
2133
+ isVerticalOrientation() {
2134
+ const o = this.canvas.orientation;
2135
+ return o === "TB" || o === "BT";
2136
+ }
2137
+ isReversedOrientation() {
2138
+ const o = this.canvas.orientation;
2139
+ return o === "BT" || o === "RL";
2140
+ }
2141
+ renderPortRow(dir, inout) {
2142
+ const ports = this.data?.ports?.[dir];
2143
+ if (!ports?.length) return null;
2144
+ const pos = this.getPortPosition(dir);
2145
+ const isVertical = this.isVerticalOrientation();
2146
+ const layoutClass = isVertical ? "row" : "col";
2147
+ const rotateLabels = false;
2148
+ const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2149
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `g3p-node-ports g3p-node-ports-${layoutClass}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2150
+ "div",
2151
+ {
2152
+ className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
2153
+ "data-node-id": this.data.id,
2154
+ "data-port-id": port.id,
2155
+ children: port.label ?? port.id
2156
+ }
2157
+ )) });
2158
+ }
2159
+ renderInsidePorts(el) {
2160
+ const isVertical = this.isVerticalOrientation();
2161
+ const isReversed = this.isReversedOrientation();
2162
+ let inPorts = this.renderPortRow("in", "in");
2163
+ let outPorts = this.renderPortRow("out", "in");
2164
+ if (!inPorts && !outPorts) return el;
2165
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2166
+ const wrapperClass = isVertical ? "v" : "h";
2167
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2168
+ inPorts,
2169
+ el,
2170
+ outPorts
1919
2171
  ] });
1920
2172
  }
2173
+ renderOutsidePorts(el) {
2174
+ const isVertical = this.isVerticalOrientation();
2175
+ const isReversed = this.isReversedOrientation();
2176
+ let inPorts = this.renderPortRow("in", "out");
2177
+ let outPorts = this.renderPortRow("out", "out");
2178
+ if (!inPorts && !outPorts) return el;
2179
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2180
+ const wrapperClass = isVertical ? "v" : "h";
2181
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2182
+ inPorts,
2183
+ el,
2184
+ outPorts
2185
+ ] });
2186
+ }
2187
+ renderBorder(el) {
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "g3p-node-border", children: el });
2189
+ }
1921
2190
  };
1922
2191
 
1923
- // src/canvas/seg.css
1924
- var seg_default = ".g3p-seg-container {\n transition: opacity 0.2s ease;\n}\n\n.g3p-seg-line {\n transition: stroke 0.2s ease, stroke-width 0.2s ease;\n}\n\n.g3p-seg-line.hovered {\n stroke-width: 4;\n opacity: 1;\n}\n\n.g3p-seg-line.selected {\n stroke: var(--graph-node-border-selected, #3b82f6);\n stroke-width: 3;\n}\n\n.g3p-seg-hitbox {\n cursor: pointer;\n}";
1925
-
1926
2192
  // src/canvas/seg.tsx
1927
2193
  var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
1928
2194
  var Seg2 = class {
2195
+ id;
1929
2196
  selected;
1930
2197
  hovered;
1931
- constructor(options) {
2198
+ canvas;
2199
+ type;
2200
+ svg;
2201
+ el;
2202
+ source;
2203
+ target;
2204
+ edgeIds;
2205
+ constructor(canvas, data, g) {
2206
+ this.id = data.id;
2207
+ this.canvas = canvas;
1932
2208
  this.selected = false;
1933
2209
  this.hovered = false;
1934
- Object.assign(this, {
1935
- onClick: () => {
1936
- },
1937
- onMouseEnter: () => {
1938
- },
1939
- onMouseLeave: () => {
1940
- },
1941
- onContextMenu: () => {
1942
- },
1943
- classPrefix: "g3p",
1944
- ...options
1945
- });
1946
- this.attrs ??= {};
1947
- this.attrs.targetTerminal ??= "arrow";
1948
- }
1949
- handleClick(e) {
1950
- e.stopPropagation();
1951
- this.onClick?.(this.edgeData, e);
1952
- }
1953
- handleMouseEnter(e) {
1954
- this.onMouseEnter?.(this.edgeData, e);
1955
- }
1956
- handleMouseLeave(e) {
1957
- this.onMouseLeave?.(this.edgeData, e);
1958
- }
1959
- handleContextMenu(e) {
1960
- if (this.onContextMenu) {
1961
- e.stopPropagation();
1962
- this.onContextMenu(this.edgeData, e);
1963
- }
1964
- }
1965
- renderTerminals() {
1966
- return {
1967
- source: this.renderTerminal(this.attrs.sourceTerminal, "source"),
1968
- target: this.renderTerminal(this.attrs.targetTerminal, "target")
1969
- };
1970
- }
1971
- setSVG(svg) {
1972
- this.svg = svg;
1973
- const n = this.el.childElementCount;
1974
- this.el.childNodes[n - 2].setAttribute("d", svg);
1975
- this.el.childNodes[n - 1].setAttribute("d", svg);
2210
+ this.svg = data.svg;
2211
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2212
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2213
+ this.type = data.type;
2214
+ this.edgeIds = data.edgeIds.toArray();
2215
+ this.el = this.render();
2216
+ }
2217
+ append() {
2218
+ this.canvas.group.appendChild(this.el);
2219
+ }
2220
+ remove() {
2221
+ this.el.remove();
2222
+ }
2223
+ update(data, g) {
2224
+ this.svg = data.svg;
2225
+ this.type = data.type;
2226
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2227
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2228
+ this.edgeIds = data.edgeIds.toArray();
2229
+ this.remove();
2230
+ this.el = this.render();
2231
+ this.append();
1976
2232
  }
1977
2233
  render() {
1978
- const c = styler("edge", seg_default, this.classPrefix);
1979
- const styleAttrs = {
1980
- stroke: this.attrs.color,
1981
- strokeWidth: this.attrs.width,
1982
- strokeDasharray: this.attrs.style
1983
- };
1984
- const hoverAttrs = {
1985
- ...styleAttrs,
1986
- strokeWidth: styleAttrs.strokeWidth ? Math.max(styleAttrs.strokeWidth * 3, 10) : void 0
1987
- };
1988
- const { source, target } = this.renderTerminals();
2234
+ let { source, target } = normalize(this);
2235
+ if (this.source.isDummy) source = void 0;
2236
+ if (this.target.isDummy) target = void 0;
2237
+ const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
1989
2238
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1990
2239
  "g",
1991
2240
  {
1992
2241
  ref: (el) => this.el = el,
1993
- id: `g3p-seg-${this.segId}`,
1994
- className: c("container"),
1995
- onClick: this.handleClick.bind(this),
1996
- onMouseEnter: this.handleMouseEnter.bind(this),
1997
- onMouseLeave: this.handleMouseLeave.bind(this),
1998
- onContextMenu: this.handleContextMenu.bind(this),
2242
+ id: `g3p-seg-${this.id}`,
2243
+ className: `g3p-seg-container ${typeClass}`.trim(),
2244
+ "data-edge-id": this.id,
1999
2245
  children: [
2000
- source?.defs,
2001
- target?.defs,
2002
2246
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2003
2247
  "path",
2004
2248
  {
2005
2249
  d: this.svg,
2006
- ...styleAttrs,
2007
2250
  fill: "none",
2008
- className: c("line"),
2009
- markerStart: source ? `url(#${source.id})` : void 0,
2010
- markerEnd: target ? `url(#${target.id})` : void 0
2251
+ className: "g3p-seg-line",
2252
+ markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2253
+ markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2011
2254
  }
2012
2255
  ),
2013
2256
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2014
2257
  "path",
2015
2258
  {
2016
2259
  d: this.svg,
2017
- ...hoverAttrs,
2018
2260
  stroke: "transparent",
2019
2261
  fill: "none",
2020
- className: c("hitbox"),
2262
+ className: "g3p-seg-hitbox",
2021
2263
  style: { cursor: "pointer" }
2022
2264
  }
2023
2265
  )
@@ -2025,243 +2267,1396 @@ var Seg2 = class {
2025
2267
  }
2026
2268
  );
2027
2269
  }
2028
- renderTerminal(type, side) {
2029
- if (!type)
2030
- return null;
2031
- const id = `g3p-seg-${this.segId}-${side}-${type}`;
2032
- const defs = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2033
- "marker",
2034
- {
2035
- id,
2036
- markerWidth: "10",
2037
- markerHeight: "10",
2038
- refX: "9",
2039
- refY: "3",
2040
- orient: "auto",
2041
- markerUnits: "userSpaceOnUse",
2042
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M0,0 L0,6 L9,3 z" })
2043
- }
2044
- ) });
2045
- return { id, defs };
2046
- }
2047
2270
  };
2048
2271
 
2049
- // src/canvas/canvas.tsx
2050
- var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2051
- var Canvas = class {
2052
- container;
2053
- root;
2054
- group;
2055
- transform;
2056
- bounds;
2057
- measurement;
2058
- nodes;
2059
- segs;
2060
- updating;
2061
- constructor(options) {
2062
- Object.assign(this, {
2063
- renderNode,
2064
- nodeStyle: () => ({}),
2065
- edgeStyle: () => ({}),
2066
- portStyle: "outside",
2067
- classPrefix: "g3p",
2068
- width: "100%",
2069
- height: "100%",
2070
- transform: { x: 0, y: 0, scale: 1 },
2071
- bounds: { min: { x: 0, y: 0 }, max: { x: 1, y: 1 } },
2072
- ...options
2073
- });
2074
- this.nodes = /* @__PURE__ */ new Map();
2075
- this.segs = /* @__PURE__ */ new Map();
2076
- this.updating = false;
2077
- this.createMeasurementContainer();
2272
+ // src/canvas/editMode.ts
2273
+ var EditMode = class {
2274
+ _state = { type: "idle" };
2275
+ _editable = false;
2276
+ get state() {
2277
+ return this._state;
2278
+ }
2279
+ get editable() {
2280
+ return this._editable;
2281
+ }
2282
+ set editable(value) {
2283
+ this._editable = value;
2284
+ if (!value) {
2285
+ this.reset();
2286
+ }
2078
2287
  }
2079
- createMeasurementContainer() {
2080
- this.measurement = document.createElement("div");
2081
- this.measurement.style.cssText = `
2082
- position: absolute;
2083
- left: -9999px;
2084
- top: -9999px;
2085
- visibility: hidden;
2086
- pointer-events: none;
2087
- `;
2088
- document.body.appendChild(this.measurement);
2288
+ get isIdle() {
2289
+ return this._state.type === "idle";
2089
2290
  }
2090
- update(callback) {
2091
- this.updating = true;
2092
- callback();
2093
- this.updating = false;
2094
- let bx0 = Infinity, by0 = Infinity;
2095
- let bx1 = -Infinity, by1 = -Infinity;
2096
- for (const node of this.nodes.values()) {
2097
- const nx0 = node.pos.x, nx1 = node.pos.x + node.dims.w;
2098
- const ny0 = node.pos.y, ny1 = node.pos.y + node.dims.h;
2099
- bx0 = Math.min(bx0, nx0);
2100
- by0 = Math.min(by0, ny0);
2101
- bx1 = Math.max(bx1, nx1);
2102
- by1 = Math.max(by1, ny1);
2103
- }
2104
- this.bounds = { min: { x: bx0, y: by0 }, max: { x: bx1, y: by1 } };
2105
- console.log("bounds", this.bounds);
2106
- this.root.setAttribute("viewBox", this.viewBox());
2291
+ get isPanning() {
2292
+ return this._state.type === "panning";
2107
2293
  }
2108
- addNode(opts) {
2109
- const node = this.nodes.get(opts.data);
2110
- if (!node) throw new Error("node not found");
2111
- if (!node.container) node.render();
2112
- node.setPos(opts.pos);
2113
- this.group.appendChild(node.container);
2114
- }
2115
- updateNode(opts) {
2116
- const node = this.nodes.get(opts.data);
2117
- if (!node) throw new Error("node not found");
2118
- node.setPos(opts.pos);
2119
- }
2120
- deleteNode(opts) {
2121
- const node = this.nodes.get(opts.data);
2122
- if (!node) throw new Error("node not found");
2123
- node.container.remove();
2124
- }
2125
- addSeg(opts) {
2126
- const seg = new Seg2(opts);
2127
- this.segs.set(seg.segId, seg);
2128
- seg.render();
2129
- this.group.appendChild(seg.el);
2130
- }
2131
- updateSeg(opts) {
2132
- const seg = this.segs.get(opts.segId);
2133
- if (!seg) throw new Error("seg not found");
2134
- seg.setSVG(opts.svg);
2294
+ get isCreatingEdge() {
2295
+ return this._state.type === "new-edge";
2135
2296
  }
2136
- deleteSeg(opts) {
2137
- const seg = this.segs.get(opts.segId);
2138
- if (!seg) throw new Error("seg not found");
2139
- seg.el.remove();
2140
- this.segs.delete(seg.segId);
2297
+ /** Start panning the canvas */
2298
+ startPan(startCanvas, startTransform) {
2299
+ this._state = { type: "panning", startCanvas, startTransform };
2141
2300
  }
2142
- async measure(nodes) {
2143
- const newNodes = [];
2144
- for (const data of nodes) {
2145
- if (this.nodes.has(data)) continue;
2146
- const node = new Node2({
2147
- data,
2148
- renderNode: this.renderNode,
2149
- classPrefix: this.classPrefix,
2150
- isDummy: false
2151
- });
2152
- this.nodes.set(node.data, node);
2153
- if (!node.measured) {
2154
- this.measurement.appendChild(node.content);
2155
- newNodes.push(node);
2156
- }
2157
- }
2158
- return new Promise((resolve) => {
2159
- requestAnimationFrame(() => {
2160
- for (const node of newNodes)
2161
- node.getSize();
2162
- this.measurement.textContent = "";
2163
- resolve();
2164
- });
2165
- });
2301
+ /** Start creating a new edge from a node or port */
2302
+ startNewEdge(id, startGraph, port) {
2303
+ if (!this._editable) return;
2304
+ this._state = {
2305
+ type: "new-edge",
2306
+ source: { id, port },
2307
+ startGraph,
2308
+ currentGraph: startGraph,
2309
+ target: null
2310
+ };
2166
2311
  }
2167
- getDims(node) {
2168
- return this.nodes.get(node).dims;
2312
+ /** Update the current position during new-edge mode */
2313
+ updateNewEdgePosition(currentGraph) {
2314
+ if (this._state.type === "new-edge") {
2315
+ this._state = { ...this._state, currentGraph };
2316
+ }
2169
2317
  }
2170
- onClick(e) {
2171
- console.log("click", e);
2318
+ /** Update the hover target during new-edge mode */
2319
+ setHoverTarget(target) {
2320
+ if (this._state.type === "new-edge") {
2321
+ this._state = { ...this._state, target };
2322
+ }
2172
2323
  }
2173
- onContextMenu(e) {
2174
- console.log("context menu", e);
2324
+ /** Get the new-edge state if active */
2325
+ getNewEdgeState() {
2326
+ if (this._state.type === "new-edge") {
2327
+ return this._state;
2328
+ }
2329
+ return null;
2175
2330
  }
2176
- groupTransform() {
2177
- return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2331
+ /** Reset to idle state */
2332
+ reset() {
2333
+ this._state = { type: "idle" };
2178
2334
  }
2179
- viewBox() {
2180
- return `${this.bounds.min.x} ${this.bounds.min.y} ${this.bounds.max.x - this.bounds.min.x} ${this.bounds.max.y - this.bounds.min.y}`;
2335
+ };
2336
+
2337
+ // src/canvas/newEdge.tsx
2338
+ var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2339
+ function renderNewEdge({ start, end }) {
2340
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: "g3p-new-edge-container", children: [
2341
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2342
+ "circle",
2343
+ {
2344
+ cx: start.x,
2345
+ cy: start.y,
2346
+ r: 4,
2347
+ className: "g3p-new-edge-origin"
2348
+ }
2349
+ ),
2350
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2351
+ "line",
2352
+ {
2353
+ x1: start.x,
2354
+ y1: start.y,
2355
+ x2: end.x,
2356
+ y2: end.y,
2357
+ className: "g3p-new-edge-line"
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2361
+ "circle",
2362
+ {
2363
+ cx: end.x,
2364
+ cy: end.y,
2365
+ r: 3,
2366
+ className: "g3p-new-edge-end"
2367
+ }
2368
+ )
2369
+ ] });
2370
+ }
2371
+
2372
+ // src/canvas/modal.tsx
2373
+ var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2374
+ var Modal = class {
2375
+ container;
2376
+ overlay;
2377
+ dialog;
2378
+ onClose;
2379
+ mouseDownOnOverlay = false;
2380
+ constructor(options) {
2381
+ this.onClose = options.onClose;
2382
+ this.overlay = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2383
+ "div",
2384
+ {
2385
+ className: "g3p-modal-overlay",
2386
+ onMouseDown: (e) => {
2387
+ this.mouseDownOnOverlay = e.target === this.overlay;
2388
+ },
2389
+ onMouseUp: (e) => {
2390
+ if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
2391
+ this.mouseDownOnOverlay = false;
2392
+ }
2393
+ }
2394
+ );
2395
+ this.dialog = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-dialog", children: [
2396
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-header", children: [
2397
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "g3p-modal-title", children: options.title }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2399
+ "button",
2400
+ {
2401
+ className: "g3p-modal-close",
2402
+ onClick: () => this.close(),
2403
+ children: "\xD7"
2404
+ }
2405
+ )
2406
+ ] }),
2407
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-body" }),
2408
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-footer" })
2409
+ ] });
2410
+ if (options.position) {
2411
+ this.dialog.style.position = "absolute";
2412
+ this.dialog.style.left = `${options.position.x}px`;
2413
+ this.dialog.style.top = `${options.position.y}px`;
2414
+ this.dialog.style.transform = "translate(-50%, -50%)";
2415
+ }
2416
+ this.overlay.appendChild(this.dialog);
2417
+ this.container = this.overlay;
2418
+ this.handleKeyDown = this.handleKeyDown.bind(this);
2419
+ document.addEventListener("keydown", this.handleKeyDown);
2420
+ }
2421
+ handleKeyDown(e) {
2422
+ if (e.key === "Escape") {
2423
+ this.close();
2424
+ }
2181
2425
  }
2182
- render() {
2183
- const c = styler("canvas", canvas_default, this.classPrefix);
2184
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2426
+ get body() {
2427
+ return this.dialog.querySelector(".g3p-modal-body");
2428
+ }
2429
+ get footer() {
2430
+ return this.dialog.querySelector(".g3p-modal-footer");
2431
+ }
2432
+ show(parent) {
2433
+ parent.appendChild(this.container);
2434
+ const firstInput = this.dialog.querySelector("input, select, button");
2435
+ if (firstInput) firstInput.focus();
2436
+ }
2437
+ close() {
2438
+ document.removeEventListener("keydown", this.handleKeyDown);
2439
+ this.container.remove();
2440
+ this.onClose();
2441
+ }
2442
+ };
2443
+ var NewNodeModal = class extends Modal {
2444
+ fieldInputs = /* @__PURE__ */ new Map();
2445
+ typeSelect;
2446
+ submitCallback;
2447
+ fields;
2448
+ constructor(options) {
2449
+ super({
2450
+ title: "New Node",
2451
+ onClose: () => options.onCancel?.()
2452
+ });
2453
+ this.submitCallback = options.onSubmit;
2454
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2455
+ this.renderBody(options.nodeTypes);
2456
+ this.renderFooter();
2457
+ }
2458
+ renderBody(nodeTypes) {
2459
+ this.body.innerHTML = "";
2460
+ for (const [name, type] of this.fields) {
2461
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2462
+ const fieldGroup = this.renderField(name, label, type);
2463
+ this.body.appendChild(fieldGroup);
2464
+ }
2465
+ if (nodeTypes && nodeTypes.length > 0) {
2466
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2468
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2469
+ "select",
2470
+ {
2471
+ className: "g3p-modal-select",
2472
+ ref: (el) => this.typeSelect = el,
2473
+ children: [
2474
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Default" }),
2475
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: type }))
2476
+ ]
2477
+ }
2478
+ )
2479
+ ] });
2480
+ this.body.appendChild(typeGroup);
2481
+ }
2482
+ }
2483
+ renderField(name, label, type) {
2484
+ if (type === "boolean") {
2485
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2486
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2487
+ "input",
2488
+ {
2489
+ type: "checkbox",
2490
+ className: "g3p-modal-checkbox",
2491
+ ref: (el) => this.fieldInputs.set(name, el)
2492
+ }
2493
+ ),
2494
+ label
2495
+ ] }) });
2496
+ }
2497
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2498
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2499
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2500
+ "input",
2501
+ {
2502
+ type: type === "number" ? "number" : "text",
2503
+ className: "g3p-modal-input",
2504
+ placeholder: `Enter ${label.toLowerCase()}`,
2505
+ ref: (el) => this.fieldInputs.set(name, el)
2506
+ }
2507
+ )
2508
+ ] });
2509
+ }
2510
+ renderFooter() {
2511
+ this.footer.innerHTML = "";
2512
+ this.footer.appendChild(
2513
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2514
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2515
+ "button",
2516
+ {
2517
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2518
+ onClick: () => this.close(),
2519
+ children: "Cancel"
2520
+ }
2521
+ ),
2522
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2523
+ "button",
2524
+ {
2525
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2526
+ onClick: () => this.submit(),
2527
+ children: "Create"
2528
+ }
2529
+ )
2530
+ ] })
2531
+ );
2532
+ }
2533
+ submit() {
2534
+ const data = {};
2535
+ for (const [name, type] of this.fields) {
2536
+ const input = this.fieldInputs.get(name);
2537
+ if (!input) continue;
2538
+ if (type === "boolean") {
2539
+ data[name] = input.checked;
2540
+ } else if (type === "number") {
2541
+ const val = input.value;
2542
+ if (val) data[name] = Number(val);
2543
+ } else {
2544
+ const val = input.value.trim();
2545
+ if (val) data[name] = val;
2546
+ }
2547
+ }
2548
+ if (Object.keys(data).length === 0) {
2549
+ const firstInput = this.fieldInputs.values().next().value;
2550
+ if (firstInput) firstInput.focus();
2551
+ return;
2552
+ }
2553
+ if (this.typeSelect?.value) {
2554
+ data.type = this.typeSelect.value;
2555
+ }
2556
+ document.removeEventListener("keydown", this.handleKeyDown);
2557
+ this.container.remove();
2558
+ this.submitCallback(data);
2559
+ }
2560
+ };
2561
+ var EditNodeModal = class extends Modal {
2562
+ fieldInputs = /* @__PURE__ */ new Map();
2563
+ typeSelect;
2564
+ node;
2565
+ fields;
2566
+ submitCallback;
2567
+ deleteCallback;
2568
+ constructor(options) {
2569
+ super({
2570
+ title: "Edit Node",
2571
+ position: options.position,
2572
+ onClose: () => options.onCancel?.()
2573
+ });
2574
+ this.node = options.node;
2575
+ this.submitCallback = options.onSubmit;
2576
+ this.deleteCallback = options.onDelete;
2577
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2578
+ if (!options.fields && !this.node.title)
2579
+ this.node = { ...this.node, title: this.node.id };
2580
+ this.renderBody(options.nodeTypes);
2581
+ this.renderFooter();
2582
+ }
2583
+ renderBody(nodeTypes) {
2584
+ console.log(`renderBody`, this.node);
2585
+ this.body.innerHTML = "";
2586
+ for (const [name, type] of this.fields) {
2587
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2588
+ const currentValue = this.node[name];
2589
+ const fieldGroup = this.renderField(name, label, type, currentValue);
2590
+ this.body.appendChild(fieldGroup);
2591
+ }
2592
+ if (nodeTypes && nodeTypes.length > 0) {
2593
+ const currentType = this.node.type ?? "";
2594
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2595
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2596
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2597
+ "select",
2598
+ {
2599
+ className: "g3p-modal-select",
2600
+ ref: (el) => this.typeSelect = el,
2601
+ children: [
2602
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2603
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2604
+ ]
2605
+ }
2606
+ )
2607
+ ] });
2608
+ this.body.appendChild(typeGroup);
2609
+ }
2610
+ }
2611
+ renderField(name, label, type, currentValue) {
2612
+ if (type === "boolean") {
2613
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2614
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2615
+ "input",
2616
+ {
2617
+ type: "checkbox",
2618
+ className: "g3p-modal-checkbox",
2619
+ checked: !!currentValue,
2620
+ ref: (el) => this.fieldInputs.set(name, el)
2621
+ }
2622
+ ),
2623
+ label
2624
+ ] }) });
2625
+ }
2626
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2627
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2628
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2629
+ "input",
2630
+ {
2631
+ type: type === "number" ? "number" : "text",
2632
+ className: "g3p-modal-input",
2633
+ value: currentValue ?? "",
2634
+ ref: (el) => this.fieldInputs.set(name, el)
2635
+ }
2636
+ )
2637
+ ] });
2638
+ }
2639
+ renderFooter() {
2640
+ this.footer.innerHTML = "";
2641
+ this.footer.appendChild(
2642
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2643
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2644
+ "button",
2645
+ {
2646
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2647
+ onClick: () => this.delete(),
2648
+ children: "Delete"
2649
+ }
2650
+ ),
2651
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2652
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2653
+ "button",
2654
+ {
2655
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2656
+ onClick: () => this.close(),
2657
+ children: "Cancel"
2658
+ }
2659
+ ),
2660
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2661
+ "button",
2662
+ {
2663
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2664
+ onClick: () => this.submit(),
2665
+ children: "Save"
2666
+ }
2667
+ )
2668
+ ] })
2669
+ );
2670
+ }
2671
+ submit() {
2672
+ const data = { ...this.node };
2673
+ for (const [name, type] of this.fields) {
2674
+ const input = this.fieldInputs.get(name);
2675
+ if (!input) continue;
2676
+ if (type === "boolean") {
2677
+ data[name] = input.checked;
2678
+ } else if (type === "number") {
2679
+ const val = input.value;
2680
+ data[name] = val ? Number(val) : void 0;
2681
+ } else {
2682
+ const val = input.value.trim();
2683
+ data[name] = val || void 0;
2684
+ }
2685
+ }
2686
+ if (this.typeSelect) {
2687
+ data.type = this.typeSelect.value || void 0;
2688
+ }
2689
+ document.removeEventListener("keydown", this.handleKeyDown);
2690
+ this.container.remove();
2691
+ this.submitCallback(data);
2692
+ }
2693
+ delete() {
2694
+ document.removeEventListener("keydown", this.handleKeyDown);
2695
+ this.container.remove();
2696
+ this.deleteCallback?.();
2697
+ }
2698
+ };
2699
+ var EditEdgeModal = class _EditEdgeModal extends Modal {
2700
+ sourceMarkerSelect;
2701
+ targetMarkerSelect;
2702
+ typeSelect;
2703
+ edge;
2704
+ submitCallback;
2705
+ deleteCallback;
2706
+ static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
2707
+ constructor(options) {
2708
+ super({
2709
+ title: "Edit Edge",
2710
+ onClose: () => options.onCancel?.()
2711
+ });
2712
+ this.edge = options.edge;
2713
+ this.submitCallback = options.onSubmit;
2714
+ this.deleteCallback = options.onDelete;
2715
+ this.renderBody(options.edgeTypes);
2716
+ this.renderFooter();
2717
+ }
2718
+ renderBody(edgeTypes) {
2719
+ this.body.innerHTML = "";
2720
+ const currentSourceMarker = this.edge.source?.marker ?? "none";
2721
+ const currentTargetMarker = this.edge.target?.marker ?? "arrow";
2722
+ const sourceGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2723
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Source Marker" }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2725
+ "select",
2726
+ {
2727
+ className: "g3p-modal-select",
2728
+ ref: (el) => this.sourceMarkerSelect = el,
2729
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentSourceMarker, children: type }))
2730
+ }
2731
+ )
2732
+ ] });
2733
+ this.body.appendChild(sourceGroup);
2734
+ const targetGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Target Marker" }),
2736
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2737
+ "select",
2738
+ {
2739
+ className: "g3p-modal-select",
2740
+ ref: (el) => this.targetMarkerSelect = el,
2741
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentTargetMarker, children: type }))
2742
+ }
2743
+ )
2744
+ ] });
2745
+ this.body.appendChild(targetGroup);
2746
+ if (edgeTypes && edgeTypes.length > 0) {
2747
+ const currentType = this.edge.type ?? "";
2748
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2749
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2750
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2751
+ "select",
2752
+ {
2753
+ className: "g3p-modal-select",
2754
+ ref: (el) => this.typeSelect = el,
2755
+ children: [
2756
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2757
+ edgeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2758
+ ]
2759
+ }
2760
+ )
2761
+ ] });
2762
+ this.body.appendChild(typeGroup);
2763
+ }
2764
+ }
2765
+ renderFooter() {
2766
+ this.footer.innerHTML = "";
2767
+ this.footer.appendChild(
2768
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2769
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2770
+ "button",
2771
+ {
2772
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2773
+ onClick: () => this.delete(),
2774
+ children: "Delete"
2775
+ }
2776
+ ),
2777
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2778
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2779
+ "button",
2780
+ {
2781
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2782
+ onClick: () => this.close(),
2783
+ children: "Cancel"
2784
+ }
2785
+ ),
2786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2787
+ "button",
2788
+ {
2789
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2790
+ onClick: () => this.submit(),
2791
+ children: "Save"
2792
+ }
2793
+ )
2794
+ ] })
2795
+ );
2796
+ }
2797
+ submit() {
2798
+ const data = {
2799
+ ...this.edge,
2800
+ source: {
2801
+ ...this.edge.source,
2802
+ marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
2803
+ },
2804
+ target: {
2805
+ ...this.edge.target,
2806
+ marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
2807
+ }
2808
+ };
2809
+ if (this.typeSelect) {
2810
+ data.type = this.typeSelect.value || void 0;
2811
+ }
2812
+ document.removeEventListener("keydown", this.handleKeyDown);
2813
+ this.container.remove();
2814
+ this.submitCallback(data);
2815
+ }
2816
+ delete() {
2817
+ document.removeEventListener("keydown", this.handleKeyDown);
2818
+ this.container.remove();
2819
+ this.deleteCallback?.();
2820
+ }
2821
+ };
2822
+
2823
+ // src/canvas/canvas.tsx
2824
+ var import_styles = __toESM(require("./styles.css?raw"), 1);
2825
+ var import_jsx_runtime6 = require("jsx-dom/jsx-runtime");
2826
+ var log10 = logger("canvas");
2827
+ var Canvas = class {
2828
+ container;
2829
+ root;
2830
+ group;
2831
+ transform;
2832
+ bounds;
2833
+ measurement;
2834
+ allNodes;
2835
+ curNodes;
2836
+ curSegs;
2837
+ updating;
2838
+ // Pan-zoom state
2839
+ panScale = null;
2840
+ zoomControls;
2841
+ // Edit mode state machine
2842
+ editMode;
2843
+ api;
2844
+ // New-edge visual element
2845
+ newEdgeEl;
2846
+ // Pending drag state (for double-click debounce)
2847
+ pendingDrag = null;
2848
+ constructor(api, options) {
2849
+ Object.assign(this, options);
2850
+ this.api = api;
2851
+ this.allNodes = /* @__PURE__ */ new Map();
2852
+ this.curNodes = /* @__PURE__ */ new Map();
2853
+ this.curSegs = /* @__PURE__ */ new Map();
2854
+ this.updating = false;
2855
+ this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2856
+ this.transform = { x: 0, y: 0, scale: 1 };
2857
+ this.editMode = new EditMode();
2858
+ this.editMode.editable = this.editable;
2859
+ this.createMeasurementContainer();
2860
+ this.createCanvasContainer();
2861
+ if (this.panZoom) this.setupPanZoom();
2862
+ }
2863
+ createMeasurementContainer() {
2864
+ this.measurement = document.createElement("div");
2865
+ this.measurement.style.cssText = `
2866
+ position: absolute;
2867
+ left: -9999px;
2868
+ top: -9999px;
2869
+ visibility: hidden;
2870
+ pointer-events: none;
2871
+ `;
2872
+ document.body.appendChild(this.measurement);
2873
+ }
2874
+ getNode(key) {
2875
+ const node = this.allNodes.get(key);
2876
+ if (!node) throw new Error(`node not found: ${key}`);
2877
+ return node;
2878
+ }
2879
+ update() {
2880
+ let bx0 = Infinity, by0 = Infinity;
2881
+ let bx1 = -Infinity, by1 = -Infinity;
2882
+ for (const node of this.curNodes.values()) {
2883
+ const { x, y } = node.pos;
2884
+ const { w, h } = node.data.dims;
2885
+ const nx0 = x, nx1 = x + w;
2886
+ const ny0 = y, ny1 = y + h;
2887
+ bx0 = Math.min(bx0, nx0);
2888
+ by0 = Math.min(by0, ny0);
2889
+ bx1 = Math.max(bx1, nx1);
2890
+ by1 = Math.max(by1, ny1);
2891
+ }
2892
+ this.bounds = { min: { x: bx0, y: by0 }, max: { x: bx1, y: by1 } };
2893
+ this.root.setAttribute("viewBox", this.viewBox());
2894
+ }
2895
+ addNode(gnode) {
2896
+ if (this.curNodes.has(gnode.id))
2897
+ throw new Error("node already exists");
2898
+ const { key } = gnode;
2899
+ let node;
2900
+ if (gnode.isDummy) {
2901
+ node = new Node2(this, gnode, true);
2902
+ node.renderContainer();
2903
+ this.allNodes.set(key, node);
2904
+ } else {
2905
+ if (!this.allNodes.has(key))
2906
+ throw new Error("node has not been measured");
2907
+ node = this.getNode(key);
2908
+ }
2909
+ this.curNodes.set(gnode.id, node);
2910
+ node.append();
2911
+ node.setPos(gnode.pos);
2912
+ }
2913
+ updateNode(gnode) {
2914
+ if (gnode.isDummy) throw new Error("dummy node cannot be updated");
2915
+ const node = this.getNode(gnode.key);
2916
+ const cur = this.curNodes.get(gnode.id);
2917
+ if (cur) cur.remove();
2918
+ this.curNodes.set(gnode.id, node);
2919
+ node.append();
2920
+ }
2921
+ deleteNode(gnode) {
2922
+ const node = this.getNode(gnode.key);
2923
+ this.curNodes.delete(gnode.id);
2924
+ node.remove();
2925
+ }
2926
+ addSeg(gseg, g) {
2927
+ if (this.curSegs.has(gseg.id))
2928
+ throw new Error("seg already exists");
2929
+ const seg = new Seg2(this, gseg, g);
2930
+ this.curSegs.set(gseg.id, seg);
2931
+ seg.append();
2932
+ }
2933
+ updateSeg(gseg, g) {
2934
+ const seg = this.curSegs.get(gseg.id);
2935
+ if (!seg) throw new Error("seg not found");
2936
+ seg.update(gseg, g);
2937
+ }
2938
+ deleteSeg(gseg) {
2939
+ const seg = this.curSegs.get(gseg.id);
2940
+ if (!seg) throw new Error("seg not found");
2941
+ this.curSegs.delete(gseg.id);
2942
+ seg.remove();
2943
+ }
2944
+ async measureNodes(nodes) {
2945
+ const newNodes = /* @__PURE__ */ new Map();
2946
+ for (const data of nodes) {
2947
+ const node = new Node2(this, data);
2948
+ newNodes.set(data.data, node);
2949
+ this.measurement.appendChild(node.content);
2950
+ }
2951
+ await new Promise(requestAnimationFrame);
2952
+ const isVertical = this.orientation === "TB" || this.orientation === "BT";
2953
+ for (const node of newNodes.values()) {
2954
+ node.measure(isVertical);
2955
+ const { id, version } = node.data;
2956
+ const key = `k:${id}:${version}`;
2957
+ this.allNodes.set(key, node);
2958
+ node.renderContainer();
2959
+ }
2960
+ this.measurement.innerHTML = "";
2961
+ return newNodes;
2962
+ }
2963
+ // ========== Mouse event handlers ==========
2964
+ onClick(e) {
2965
+ const hit = this.hitTest(e.clientX, e.clientY);
2966
+ if (hit.type === "node") {
2967
+ this.api.handleClickNode(hit.node.data.id);
2968
+ } else if (hit.type === "edge") {
2969
+ this.api.handleClickEdge(hit.segId);
2970
+ }
2971
+ }
2972
+ onDoubleClick(e) {
2973
+ if (this.pendingDrag) {
2974
+ window.clearTimeout(this.pendingDrag.timeout);
2975
+ this.pendingDrag = null;
2976
+ }
2977
+ if (!this.editMode.editable) return;
2978
+ const hit = this.hitTest(e.clientX, e.clientY);
2979
+ if (hit.type === "node") {
2980
+ if (hit.node.isDummy) return;
2981
+ this.api.handleEditNode(hit.node.data.id);
2982
+ } else if (hit.type === "edge") {
2983
+ this.api.handleEditEdge(hit.segId);
2984
+ } else {
2985
+ this.api.handleNewNode();
2986
+ }
2987
+ }
2988
+ // ========== Built-in Modals ==========
2989
+ /** Show the new node modal */
2990
+ showNewNodeModal(callback) {
2991
+ const nodeTypes = Object.keys(this.nodeTypes);
2992
+ const fields = this.api.getNodeFields();
2993
+ const modal = new NewNodeModal({
2994
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
2995
+ fields: fields.size > 0 ? fields : void 0,
2996
+ onSubmit: (data) => {
2997
+ callback(data);
2998
+ }
2999
+ });
3000
+ modal.show(document.body);
3001
+ }
3002
+ /** Show the edit node modal */
3003
+ showEditNodeModal(node, callback) {
3004
+ const nodeTypes = Object.keys(this.nodeTypes);
3005
+ const fields = this.api.getNodeFields();
3006
+ const modal = new EditNodeModal({
3007
+ node,
3008
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
3009
+ fields: fields.size > 0 ? fields : void 0,
3010
+ onSubmit: (data) => {
3011
+ callback(data);
3012
+ },
3013
+ onDelete: () => {
3014
+ this.api.handleDeleteNode(node.id);
3015
+ }
3016
+ });
3017
+ modal.show(document.body);
3018
+ }
3019
+ /** Show the edit edge modal */
3020
+ showEditEdgeModal(edge, callback) {
3021
+ const modal = new EditEdgeModal({
3022
+ edge,
3023
+ edgeTypes: Object.keys(this.edgeTypes),
3024
+ onSubmit: callback,
3025
+ onDelete: () => {
3026
+ this.api.handleDeleteEdge(edge.id);
3027
+ }
3028
+ });
3029
+ modal.show(document.body);
3030
+ }
3031
+ onContextMenu(e) {
3032
+ }
3033
+ groupTransform() {
3034
+ return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
3035
+ }
3036
+ viewBox() {
3037
+ const p = this.padding;
3038
+ const x = this.bounds.min.x - p;
3039
+ const y = this.bounds.min.y - p;
3040
+ const w = this.bounds.max.x - this.bounds.min.x + p * 2;
3041
+ const h = this.bounds.max.y - this.bounds.min.y + p * 2;
3042
+ return `${x} ${y} ${w} ${h}`;
3043
+ }
3044
+ generateDynamicStyles() {
3045
+ let css = "";
3046
+ css += themeToCSS(this.theme, `.g3p-canvas-container`);
3047
+ for (const [type, vars] of Object.entries(this.nodeTypes)) {
3048
+ css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
3049
+ }
3050
+ for (const [type, vars] of Object.entries(this.edgeTypes)) {
3051
+ css += themeToCSS(vars, `.g3p-edge-type-${type}`);
3052
+ }
3053
+ return css;
3054
+ }
3055
+ createCanvasContainer() {
3056
+ if (!document.getElementById("g3p-styles")) {
3057
+ const baseStyleEl = document.createElement("style");
3058
+ baseStyleEl.id = "g3p-styles";
3059
+ baseStyleEl.textContent = import_styles.default;
3060
+ document.head.appendChild(baseStyleEl);
3061
+ }
3062
+ const dynamicStyles = this.generateDynamicStyles();
3063
+ if (dynamicStyles) {
3064
+ const dynamicStyleEl = document.createElement("style");
3065
+ dynamicStyleEl.textContent = dynamicStyles;
3066
+ document.head.appendChild(dynamicStyleEl);
3067
+ }
3068
+ const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
3069
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2185
3070
  "div",
2186
3071
  {
2187
- className: c("container"),
3072
+ className: `g3p-canvas-container ${colorModeClass}`.trim(),
2188
3073
  ref: (el) => this.container = el,
2189
3074
  onContextMenu: this.onContextMenu.bind(this),
2190
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3075
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2191
3076
  "svg",
2192
3077
  {
2193
3078
  ref: (el) => this.root = el,
2194
- className: c("root"),
3079
+ className: "g3p-canvas-root",
2195
3080
  width: this.width,
2196
3081
  height: this.height,
2197
3082
  viewBox: this.viewBox(),
2198
3083
  preserveAspectRatio: "xMidYMid meet",
2199
3084
  onClick: this.onClick.bind(this),
2200
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2201
- "g",
2202
- {
2203
- ref: (el) => this.group = el,
2204
- transform: this.groupTransform()
2205
- }
2206
- )
3085
+ onDblClick: this.onDoubleClick.bind(this),
3086
+ children: [
3087
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
3088
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3089
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
3090
+ ] }),
3091
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3092
+ "g",
3093
+ {
3094
+ ref: (el) => this.group = el,
3095
+ transform: this.groupTransform()
3096
+ }
3097
+ )
3098
+ ]
2207
3099
  }
2208
3100
  )
2209
3101
  }
2210
3102
  );
2211
3103
  }
3104
+ // ==================== Pan-Zoom ====================
3105
+ setupPanZoom() {
3106
+ this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false });
3107
+ this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
3108
+ document.addEventListener("mousemove", this.onMouseMove.bind(this));
3109
+ document.addEventListener("mouseup", this.onMouseUp.bind(this));
3110
+ document.addEventListener("keydown", this.onKeyDown.bind(this));
3111
+ this.createZoomControls();
3112
+ }
3113
+ onKeyDown(e) {
3114
+ if (e.key === "Escape" && this.editMode.isCreatingEdge) {
3115
+ this.endNewEdge(true);
3116
+ }
3117
+ }
3118
+ /** Convert screen coordinates to canvas-relative coordinates */
3119
+ screenToCanvas(screen) {
3120
+ const rect = this.container.getBoundingClientRect();
3121
+ return canvasPos(screen.x - rect.left, screen.y - rect.top);
3122
+ }
3123
+ /** Convert canvas coordinates to graph coordinates */
3124
+ canvasToGraph(canvas) {
3125
+ const vb = this.currentViewBox();
3126
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
3127
+ return graphPos(
3128
+ vb.x - offsetX + canvas.x * scale,
3129
+ vb.y - offsetY + canvas.y * scale
3130
+ );
3131
+ }
3132
+ /** Convert screen coordinates to graph coordinates */
3133
+ screenToGraph(screen) {
3134
+ const canvas = this.screenToCanvas(screen);
3135
+ return this.canvasToGraph(canvas);
3136
+ }
3137
+ /**
3138
+ * Get the effective scale from canvas pixels to graph units,
3139
+ * accounting for preserveAspectRatio="xMidYMid meet" which uses
3140
+ * the smaller scale (to fit) and centers the content.
3141
+ */
3142
+ getEffectiveScale() {
3143
+ const vb = this.currentViewBox();
3144
+ const rect = this.container.getBoundingClientRect();
3145
+ const scaleX = vb.w / rect.width;
3146
+ const scaleY = vb.h / rect.height;
3147
+ const scale = Math.max(scaleX, scaleY);
3148
+ const actualW = rect.width * scale;
3149
+ const actualH = rect.height * scale;
3150
+ const offsetX = (actualW - vb.w) / 2;
3151
+ const offsetY = (actualH - vb.h) / 2;
3152
+ return { scale, offsetX, offsetY };
3153
+ }
3154
+ /** Get current viewBox as an object */
3155
+ currentViewBox() {
3156
+ const p = this.padding;
3157
+ const t = this.transform;
3158
+ const baseX = this.bounds.min.x - p;
3159
+ const baseY = this.bounds.min.y - p;
3160
+ const baseW = this.bounds.max.x - this.bounds.min.x + p * 2;
3161
+ const baseH = this.bounds.max.y - this.bounds.min.y + p * 2;
3162
+ const cx = baseX + baseW / 2;
3163
+ const cy = baseY + baseH / 2;
3164
+ const w = baseW / t.scale;
3165
+ const h = baseH / t.scale;
3166
+ const x = cx - w / 2 - t.x;
3167
+ const y = cy - h / 2 - t.y;
3168
+ return { x, y, w, h };
3169
+ }
3170
+ onWheel(e) {
3171
+ e.preventDefault();
3172
+ const zoomFactor = 1.1;
3173
+ const delta = e.deltaY > 0 ? 1 / zoomFactor : zoomFactor;
3174
+ const screenCursor = screenPos(e.clientX, e.clientY);
3175
+ const canvasCursor = this.screenToCanvas(screenCursor);
3176
+ const graphCursor = this.canvasToGraph(canvasCursor);
3177
+ const oldScale = this.transform.scale;
3178
+ const newScale = Math.max(0.1, Math.min(10, oldScale * delta));
3179
+ this.transform.scale = newScale;
3180
+ const newGraphCursor = this.canvasToGraph(canvasCursor);
3181
+ this.transform.x += newGraphCursor.x - graphCursor.x;
3182
+ this.transform.y += newGraphCursor.y - graphCursor.y;
3183
+ this.applyTransform();
3184
+ }
3185
+ onMouseDown(e) {
3186
+ if (e.button !== 0) return;
3187
+ if (e.target.closest(".g3p-zoom-controls")) return;
3188
+ const hit = this.hitTest(e.clientX, e.clientY);
3189
+ if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
3190
+ const node = hit.node;
3191
+ if (node.isDummy) return;
3192
+ e.preventDefault();
3193
+ e.stopPropagation();
3194
+ const startGraph = this.screenToGraph(hit.center);
3195
+ const portId = hit.type === "port" ? hit.port : void 0;
3196
+ this.pendingDrag = {
3197
+ timeout: window.setTimeout(() => {
3198
+ if (this.pendingDrag) {
3199
+ this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
3200
+ this.pendingDrag = null;
3201
+ }
3202
+ }, 200),
3203
+ nodeId: node.data.id,
3204
+ startGraph,
3205
+ portId
3206
+ };
3207
+ return;
3208
+ }
3209
+ if (hit.type === "canvas" || hit.type === "edge") {
3210
+ const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
3211
+ this.editMode.startPan(startCanvas, { ...this.transform });
3212
+ const { scale } = this.getEffectiveScale();
3213
+ this.panScale = { x: scale, y: scale };
3214
+ this.container.style.cursor = "grabbing";
3215
+ e.preventDefault();
3216
+ }
3217
+ }
3218
+ onMouseMove(e) {
3219
+ if (this.editMode.isCreatingEdge) {
3220
+ const screenCursor = screenPos(e.clientX, e.clientY);
3221
+ const canvasCursor = this.screenToCanvas(screenCursor);
3222
+ const graphCursor = this.canvasToGraph(canvasCursor);
3223
+ this.editMode.updateNewEdgePosition(graphCursor);
3224
+ this.updateNewEdgeVisual();
3225
+ this.detectHoverTarget(e.clientX, e.clientY);
3226
+ return;
3227
+ }
3228
+ if (!this.editMode.isPanning || !this.panScale) return;
3229
+ const panState = this.editMode.state;
3230
+ if (panState.type !== "panning") return;
3231
+ const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
3232
+ const dx = current.x - panState.startCanvas.x;
3233
+ const dy = current.y - panState.startCanvas.y;
3234
+ this.transform.x = panState.startTransform.x + dx * this.panScale.x;
3235
+ this.transform.y = panState.startTransform.y + dy * this.panScale.y;
3236
+ this.applyTransform();
3237
+ }
3238
+ onMouseUp(e) {
3239
+ if (this.pendingDrag) {
3240
+ window.clearTimeout(this.pendingDrag.timeout);
3241
+ this.pendingDrag = null;
3242
+ }
3243
+ if (this.editMode.isCreatingEdge) {
3244
+ this.endNewEdge(false);
3245
+ return;
3246
+ }
3247
+ if (!this.editMode.isPanning) return;
3248
+ this.editMode.reset();
3249
+ this.panScale = null;
3250
+ this.container.style.cursor = "";
3251
+ }
3252
+ applyTransform() {
3253
+ const vb = this.currentViewBox();
3254
+ this.root.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
3255
+ this.updateZoomLevel();
3256
+ }
3257
+ createZoomControls() {
3258
+ this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "g3p-zoom-controls", children: [
3259
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
3260
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
3261
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
3262
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
3263
+ ] });
3264
+ this.container.appendChild(this.zoomControls);
3265
+ }
3266
+ updateZoomLevel() {
3267
+ const level = this.container.querySelector("#g3p-zoom-level");
3268
+ if (level) {
3269
+ level.textContent = `${Math.round(this.transform.scale * 100)}%`;
3270
+ }
3271
+ }
3272
+ zoomIn() {
3273
+ this.transform.scale = Math.min(10, this.transform.scale * 1.2);
3274
+ this.applyTransform();
3275
+ }
3276
+ zoomOut() {
3277
+ this.transform.scale = Math.max(0.1, this.transform.scale / 1.2);
3278
+ this.applyTransform();
3279
+ }
3280
+ zoomReset() {
3281
+ this.transform = { x: 0, y: 0, scale: 1 };
3282
+ this.applyTransform();
3283
+ }
3284
+ // ==================== New-Edge Mode ====================
3285
+ /** Start creating a new edge from a node */
3286
+ startNewEdge(sourceNodeId, startGraph, sourcePortId) {
3287
+ this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
3288
+ this.updateNewEdgeVisual();
3289
+ this.container.style.cursor = "crosshair";
3290
+ }
3291
+ /** Update the new-edge visual during drag */
3292
+ updateNewEdgeVisual() {
3293
+ const state = this.editMode.getNewEdgeState();
3294
+ if (!state) {
3295
+ this.removeNewEdgeVisual();
3296
+ return;
3297
+ }
3298
+ if (this.newEdgeEl) {
3299
+ this.newEdgeEl.remove();
3300
+ }
3301
+ this.newEdgeEl = renderNewEdge({
3302
+ start: state.startGraph,
3303
+ end: state.currentGraph
3304
+ });
3305
+ this.group.appendChild(this.newEdgeEl);
3306
+ }
3307
+ /** Remove the new-edge visual */
3308
+ removeNewEdgeVisual() {
3309
+ if (this.newEdgeEl) {
3310
+ this.newEdgeEl.remove();
3311
+ this.newEdgeEl = void 0;
3312
+ }
3313
+ }
3314
+ /** Complete or cancel the new-edge creation */
3315
+ endNewEdge(cancelled = false) {
3316
+ const state = this.editMode.getNewEdgeState();
3317
+ if (!state) return;
3318
+ if (!cancelled) {
3319
+ const { target, source } = state;
3320
+ if (target?.type == "node") {
3321
+ this.api.handleAddEdge({ id: "", source, target });
3322
+ } else {
3323
+ this.api.handleNewNodeFrom(source);
3324
+ }
3325
+ }
3326
+ this.removeNewEdgeVisual();
3327
+ this.clearDropTargetHighlight();
3328
+ this.editMode.reset();
3329
+ this.container.style.cursor = "";
3330
+ }
3331
+ /** Find node data by internal ID */
3332
+ findNodeDataById(nodeId) {
3333
+ for (const node of this.curNodes.values()) {
3334
+ if (node.data?.id === nodeId) {
3335
+ return node.data.data;
3336
+ }
3337
+ }
3338
+ return null;
3339
+ }
3340
+ /** Set hover target for new-edge mode */
3341
+ setNewEdgeHoverTarget(id, port) {
3342
+ this.clearDropTargetHighlight();
3343
+ this.editMode.setHoverTarget({ type: "node", id, port });
3344
+ if (port) {
3345
+ const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
3346
+ if (portEl) portEl.classList.add("g3p-drop-target");
3347
+ } else {
3348
+ const node = this.curNodes.get(id);
3349
+ if (node?.container) node.container.classList.add("g3p-drop-target");
3350
+ }
3351
+ }
3352
+ /** Clear hover target for new-edge mode */
3353
+ clearNewEdgeHoverTarget() {
3354
+ this.clearDropTargetHighlight();
3355
+ this.editMode.setHoverTarget(null);
3356
+ }
3357
+ /** Remove drop target highlight from all elements */
3358
+ clearDropTargetHighlight() {
3359
+ for (const node of this.curNodes.values()) {
3360
+ node.container?.classList.remove("g3p-drop-target");
3361
+ }
3362
+ this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
3363
+ el.classList.remove("g3p-drop-target");
3364
+ });
3365
+ }
3366
+ /** Detect hover target during new-edge drag using elementFromPoint */
3367
+ detectHoverTarget(clientX, clientY) {
3368
+ if (this.newEdgeEl) {
3369
+ this.newEdgeEl.style.display = "none";
3370
+ }
3371
+ const el = document.elementFromPoint(clientX, clientY);
3372
+ if (this.newEdgeEl) {
3373
+ this.newEdgeEl.style.display = "";
3374
+ }
3375
+ if (!el) {
3376
+ this.clearNewEdgeHoverTarget();
3377
+ return;
3378
+ }
3379
+ const portEl = el.closest(".g3p-node-port");
3380
+ if (portEl) {
3381
+ const nodeId = portEl.getAttribute("data-node-id");
3382
+ const portId = portEl.getAttribute("data-port-id");
3383
+ if (nodeId && portId) {
3384
+ const node = this.curNodes.get(nodeId);
3385
+ if (node && !node.isDummy) {
3386
+ this.setNewEdgeHoverTarget(nodeId, portId);
3387
+ return;
3388
+ }
3389
+ }
3390
+ }
3391
+ const nodeEl = el.closest(".g3p-node-container");
3392
+ if (nodeEl) {
3393
+ const nodeId = nodeEl.getAttribute("data-node-id");
3394
+ if (nodeId) {
3395
+ const node = this.curNodes.get(nodeId);
3396
+ if (node && !node.isDummy) {
3397
+ this.setNewEdgeHoverTarget(node.data.id);
3398
+ return;
3399
+ }
3400
+ }
3401
+ }
3402
+ this.clearNewEdgeHoverTarget();
3403
+ }
3404
+ // ==================== Hit Testing ====================
3405
+ /** Result of a hit test */
3406
+ hitTest(clientX, clientY) {
3407
+ const el = document.elementFromPoint(clientX, clientY);
3408
+ if (!el) return { type: "canvas" };
3409
+ const getCenter = (el2) => {
3410
+ const rect = el2.getBoundingClientRect();
3411
+ return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
3412
+ };
3413
+ const portEl = el.closest(".g3p-node-port");
3414
+ if (portEl) {
3415
+ const nodeId = portEl.getAttribute("data-node-id");
3416
+ const portId = portEl.getAttribute("data-port-id");
3417
+ if (nodeId && portId) {
3418
+ const center = getCenter(portEl);
3419
+ const node = this.curNodes.get(nodeId);
3420
+ if (node) {
3421
+ return { type: "port", node, port: portId, center };
3422
+ }
3423
+ }
3424
+ }
3425
+ const nodeEl = el.closest(".g3p-node-container");
3426
+ if (nodeEl) {
3427
+ const nodeId = nodeEl.getAttribute("data-node-id");
3428
+ if (nodeId) {
3429
+ const borderEl = el.closest(".g3p-node-border");
3430
+ const center = getCenter(borderEl ?? nodeEl);
3431
+ const node = this.curNodes.get(nodeId);
3432
+ if (node) {
3433
+ return { type: "node", node, center };
3434
+ }
3435
+ }
3436
+ }
3437
+ const edgeEl = el.closest(".g3p-seg-container");
3438
+ if (edgeEl) {
3439
+ const segId = edgeEl.getAttribute("data-edge-id");
3440
+ if (segId) {
3441
+ return { type: "edge", segId };
3442
+ }
3443
+ }
3444
+ return { type: "canvas" };
3445
+ }
2212
3446
  };
3447
+ var themeVarMap = {
3448
+ // Canvas
3449
+ bg: "--g3p-bg",
3450
+ shadow: "--g3p-shadow",
3451
+ // Node
3452
+ border: "--g3p-border",
3453
+ borderHover: "--g3p-border-hover",
3454
+ borderSelected: "--g3p-border-selected",
3455
+ text: "--g3p-text",
3456
+ textMuted: "--g3p-text-muted",
3457
+ // Port
3458
+ bgHover: "--g3p-port-bg-hover",
3459
+ // Edge
3460
+ color: "--g3p-edge-color"
3461
+ };
3462
+ function themeToCSS(theme, selector, prefix = "") {
3463
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
3464
+ if (!entries.length) return "";
3465
+ let css = `${selector} {
3466
+ `;
3467
+ for (const [key, value] of entries) {
3468
+ let cssVar = themeVarMap[key];
3469
+ if (key === "bg" && prefix === "node") {
3470
+ cssVar = "--g3p-bg-node";
3471
+ } else if (key === "bg" && prefix === "port") {
3472
+ cssVar = "--g3p-port-bg";
3473
+ }
3474
+ if (cssVar) {
3475
+ css += ` ${cssVar}: ${value};
3476
+ `;
3477
+ }
3478
+ }
3479
+ css += "}\n";
3480
+ return css;
3481
+ }
2213
3482
 
2214
- // src/index.ts
2215
- var import_immutable9 = require("immutable");
2216
- var identity = (x) => x;
2217
- var defaultOptions2 = () => ({
2218
- ...defaultOptions,
2219
- nodeProps: identity,
2220
- edgeProps: identity,
2221
- portProps: identity,
2222
- renderNode,
2223
- nodeStyle: (() => ({})),
2224
- edgeStyle: (() => ({})),
2225
- portStyle: "outside",
2226
- width: "100%",
2227
- height: "100%",
2228
- classPrefix: "g3p"
2229
- });
3483
+ // src/canvas/render-node.tsx
3484
+ var import_jsx_runtime7 = require("jsx-dom/jsx-runtime");
3485
+ function renderNode(node, props) {
3486
+ if (typeof node == "string") node = { id: node };
3487
+ const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
3488
+ const detail = node?.detail ?? node?.description ?? node?.subtitle;
3489
+ console.log(`renderNode: ${node.id} ${title} ${detail}`);
3490
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "g3p-node-default", children: [
3491
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-title", children: title }),
3492
+ detail && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-detail", children: detail })
3493
+ ] });
3494
+ }
3495
+
3496
+ // src/api/defaults.ts
3497
+ function applyDefaults(options) {
3498
+ const { graph: graph2, canvas, props } = defaults();
3499
+ return {
3500
+ graph: { ...graph2, ...options?.graph },
3501
+ canvas: { ...canvas, ...options?.canvas },
3502
+ props: { ...props, ...options?.props }
3503
+ };
3504
+ }
3505
+ function defaults() {
3506
+ return {
3507
+ graph: {
3508
+ mergeOrder: ["target", "source"],
3509
+ nodeMargin: 15,
3510
+ dummyNodeSize: 15,
3511
+ nodeAlign: "natural",
3512
+ edgeSpacing: 10,
3513
+ turnRadius: 10,
3514
+ orientation: "TB",
3515
+ layerMargin: 5,
3516
+ alignIterations: 5,
3517
+ alignThreshold: 10,
3518
+ separateTrackSets: true,
3519
+ markerSize: 10,
3520
+ layoutSteps: null
3521
+ },
3522
+ canvas: {
3523
+ renderNode,
3524
+ width: "100%",
3525
+ height: "100%",
3526
+ padding: 20,
3527
+ editable: false,
3528
+ panZoom: true,
3529
+ markerSize: 10,
3530
+ colorMode: "system",
3531
+ theme: {},
3532
+ nodeTypes: {},
3533
+ edgeTypes: {}
3534
+ },
3535
+ props: {}
3536
+ };
3537
+ }
3538
+
3539
+ // src/api/updater.ts
3540
+ var Updater = class _Updater {
3541
+ update;
3542
+ constructor() {
3543
+ this.update = {
3544
+ addNodes: [],
3545
+ removeNodes: [],
3546
+ updateNodes: [],
3547
+ addEdges: [],
3548
+ removeEdges: [],
3549
+ updateEdges: []
3550
+ };
3551
+ }
3552
+ describe(desc) {
3553
+ this.update.description = desc;
3554
+ return this;
3555
+ }
3556
+ addNode(node) {
3557
+ this.update.addNodes.push(node);
3558
+ return this;
3559
+ }
3560
+ deleteNode(node) {
3561
+ this.update.removeNodes.push(node);
3562
+ return this;
3563
+ }
3564
+ updateNode(node) {
3565
+ this.update.updateNodes.push(node);
3566
+ return this;
3567
+ }
3568
+ addEdge(edge) {
3569
+ this.update.addEdges.push(edge);
3570
+ return this;
3571
+ }
3572
+ deleteEdge(edge) {
3573
+ this.update.removeEdges.push(edge);
3574
+ return this;
3575
+ }
3576
+ updateEdge(edge) {
3577
+ this.update.updateEdges.push(edge);
3578
+ return this;
3579
+ }
3580
+ static add(nodes, edges) {
3581
+ const updater = new _Updater();
3582
+ updater.update.addNodes = nodes;
3583
+ updater.update.addEdges = edges;
3584
+ return updater;
3585
+ }
3586
+ };
3587
+
3588
+ // src/api/api.ts
3589
+ var log11 = logger("api");
2230
3590
  var API = class {
2231
3591
  state;
2232
3592
  seq;
2233
3593
  index;
2234
3594
  canvas;
2235
- _options;
2236
3595
  options;
2237
- constructor(options) {
2238
- this._options = {
2239
- ...defaultOptions2(),
2240
- ...options
2241
- };
2242
- this.options = new Proxy(this._options, {
2243
- set: (target, prop, value) => {
2244
- target[prop] = value;
2245
- this._onOptionChange(prop);
2246
- return true;
2247
- }
2248
- });
2249
- this.state = {
2250
- nodes: (0, import_immutable9.Map)(),
2251
- edges: (0, import_immutable9.Map)(),
2252
- ports: (0, import_immutable9.Map)(),
2253
- segs: (0, import_immutable9.Map)()
2254
- };
2255
- let graph2 = new Graph({ options: this._options });
2256
- this.state.graph = graph2;
3596
+ history;
3597
+ nodeIds;
3598
+ edgeIds;
3599
+ nodeVersions;
3600
+ nodeOverrides;
3601
+ edgeOverrides;
3602
+ nodeFields;
3603
+ nextNodeId;
3604
+ nextEdgeId;
3605
+ events;
3606
+ root;
3607
+ constructor(args) {
3608
+ this.root = args.root;
3609
+ this.options = applyDefaults(args.options);
3610
+ let graph2 = new Graph({ options: this.options.graph });
3611
+ this.state = { graph: graph2, update: null };
3612
+ this.events = args.events || {};
2257
3613
  this.seq = [this.state];
2258
3614
  this.index = 0;
2259
- this.canvas = new Canvas(this._options);
2260
- this.canvas.render();
3615
+ this.nodeIds = /* @__PURE__ */ new Map();
3616
+ this.edgeIds = /* @__PURE__ */ new Map();
3617
+ this.nodeVersions = /* @__PURE__ */ new Map();
3618
+ this.nodeOverrides = /* @__PURE__ */ new Map();
3619
+ this.edgeOverrides = /* @__PURE__ */ new Map();
3620
+ this.nodeFields = /* @__PURE__ */ new Map();
3621
+ this.nextNodeId = 1;
3622
+ this.nextEdgeId = 1;
3623
+ this.canvas = new Canvas(this, {
3624
+ ...this.options.canvas,
3625
+ dummyNodeSize: this.options.graph.dummyNodeSize,
3626
+ orientation: this.options.graph.orientation
3627
+ });
3628
+ if (args.history) {
3629
+ this.history = args.history;
3630
+ } else if (args.nodes) {
3631
+ this.history = [Updater.add(args.nodes, args.edges || []).update];
3632
+ } else {
3633
+ this.history = [];
3634
+ }
2261
3635
  }
2262
- render() {
2263
- return this.canvas.container;
3636
+ /** Current history index (0-based) */
3637
+ getHistoryIndex() {
3638
+ return this.index;
3639
+ }
3640
+ /** Current history length */
3641
+ getHistoryLength() {
3642
+ return this.seq.length;
3643
+ }
3644
+ /** Toggle canvas editable mode without re-creating the graph */
3645
+ setEditable(editable) {
3646
+ this.canvas.editMode.editable = editable;
2264
3647
  }
3648
+ get graph() {
3649
+ return this.state.graph;
3650
+ }
3651
+ /** Initialize the API */
3652
+ async init() {
3653
+ const root = document.getElementById(this.root);
3654
+ if (!root) throw new Error("root element not found");
3655
+ root.appendChild(this.canvas.container);
3656
+ for (const update of this.history)
3657
+ await this.applyUpdate(update);
3658
+ }
3659
+ /** Navigate to a different state */
2265
3660
  nav(nav) {
2266
3661
  let newIndex;
2267
3662
  switch (nav) {
@@ -2283,238 +3678,409 @@ var API = class {
2283
3678
  this.applyDiff(this.index, newIndex);
2284
3679
  this.index = newIndex;
2285
3680
  this.state = this.seq[this.index];
3681
+ if (this.events.historyChange)
3682
+ this.events.historyChange(this.index, this.seq.length);
2286
3683
  }
2287
3684
  applyDiff(oldIndex, newIndex) {
2288
- const oldState = this.seq[oldIndex];
2289
- const newState = this.seq[newIndex];
2290
- this.canvas.update(() => {
2291
- for (const oldNode of oldState.nodes.values()) {
2292
- const newNode = newState.nodes.get(oldNode.id);
2293
- if (!newNode) {
2294
- this.canvas.deleteNode(oldNode);
2295
- } else if (oldNode.data !== newNode.data) {
2296
- this.canvas.deleteNode(oldNode);
2297
- this.canvas.addNode(newNode);
2298
- } else if (oldNode.pos.x !== newNode.pos.x || oldNode.pos.y !== newNode.pos.y) {
2299
- this.canvas.updateNode(newNode);
2300
- }
2301
- }
2302
- for (const newNode of newState.nodes.values()) {
2303
- if (!oldState.nodes.has(newNode.id))
2304
- this.canvas.addNode(newNode);
3685
+ const oldGraph = this.seq[oldIndex].graph;
3686
+ const newGraph = this.seq[newIndex].graph;
3687
+ for (const oldNode of oldGraph.nodes.values()) {
3688
+ const newNode = newGraph.nodes.get(oldNode.id);
3689
+ if (!newNode) this.canvas.deleteNode(oldNode);
3690
+ }
3691
+ for (const newNode of newGraph.nodes.values()) {
3692
+ const oldNode = oldGraph.nodes.get(newNode.id);
3693
+ if (!oldNode) {
3694
+ this.canvas.addNode(newNode);
3695
+ } else if (oldNode.key !== newNode.key) {
3696
+ this.canvas.deleteNode(oldNode);
3697
+ this.canvas.addNode(newNode);
3698
+ } else if (oldNode.pos !== newNode.pos) {
3699
+ this.canvas.getNode(newNode.key).setPos(newNode.pos);
2305
3700
  }
2306
- for (const oldSeg of oldState.segs.values()) {
2307
- const newSeg = newState.segs.get(oldSeg.segId);
2308
- if (!newSeg)
2309
- this.canvas.deleteSeg(oldSeg);
2310
- else if (oldSeg.svg != newSeg.svg)
2311
- this.canvas.updateSeg(newSeg);
3701
+ }
3702
+ for (const oldSeg of oldGraph.segs.values()) {
3703
+ const newSeg = newGraph.segs.get(oldSeg.id);
3704
+ if (!newSeg) {
3705
+ this.canvas.deleteSeg(oldSeg);
3706
+ } else if (oldSeg !== newSeg) {
3707
+ this.canvas.updateSeg(newSeg, newGraph);
2312
3708
  }
2313
- for (const newSeg of newState.segs.values()) {
2314
- if (!oldState.segs.has(newSeg.segId))
2315
- this.canvas.addSeg(newSeg);
3709
+ }
3710
+ for (const newSeg of newGraph.segs.values()) {
3711
+ if (!oldGraph.segs.has(newSeg.id)) {
3712
+ this.canvas.addSeg(newSeg, newGraph);
2316
3713
  }
2317
- });
3714
+ }
3715
+ this.canvas.update();
2318
3716
  }
3717
+ /** Add a node */
2319
3718
  async addNode(node) {
2320
3719
  await this.update((update) => update.addNode(node));
2321
3720
  }
3721
+ /** Delete a node */
2322
3722
  async deleteNode(node) {
2323
3723
  await this.update((update) => update.deleteNode(node));
2324
3724
  }
3725
+ /** Update a node */
2325
3726
  async updateNode(node) {
2326
3727
  await this.update((update) => update.updateNode(node));
2327
3728
  }
3729
+ /** Add an edge */
2328
3730
  async addEdge(edge) {
2329
3731
  await this.update((update) => update.addEdge(edge));
2330
3732
  }
3733
+ /** Delete an edge */
2331
3734
  async deleteEdge(edge) {
2332
3735
  await this.update((update) => update.deleteEdge(edge));
2333
3736
  }
3737
+ /** Update an edge */
3738
+ async updateEdge(edge) {
3739
+ await this.update((update) => update.updateEdge(edge));
3740
+ }
3741
+ /** Perform a batch of updates */
2334
3742
  async update(callback) {
2335
- const update = new Update();
2336
- callback(update);
2337
- await this.measureNodes(update);
2338
- const newGraph = this.state.graph.withMutations((mut) => {
2339
- this.state = {
2340
- nodes: this.state.nodes.asMutable(),
2341
- edges: this.state.edges.asMutable(),
2342
- ports: this.state.ports.asMutable(),
2343
- segs: this.state.segs.asMutable()
2344
- };
2345
- for (const node of update.updatedNodes)
2346
- this._updateNode(node, mut);
2347
- for (const edge of update.updatedEdges)
2348
- this._updateEdge(edge, mut);
2349
- for (const node of update.addedNodes)
2350
- this._addNode(node, mut);
2351
- for (const node of update.removedNodes)
3743
+ const updater = new Updater();
3744
+ callback(updater);
3745
+ await this.applyUpdate(updater.update);
3746
+ }
3747
+ /** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
3748
+ async rebuild() {
3749
+ const nodes = [...this.nodeIds.keys()];
3750
+ const edges = [...this.edgeIds.keys()];
3751
+ await this.update((updater) => {
3752
+ for (const edge of edges) updater.deleteEdge(edge);
3753
+ for (const node of nodes) updater.deleteNode(node);
3754
+ for (const node of nodes) updater.addNode(node);
3755
+ for (const edge of edges) updater.addEdge(edge);
3756
+ });
3757
+ }
3758
+ async applyUpdate(update) {
3759
+ log11.info("applyUpdate", update);
3760
+ const nodes = await this.measureNodes(update);
3761
+ const graph2 = this.state.graph.withMutations((mut) => {
3762
+ for (const edge of update.removeEdges ?? [])
3763
+ this._removeEdge(edge, mut);
3764
+ for (const node of update.removeNodes ?? [])
2352
3765
  this._removeNode(node, mut);
2353
- for (const edge of update.addedEdges)
3766
+ for (const node of update.addNodes ?? [])
3767
+ this._addNode(nodes.get(node), mut);
3768
+ for (const node of update.updateNodes ?? [])
3769
+ this._updateNode(nodes.get(node), mut);
3770
+ for (const edge of update.addEdges ?? [])
2354
3771
  this._addEdge(edge, mut);
2355
- for (const edge of update.removedEdges)
2356
- this._removeEdge(edge, mut);
3772
+ for (const edge of update.updateEdges ?? [])
3773
+ this._updateEdge(edge, mut);
3774
+ this.nodeOverrides.clear();
3775
+ this.edgeOverrides.clear();
2357
3776
  });
2358
- console.log("new graph:", newGraph);
2359
- for (const nodeId of newGraph.dirtyNodes.values()) {
2360
- const node = newGraph.getNode(nodeId);
2361
- console.log(`got pos of node ${nodeId}:`, node.pos);
2362
- if (node.isDummy) {
2363
- this.state.nodes.set(nodeId, { id: nodeId, pos: node.pos, isDummy: true });
2364
- } else {
2365
- const myNode = this.state.nodes.get(nodeId);
2366
- this.state.nodes.set(nodeId, { ...myNode, pos: node.pos });
2367
- }
2368
- }
2369
- for (const nodeId of newGraph.delNodes)
2370
- this.state.nodes.delete(nodeId);
2371
- for (const segId of newGraph.delSegs)
2372
- this.state.segs.delete(segId);
2373
- for (const segId of newGraph.dirtySegs) {
2374
- const seg = newGraph.getSeg(segId);
2375
- const edge = this.state.edges.get(seg.edgeIds.values().next().value);
2376
- const target = seg.targetNode(newGraph);
2377
- this.state.segs.set(seg.id, {
2378
- segId: seg.id,
2379
- edgeId: edge.id,
2380
- svg: seg.svg,
2381
- attrs: edge.attrs,
2382
- targetDummy: target.isDummy,
2383
- edgeData: edge.data
2384
- });
2385
- }
2386
- this.state = {
2387
- nodes: this.state.nodes.asImmutable(),
2388
- edges: this.state.edges.asImmutable(),
2389
- ports: this.state.ports.asImmutable(),
2390
- segs: this.state.segs.asImmutable(),
2391
- graph: newGraph,
2392
- update
2393
- };
3777
+ this.state = { graph: graph2, update };
3778
+ this.setNodePositions();
2394
3779
  this.seq.splice(this.index + 1);
2395
3780
  this.seq.push(this.state);
2396
3781
  this.nav("last");
3782
+ if (this.events.historyChange)
3783
+ this.events.historyChange(this.index, this.seq.length);
3784
+ }
3785
+ setNodePositions() {
3786
+ const { graph: graph2 } = this.state;
3787
+ for (const nodeId of graph2.dirtyNodes) {
3788
+ const node = graph2.getNode(nodeId);
3789
+ if (!node.isDummy)
3790
+ this.canvas.getNode(node.key).setPos(node.pos);
3791
+ }
2397
3792
  }
2398
3793
  async measureNodes(update) {
2399
- const nodes = update.updatedNodes.concat(update.addedNodes);
2400
- await this.canvas.measure(nodes);
3794
+ const data = [];
3795
+ for (const set of [update.updateNodes, update.addNodes])
3796
+ for (const node of set ?? [])
3797
+ data.push(this.parseNode(node, true));
3798
+ return await this.canvas.measureNodes(data);
3799
+ }
3800
+ parseNode(data, bumpVersion = false) {
3801
+ const get = this.options.props.node;
3802
+ let props;
3803
+ if (get) props = get(data);
3804
+ else if (!data) throw new Error(`invalid node ${data}`);
3805
+ else if (typeof data == "string") props = { id: data };
3806
+ else if (typeof data == "object") props = data;
3807
+ else throw new Error(`invalid node ${JSON.stringify(data)}`);
3808
+ this.detectNodeFields(data);
3809
+ const overrides = this.nodeOverrides.get(data);
3810
+ if (overrides) props = { ...props, ...overrides };
3811
+ let { id, title, text, type, render } = props;
3812
+ id ??= this.getNodeId(data);
3813
+ const ports = this.parsePorts(props.ports);
3814
+ let version = this.nodeVersions.get(data);
3815
+ if (!version) version = 1;
3816
+ else if (bumpVersion) version++;
3817
+ this.nodeVersions.set(data, version);
3818
+ return { id, data, ports, title, text, type, render, version };
3819
+ }
3820
+ detectNodeFields(data) {
3821
+ if (typeof data != "object" || !data) return;
3822
+ const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
3823
+ for (const [key, value] of Object.entries(data)) {
3824
+ if (skip.has(key)) continue;
3825
+ if (value === null || value === void 0) continue;
3826
+ const type = typeof value;
3827
+ if (type === "string" || type === "number" || type === "boolean") {
3828
+ this.nodeFields.set(key, type);
3829
+ }
3830
+ }
2401
3831
  }
2402
- getDims(node) {
2403
- return this.canvas.getDims(node);
3832
+ getNodeFields() {
3833
+ return this.nodeFields;
3834
+ }
3835
+ parseEdge(data) {
3836
+ const get = this.options.props.edge;
3837
+ let props;
3838
+ if (get) props = get(data);
3839
+ else if (!data) throw new Error(`invalid edge ${data}`);
3840
+ else if (typeof data == "string") props = this.parseStringEdge(data);
3841
+ else if (typeof data == "object") props = data;
3842
+ else throw new Error(`invalid edge ${data}`);
3843
+ const overrides = this.edgeOverrides.get(data);
3844
+ if (overrides) props = { ...props, ...overrides };
3845
+ let { id, source, target, type } = props;
3846
+ if (!id) id = this.getEdgeId(data);
3847
+ source = this.parseEdgeEnd(source);
3848
+ target = this.parseEdgeEnd(target);
3849
+ const edge = { id, source, target, type, data };
3850
+ return edge;
2404
3851
  }
2405
- _updateNode(node, mut) {
2406
- const props = this._options.nodeProps(node);
2407
- const oldData = this.state.nodes.get(props.id);
2408
- if (oldData === void 0)
2409
- throw new Error(`updating node ${props.id} which does not exist`);
2410
- const attrs = this._options.nodeStyle(node);
2411
- mut.updateNode({ id: props.id, dims: this.getDims(node) });
2412
- const data = { id: props.id, attrs, orig: node, node: props };
2413
- this.state.nodes.set(props.id, data);
3852
+ parseEdgeEnd(end) {
3853
+ if (!end) throw new Error(`edge has an undefined source or target`);
3854
+ if (typeof end == "string") return { id: end };
3855
+ if (typeof end == "object") {
3856
+ const keys = Object.keys(end);
3857
+ const pidx = keys.indexOf("port");
3858
+ if (pidx != -1) {
3859
+ if (end.port !== void 0 && typeof end.port != "string") return end;
3860
+ keys.splice(pidx, 1);
3861
+ }
3862
+ if (keys.length != 1) return end;
3863
+ if (keys[0] == "id") return end;
3864
+ if (keys[0] != "node") return end;
3865
+ const id = this.nodeIds.get(end.node);
3866
+ if (!id) throw new Error(`edge end references unknown node ${end.node}`);
3867
+ return { id, port: end.port };
3868
+ }
3869
+ throw new Error(`invalid edge end ${end}`);
2414
3870
  }
2415
- _updateEdge(edge, mut) {
2416
- const props = this._options.edgeProps(edge);
2417
- const id = Edge.id(props), str = Edge.str(props);
2418
- const oldData = this.state.edges.get(id);
2419
- if (oldData === void 0)
2420
- throw new Error(`updating edge ${str} which does not exist`);
2421
- const attrs = props.type ? this._options.edgeStyle(props.type) : void 0;
2422
- const data = { id, attrs, data: edge, edge: props };
2423
- this.state.edges.set(id, data);
2424
- if (props.type !== oldData.edge.type) {
2425
- mut.removeEdge(oldData.edge);
2426
- mut.addEdge(props);
2427
- } else {
3871
+ parseStringEdge(str) {
3872
+ const [source, target] = str.split(/\s*(?::|-+>?)\s*/);
3873
+ return { source, target };
3874
+ }
3875
+ parsePorts(ports) {
3876
+ const fixed = {};
3877
+ for (const key of ["in", "out"]) {
3878
+ if (ports?.[key] && ports[key].length > 0)
3879
+ fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
3880
+ }
3881
+ return fixed;
3882
+ }
3883
+ getNode(id) {
3884
+ return this.graph.getNode(id);
3885
+ }
3886
+ getEdge(id) {
3887
+ return this.graph.getEdge(id);
3888
+ }
3889
+ getNodeId(node) {
3890
+ let id = this.nodeIds.get(node);
3891
+ if (!id) {
3892
+ id = `n${this.nextNodeId++}`;
3893
+ this.nodeIds.set(node, id);
3894
+ }
3895
+ return id;
3896
+ }
3897
+ getEdgeId(edge) {
3898
+ let id = this.edgeIds.get(edge);
3899
+ if (!id) {
3900
+ id = `e${this.nextEdgeId++}`;
3901
+ this.edgeIds.set(edge, id);
2428
3902
  }
3903
+ return id;
2429
3904
  }
2430
3905
  _addNode(node, mut) {
2431
- const props = this._options.nodeProps(node);
2432
- if (this.state.nodes.has(props.id))
2433
- throw new Error(`node with id ${props.id} already exists`);
2434
- props.ports = { in: [], out: [], ...props.ports };
2435
- const attrs = this._options.nodeStyle(node);
2436
- const data = { id: props.id, attrs, data: node, node: props };
2437
- this.state.nodes.set(props.id, data);
2438
- console.log("adding node:", { ...props, dims: this.getDims(node) });
2439
- mut.addNode({ ...props, dims: this.getDims(node) });
3906
+ const { data, id: newId } = node.data;
3907
+ const oldId = this.nodeIds.get(data);
3908
+ if (oldId && oldId != newId)
3909
+ throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
3910
+ this.nodeIds.set(data, newId);
3911
+ mut.addNode(node.data);
2440
3912
  }
2441
3913
  _removeNode(node, mut) {
2442
- const props = this._options.nodeProps(node);
2443
- if (!this.state.nodes.has(props.id))
2444
- throw new Error(`removing node ${props.id} which does not exist`);
2445
- this.state.nodes.delete(props.id);
2446
- mut.removeNode(props);
3914
+ const id = this.nodeIds.get(node);
3915
+ if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
3916
+ mut.removeNode({ id });
3917
+ }
3918
+ _updateNode(node, mut) {
3919
+ const { data, id: newId } = node.data;
3920
+ const oldId = this.nodeIds.get(data);
3921
+ if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
3922
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
3923
+ mut.updateNode(node.data);
2447
3924
  }
2448
3925
  _addEdge(edge, mut) {
2449
- const props = this._options.edgeProps(edge);
2450
- const id = Edge.id(props), str = Edge.str(props);
2451
- if (this.state.edges.has(id))
2452
- throw new Error(`edge ${str} already exists`);
2453
- const attrs = props.type ? this._options.edgeStyle(props.type) : void 0;
2454
- const data = { id, attrs, data: edge, edge: props };
2455
- this.state.edges.set(id, data);
2456
- mut.addEdge(props);
3926
+ const data = this.parseEdge(edge);
3927
+ const id = this.edgeIds.get(edge);
3928
+ if (id && id != data.id)
3929
+ throw new Error(`edge id changed from ${id} to ${data.id} `);
3930
+ this.edgeIds.set(edge, data.id);
3931
+ mut.addEdge(data);
2457
3932
  }
2458
3933
  _removeEdge(edge, mut) {
2459
- const props = this._options.edgeProps(edge);
2460
- const id = Edge.id(props), str = Edge.str(props);
2461
- if (!this.state.edges.has(id))
2462
- throw new Error(`removing edge ${str} which does not exist`);
2463
- this.state.edges.delete(id);
2464
- mut.removeEdge(props);
2465
- }
2466
- _onOptionChange(prop) {
2467
- }
2468
- };
2469
- var Update = class {
2470
- addedNodes;
2471
- removedNodes;
2472
- updatedNodes;
2473
- addedEdges;
2474
- removedEdges;
2475
- updatedEdges;
2476
- desc;
2477
- constructor() {
2478
- this.addedNodes = [];
2479
- this.removedNodes = [];
2480
- this.updatedNodes = [];
2481
- this.addedEdges = [];
2482
- this.removedEdges = [];
2483
- this.updatedEdges = [];
3934
+ const id = this.edgeIds.get(edge);
3935
+ if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
3936
+ mut.removeEdge(this.parseEdge(edge));
2484
3937
  }
2485
- describe(desc) {
2486
- this.desc = desc;
3938
+ _updateEdge(edge, mut) {
3939
+ const id = this.edgeIds.get(edge);
3940
+ if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
3941
+ const data = this.parseEdge(edge);
3942
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
3943
+ mut.updateEdge(data);
3944
+ }
3945
+ // Event Handlers
3946
+ handleClickNode(id) {
3947
+ const handler = this.events.nodeClick;
3948
+ const node = this.graph.getNode(id);
3949
+ if (handler) handler(node.data);
3950
+ }
3951
+ handleClickEdge(id) {
3952
+ const handler = this.events.edgeClick;
3953
+ if (!handler) return;
3954
+ const seg = this.graph.getSeg(id);
3955
+ if (seg.edgeIds.size != 1) return;
3956
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
3957
+ handler(edge.data);
3958
+ }
3959
+ async handleNewNode() {
3960
+ const gotNode = async (node) => {
3961
+ await this.addNode(node);
3962
+ };
3963
+ if (this.events.newNode)
3964
+ this.events.newNode(gotNode);
3965
+ else
3966
+ this.canvas.showNewNodeModal(async (data) => {
3967
+ if (this.events.addNode)
3968
+ this.events.addNode(data, gotNode);
3969
+ else
3970
+ await gotNode(data);
3971
+ });
2487
3972
  }
2488
- addNode(node) {
2489
- this.addedNodes.push(node);
3973
+ async handleNewNodeFrom(source) {
3974
+ const gotNode = async (node) => {
3975
+ const gotEdge = async (edge) => {
3976
+ await this.update((u) => {
3977
+ u.addNode(node).addEdge(edge);
3978
+ });
3979
+ };
3980
+ const data = this.graph.getNode(source.id).data;
3981
+ const newEdge = {
3982
+ source: { node: data, port: source.port },
3983
+ target: { node }
3984
+ };
3985
+ if (this.events.addEdge)
3986
+ this.events.addEdge(newEdge, gotEdge);
3987
+ else
3988
+ await gotEdge(newEdge);
3989
+ };
3990
+ if (this.events.newNode)
3991
+ this.events.newNode(gotNode);
3992
+ else
3993
+ this.canvas.showNewNodeModal(async (data) => {
3994
+ if (this.events.addNode)
3995
+ this.events.addNode(data, gotNode);
3996
+ else
3997
+ await gotNode(data);
3998
+ });
2490
3999
  }
2491
- deleteNode(node) {
2492
- this.removedNodes.push(node);
4000
+ async handleEditNode(id) {
4001
+ const node = this.graph.getNode(id);
4002
+ const gotNode = async (node2) => {
4003
+ if (node2) await this.updateNode(node2);
4004
+ };
4005
+ if (this.events.editNode)
4006
+ this.events.editNode(node.data, gotNode);
4007
+ else {
4008
+ this.canvas.showEditNodeModal(node, async (data) => {
4009
+ if (this.events.updateNode)
4010
+ this.events.updateNode(node.data, data, gotNode);
4011
+ else {
4012
+ this.nodeOverrides.set(node.data, data);
4013
+ await gotNode(node.data);
4014
+ }
4015
+ });
4016
+ }
2493
4017
  }
2494
- updateNode(node) {
2495
- this.updatedNodes.push(node);
4018
+ async handleEditEdge(id) {
4019
+ const seg = this.graph.getSeg(id);
4020
+ if (seg.edgeIds.size != 1) return;
4021
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
4022
+ const gotEdge = async (edge2) => {
4023
+ if (edge2) await this.updateEdge(edge2);
4024
+ };
4025
+ if (this.events.editEdge)
4026
+ this.events.editEdge(edge.data, gotEdge);
4027
+ else
4028
+ this.canvas.showEditEdgeModal(edge, async (data) => {
4029
+ const sourceNode = edge.sourceNode(this.graph);
4030
+ const targetNode = edge.targetNode(this.graph);
4031
+ const update = {
4032
+ source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
4033
+ target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
4034
+ };
4035
+ if (this.events.updateEdge)
4036
+ this.events.updateEdge(edge.data, update, gotEdge);
4037
+ else {
4038
+ this.edgeOverrides.set(edge.data, {
4039
+ source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
4040
+ target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
4041
+ type: data.type
4042
+ });
4043
+ await gotEdge(edge.data);
4044
+ }
4045
+ });
2496
4046
  }
2497
- addEdge(edge) {
2498
- this.addedEdges.push(edge);
4047
+ async handleAddEdge(data) {
4048
+ const gotEdge = async (edge) => {
4049
+ if (edge) await this.addEdge(edge);
4050
+ };
4051
+ const newEdge = {
4052
+ source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
4053
+ target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
4054
+ };
4055
+ if (this.events.addEdge)
4056
+ this.events.addEdge(newEdge, gotEdge);
4057
+ else
4058
+ await gotEdge(data);
2499
4059
  }
2500
- deleteEdge(edge) {
2501
- this.removedEdges.push(edge);
4060
+ async handleDeleteNode(id) {
4061
+ const node = this.getNode(id);
4062
+ if (this.events.removeNode)
4063
+ this.events.removeNode(node.data, async (remove) => {
4064
+ if (remove) await this.deleteNode(node.data);
4065
+ });
4066
+ else
4067
+ await this.deleteNode(node.data);
2502
4068
  }
2503
- updateEdge(edge) {
2504
- this.updatedEdges.push(edge);
4069
+ async handleDeleteEdge(id) {
4070
+ const edge = this.getEdge(id);
4071
+ if (this.events.removeEdge)
4072
+ this.events.removeEdge(edge.data, async (remove) => {
4073
+ if (remove) await this.deleteEdge(edge.data);
4074
+ });
4075
+ else
4076
+ await this.deleteEdge(edge.data);
2505
4077
  }
2506
4078
  };
2507
- async function graph(args = {}) {
2508
- const { nodes = [], edges = [], ...options } = args;
2509
- const api = new API(options);
2510
- if (nodes.length > 0 || edges.length > 0) {
2511
- await api.update((update) => {
2512
- for (const node of nodes)
2513
- update.addNode(node);
2514
- for (const edge of edges)
2515
- update.addEdge(edge);
2516
- });
2517
- }
4079
+
4080
+ // src/index.ts
4081
+ async function graph(args = { root: "app" }) {
4082
+ const api = new API(args);
4083
+ await api.init();
2518
4084
  return api;
2519
4085
  }
2520
4086
  var index_default = graph;