@3plate/graph-core 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,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,23 @@ __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
+ var defNodeData = {
40
44
  id: "",
45
+ data: void 0,
46
+ version: 0,
47
+ title: void 0,
48
+ text: void 0,
49
+ type: void 0,
50
+ render: void 0,
51
+ ports: { in: null, out: null },
41
52
  aligned: {},
42
53
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
43
54
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
44
- ports: { in: [], out: [] },
45
55
  layerId: "",
46
56
  isDummy: false,
47
57
  isMerged: false,
@@ -52,29 +62,35 @@ var defaultNodeProps = {
52
62
  dims: void 0,
53
63
  mutable: false
54
64
  };
55
- var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
65
+ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
56
66
  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");
67
+ // get edgeId(): EdgeId {
68
+ // if (!this.isDummy)
69
+ // throw new Error(`node ${this.id} is not a dummy`)
70
+ // if (this.isMerged)
71
+ // throw new Error(`node ${this.id} is merged`)
72
+ // return this.get('edgeIds')[0]
73
+ // }
74
+ // get edgeIds(): EdgeId[] {
75
+ // if (!this.isDummy)
76
+ // throw new Error(`node ${this.id} is not a dummy`)
77
+ // if (!this.isMerged)
78
+ // throw new Error(`node ${this.id} is not merged`)
79
+ // return this.get('edgeIds')
80
+ // }
81
+ get key() {
82
+ return this.isDummy ? this.id : _Node.key(this);
83
+ }
84
+ static key(node) {
85
+ return `${node.id}:${node.version}`;
70
86
  }
71
87
  static isDummyId(nodeId) {
72
88
  return nodeId.startsWith(_Node.dummyPrefix);
73
89
  }
74
- static addNormal(g, props) {
90
+ static addNormal(g, data) {
75
91
  const layer = g.layerAt(0);
76
92
  const node = new _Node({
77
- ...props,
93
+ ...data,
78
94
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
79
95
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
80
96
  aligned: {},
@@ -86,10 +102,10 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
86
102
  g.dirtyNodes.add(node.id);
87
103
  return node;
88
104
  }
89
- static addDummy(g, props) {
90
- const layer = g.getLayer(props.layerId);
105
+ static addDummy(g, data) {
106
+ const layer = g.getLayer(data.layerId);
91
107
  const node = new _Node({
92
- ...props,
108
+ ...data,
93
109
  id: `${_Node.dummyPrefix}${g.nextDummyId++}`,
94
110
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
95
111
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
@@ -105,6 +121,12 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
105
121
  g.dirtyNodes.add(node.id);
106
122
  return node;
107
123
  }
124
+ static del(g, node) {
125
+ return g.getNode(node.id).delSelf(g);
126
+ }
127
+ static update(g, data) {
128
+ return g.getNode(data.id).mut(g).merge(data);
129
+ }
108
130
  mut(g) {
109
131
  if (this.mutable) return this;
110
132
  return g.mutateNode(this);
@@ -127,6 +149,9 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
127
149
  isUnlinked() {
128
150
  return this.edges.in.size == 0 && this.edges.out.size == 0 && this.segs.in.size == 0 && this.segs.out.size == 0;
129
151
  }
152
+ hasPorts() {
153
+ return !!this.ports?.in?.length || !!this.ports?.out?.length;
154
+ }
130
155
  layerIndex(g) {
131
156
  return this.getLayer(g).index;
132
157
  }
@@ -135,11 +160,9 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
135
160
  }
136
161
  setIndex(g, index) {
137
162
  if (this.index == index) return this;
138
- console.log(`set index of ${this.id} to ${index}`);
139
163
  return this.mut(g).set("index", index);
140
164
  }
141
165
  setLayerPos(g, lpos) {
142
- console.log("setLayerPos", this.id, lpos);
143
166
  if (this.lpos == lpos) return this;
144
167
  return this.mut(g).set("lpos", lpos);
145
168
  }
@@ -153,7 +176,6 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
153
176
  return this;
154
177
  }
155
178
  moveToLayer(g, layer) {
156
- console.log("moveToLayer", this, this.getLayer(g), layer);
157
179
  this.getLayer(g).delNode(g, this.id);
158
180
  layer.addNode(g, this.id);
159
181
  return this.setLayer(g, layer.id);
@@ -275,16 +297,44 @@ var Node = class _Node extends (0, import_immutable.Record)(defaultNodeProps) {
275
297
  }
276
298
  };
277
299
 
278
- // src/graph/types/edge.ts
300
+ // src/graph/edge.ts
279
301
  var import_immutable2 = require("immutable");
280
- var defaultEdgeProps = {
302
+
303
+ // src/log.ts
304
+ var levels = {
305
+ error: 0,
306
+ warn: 1,
307
+ info: 2,
308
+ debug: 3
309
+ };
310
+ var currentLevel = "debug";
311
+ function shouldLog(level) {
312
+ return levels[level] <= levels[currentLevel];
313
+ }
314
+ function logger(module2) {
315
+ return {
316
+ error: (msg, ...args) => shouldLog("error") && console.error(`[${module2}] ${msg}`, ...args),
317
+ warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module2}] ${msg}`, ...args),
318
+ info: (msg, ...args) => shouldLog("info") && console.info(`[${module2}] ${msg}`, ...args),
319
+ debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module2}] ${msg}`, ...args)
320
+ };
321
+ }
322
+ var log = logger("core");
323
+
324
+ // src/graph/edge.ts
325
+ var log2 = logger("edge");
326
+ var defEdgeData = {
281
327
  id: "",
328
+ data: null,
329
+ label: void 0,
282
330
  source: { id: "" },
283
331
  target: { id: "" },
332
+ type: void 0,
333
+ style: void 0,
284
334
  mutable: false,
285
335
  segIds: []
286
336
  };
287
- var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
337
+ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
288
338
  static prefix = "e:";
289
339
  mut(g) {
290
340
  if (this.mutable) return this;
@@ -342,37 +392,44 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
342
392
  return _Edge.str(this);
343
393
  }
344
394
  static str(edge) {
345
- let source = edge.source.id;
346
- if (edge.source.port)
395
+ let source = edge.source?.id;
396
+ if (!source) throw new Error("edge source is undefined");
397
+ if (edge.source?.port)
347
398
  source = `${source} (port ${edge.source.port})`;
348
- let target = edge.target.id;
349
- if (edge.target.port)
399
+ let target = edge.target?.id;
400
+ if (!target) throw new Error("edge target is undefined");
401
+ if (edge.target?.port)
350
402
  target = `${target} (port ${edge.target.port})`;
351
403
  let str = `edge from ${source} to ${target}`;
352
404
  if (edge.type) str += ` of type ${edge.type}`;
353
405
  return str;
354
406
  }
355
- static id(edge, prefix = _Edge.prefix, side = "both") {
407
+ static key(edge, prefix = _Edge.prefix, side = "both") {
356
408
  let source = "", target = "";
357
409
  if (side == "source" || side == "both") {
410
+ if (!edge.source?.id) throw new Error("edge source is undefined");
358
411
  source = edge.source.id;
359
- if (edge.source.port)
412
+ if (edge.source?.port)
360
413
  source = `${source}.${edge.source.port}`;
414
+ const marker = edge.source?.marker ?? edge.style?.marker?.source;
415
+ if (marker && marker != "none") source += `[${marker}]`;
361
416
  source += "-";
362
417
  }
363
418
  if (side == "target" || side == "both") {
419
+ if (!edge.target?.id) throw new Error("edge target is undefined");
364
420
  target = edge.target.id;
365
421
  if (edge.target.port)
366
422
  target = `${target}.${edge.target.port}`;
367
423
  target = "-" + target;
424
+ const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
425
+ if (marker && marker != "none") target += `[${marker}]`;
368
426
  }
369
427
  const type = edge.type || "";
370
428
  return `${prefix}${source}${type}${target}`;
371
429
  }
372
- static add(g, props) {
430
+ static add(g, data) {
373
431
  const edge = new _Edge({
374
- ...props,
375
- id: _Edge.id(props),
432
+ ...data,
376
433
  segIds: []
377
434
  });
378
435
  edge.link(g);
@@ -380,18 +437,39 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defaultEdgeProps) {
380
437
  g.dirtyEdges.add(edge.id);
381
438
  return edge;
382
439
  }
440
+ static del(g, data) {
441
+ return g.getEdge(data.id).delSelf(g);
442
+ }
443
+ static update(g, data) {
444
+ let edge = g.getEdge(data.id);
445
+ let relink = false;
446
+ if (data.source.id !== edge.source.id || data.target.id !== edge.target.id || data.source.port !== edge.source.port || data.target.port !== edge.target.port || data.type !== edge.type) {
447
+ for (const seg of edge.segs(g))
448
+ seg.delEdgeId(g, edge.id);
449
+ edge.unlink(g);
450
+ relink = true;
451
+ }
452
+ edge = edge.mut(g).merge(data);
453
+ if (relink)
454
+ edge.link(g);
455
+ return edge;
456
+ }
383
457
  };
384
458
 
385
- // src/graph/types/seg.ts
459
+ // src/graph/seg.ts
386
460
  var import_immutable3 = require("immutable");
387
- var defaultSegProps = {
461
+ var defSegData = {
388
462
  id: "",
389
463
  source: { id: "" },
390
464
  target: { id: "" },
465
+ type: void 0,
466
+ style: void 0,
391
467
  edgeIds: (0, import_immutable3.Set)(),
468
+ trackPos: void 0,
469
+ svg: void 0,
392
470
  mutable: false
393
471
  };
394
- var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
472
+ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
395
473
  static prefix = "s:";
396
474
  mut(g) {
397
475
  if (this.mutable) return this;
@@ -416,7 +494,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
416
494
  sameEnd(other, side) {
417
495
  const mine = this[side];
418
496
  const yours = other[side];
419
- return mine.id === yours.id && mine.port === yours.port;
497
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
420
498
  }
421
499
  setPos(g, source, target) {
422
500
  return this.mut(g).merge({
@@ -474,10 +552,10 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
474
552
  }
475
553
  return this.mut(g).set("edgeIds", this.edgeIds.asMutable().remove(edgeId));
476
554
  }
477
- static add(g, props) {
555
+ static add(g, data) {
478
556
  const seg = new _Seg({
479
- ...props,
480
- id: Edge.id(props, _Seg.prefix)
557
+ ...data,
558
+ id: Edge.key(data, _Seg.prefix)
481
559
  });
482
560
  seg.link(g);
483
561
  g.segs.set(seg.id, seg);
@@ -486,33 +564,10 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defaultSegProps) {
486
564
  }
487
565
  };
488
566
 
489
- // src/graph/types/layer.ts
567
+ // src/graph/layer.ts
490
568
  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 = {
569
+ var log3 = logger("layer");
570
+ var defLayerData = {
516
571
  id: "",
517
572
  index: 0,
518
573
  nodeIds: (0, import_immutable4.Set)(),
@@ -523,7 +578,7 @@ var defaultLayerProps = {
523
578
  isSorted: false,
524
579
  mutable: false
525
580
  };
526
- var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
581
+ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
527
582
  static prefix = "l:";
528
583
  mut(g) {
529
584
  if (this.mutable) return this;
@@ -605,7 +660,6 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
605
660
  }
606
661
  setSorted(g, nodeIds) {
607
662
  if (this.hasSortOrder(nodeIds)) return this;
608
- console.log(`setting sorted for layer ${this.id}`);
609
663
  nodeIds.forEach((nodeId, i) => g.getNode(nodeId).setIndex(g, i));
610
664
  return this.mut(g).merge({ sorted: nodeIds, isSorted: true });
611
665
  }
@@ -615,7 +669,7 @@ var Layer = class extends (0, import_immutable4.Record)(defaultLayerProps) {
615
669
  }
616
670
  };
617
671
 
618
- // src/graph/types/mutator.ts
672
+ // src/graph/mutator.ts
619
673
  var Mutator = class {
620
674
  changes;
621
675
  constructor() {
@@ -624,20 +678,27 @@ var Mutator = class {
624
678
  removedNodes: [],
625
679
  updatedNodes: [],
626
680
  addedEdges: [],
627
- removedEdges: []
681
+ removedEdges: [],
682
+ updatedEdges: []
628
683
  };
629
684
  }
685
+ describe(description) {
686
+ this.changes.description = description;
687
+ }
630
688
  addNode(node) {
631
689
  this.changes.addedNodes.push(node);
632
690
  }
633
691
  addNodes(...nodes) {
634
692
  nodes.forEach((node) => this.addNode(node));
635
693
  }
694
+ removeNode(node) {
695
+ this.changes.removedNodes.push(node);
696
+ }
697
+ removeNodes(...nodes) {
698
+ nodes.forEach((node) => this.removeNode(node));
699
+ }
636
700
  updateNode(node) {
637
- if (typeof node === "string")
638
- this.changes.updatedNodes.push({ id: node });
639
- else
640
- this.changes.updatedNodes.push(node);
701
+ this.changes.updatedNodes.push(node);
641
702
  }
642
703
  updateNodes(...nodes) {
643
704
  nodes.forEach((node) => this.updateNode(node));
@@ -648,21 +709,18 @@ var Mutator = class {
648
709
  addEdges(...edges) {
649
710
  edges.forEach((edge) => this.addEdge(edge));
650
711
  }
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
712
  removeEdge(edge) {
661
713
  this.changes.removedEdges.push(edge);
662
714
  }
663
715
  removeEdges(...edges) {
664
716
  edges.forEach((edge) => this.removeEdge(edge));
665
717
  }
718
+ updateEdge(edge) {
719
+ this.changes.updatedEdges.push(edge);
720
+ }
721
+ updateEdges(...edges) {
722
+ edges.forEach((edge) => this.updateEdge(edge));
723
+ }
666
724
  };
667
725
 
668
726
  // src/graph/services/cycles.ts
@@ -762,14 +820,12 @@ var Cycles = class _Cycles {
762
820
 
763
821
  // src/graph/services/dummy.ts
764
822
  var import_immutable5 = require("immutable");
765
- var log3 = logger("dummy");
823
+ var log4 = logger("dummy");
766
824
  var Dummy = class _Dummy {
767
825
  static updateDummies(g) {
768
- log3.debug(`updating dummies:`, [...g.dirtyEdges]);
769
826
  for (const edgeId of g.dirtyEdges) {
770
- log3.debug(`updating dummies of edge ${edgeId}`);
771
827
  const edge = g.getEdge(edgeId);
772
- const { type } = edge;
828
+ const { type, style } = edge;
773
829
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
774
830
  const targetLayer = edge.targetNode(g).layerIndex(g);
775
831
  let segIndex = 0;
@@ -793,12 +849,10 @@ var Dummy = class _Dummy {
793
849
  });
794
850
  target = { id: dummy.id };
795
851
  }
796
- 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}`);
852
+ seg = Seg.add(g, { source, target, type, style, edgeIds: (0, import_immutable5.Set)([edgeId]) });
798
853
  segs.splice(segIndex, 0, seg.id);
799
854
  changed = true;
800
855
  } else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
801
- log3.debug(`edge ${edgeId}: removing segment ${seg.id} from layer ${layerIndex - 1} to layer ${layerIndex}`);
802
856
  seg = seg.delEdgeId(g, edgeId);
803
857
  segs.splice(segIndex, 1);
804
858
  changed = true;
@@ -810,14 +864,12 @@ var Dummy = class _Dummy {
810
864
  }
811
865
  }
812
866
  while (segIndex < segs.length) {
813
- log3.debug(`edge ${edgeId}: removing trailing segment ${segs[segIndex]}`);
814
867
  g.getSeg(segs[segIndex]).delEdgeId(g, edgeId);
815
868
  segs.splice(segIndex, 1);
816
869
  changed = true;
817
870
  segIndex++;
818
871
  }
819
872
  if (changed) {
820
- log3.debug(`edge ${edgeId}: updated segments to ${segs.join(", ")}`);
821
873
  edge.setSegIds(g, segs);
822
874
  }
823
875
  }
@@ -832,7 +884,6 @@ var Dummy = class _Dummy {
832
884
  const dir = side == "source" ? "in" : "out";
833
885
  const altSide = side == "source" ? "target" : "source";
834
886
  const altDir = altSide == "source" ? "in" : "out";
835
- log3.debug(`merging dummies by ${side}`);
836
887
  for (const layerId of layerIds) {
837
888
  let layer = g.getLayer(layerId);
838
889
  const groups = /* @__PURE__ */ new Map();
@@ -841,7 +892,7 @@ var Dummy = class _Dummy {
841
892
  const node = g.getNode(nodeId);
842
893
  if (node.isMerged) continue;
843
894
  const edge = g.getEdge(node.edgeIds[0]);
844
- const key = Edge.id(edge, "k:", side);
895
+ const key = Edge.key(edge, "k:", side);
845
896
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
846
897
  groups.get(key).add(node);
847
898
  }
@@ -883,7 +934,7 @@ var Dummy = class _Dummy {
883
934
 
884
935
  // src/graph/services/layers.ts
885
936
  var import_immutable6 = require("immutable");
886
- var log4 = logger("layers");
937
+ var log5 = logger("layers");
887
938
  var Layers = class {
888
939
  static updateLayers(g) {
889
940
  const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
@@ -946,13 +997,11 @@ var Layers = class {
946
997
 
947
998
  // src/graph/services/layout.ts
948
999
  var import_immutable7 = require("immutable");
949
- var log5 = logger("layout");
1000
+ var log6 = logger("layout");
950
1001
  var Layout = class _Layout {
951
1002
  static parentIndex(g, node) {
952
1003
  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
1004
  const pidx = parents.map((p) => p.index).min();
955
- log5.debug(`node ${node.id}: parent index ${pidx}`);
956
1005
  if (pidx !== void 0) return pidx;
957
1006
  return node.isDummy ? -Infinity : Infinity;
958
1007
  }
@@ -970,7 +1019,6 @@ var Layout = class _Layout {
970
1019
  return minA.localeCompare(minB);
971
1020
  }
972
1021
  static positionNodes(g) {
973
- console.log("positionNodes", g.dirtyNodes);
974
1022
  for (const nodeId of g.dirtyNodes)
975
1023
  g.dirtyLayers.add(g.getNode(nodeId).layerId);
976
1024
  let adjustNext = false;
@@ -978,15 +1026,12 @@ var Layout = class _Layout {
978
1026
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
979
1027
  adjustNext = false;
980
1028
  let layer = g.getLayer(layerId);
981
- console.log(`positioning layer ${layerId} at ${layer.index}`);
982
1029
  const pidxs = /* @__PURE__ */ new Map();
983
1030
  for (const nodeId of layer.nodeIds)
984
1031
  pidxs.set(nodeId, _Layout.parentIndex(g, g.getNode(nodeId)));
985
- console.log("pidxs", pidxs);
986
1032
  const sorted = [...layer.nodeIds].sort(
987
1033
  (aId, bId) => _Layout.compareNodes(g, aId, bId, pidxs)
988
1034
  );
989
- console.log(`sorted:`, sorted);
990
1035
  if (layer.hasSortOrder(sorted)) continue;
991
1036
  g.dirtyLayers.add(layerId);
992
1037
  layer = layer.setSorted(g, sorted);
@@ -994,7 +1039,6 @@ var Layout = class _Layout {
994
1039
  let lpos = 0;
995
1040
  for (let i = 0; i < sorted.length; i++) {
996
1041
  let node = g.getNode(sorted[i]);
997
- log5.debug(`node ${node.id}: final index ${i}`);
998
1042
  node = node.setIndex(g, i).setLayerPos(g, lpos);
999
1043
  const size = node.dims?.[g.w] ?? 0;
1000
1044
  lpos += size + g.options.nodeMargin;
@@ -1026,7 +1070,12 @@ var Layout = class _Layout {
1026
1070
  for (const layerId of layerIds) {
1027
1071
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
1028
1072
  adjustNext = false;
1073
+ let iterations = 0;
1029
1074
  while (true) {
1075
+ if (++iterations > 10) {
1076
+ log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1077
+ break;
1078
+ }
1030
1079
  let changed = false;
1031
1080
  const nodeIds = _Layout.sortLayer(g, layerId, reverseNodes);
1032
1081
  for (const nodeId of nodeIds) {
@@ -1092,17 +1141,45 @@ var Layout = class _Layout {
1092
1141
  [g.x]: p[g.x] + w / 2,
1093
1142
  [g.y]: p[g.y] + h / 2
1094
1143
  };
1095
- p[g.x] += _Layout.nodePortOffset(g, nodeId, seg[side].port);
1096
- if (side == "source" == g.r)
1144
+ p[g.x] += _Layout.nodePortOffset(g, nodeId, seg, side);
1145
+ if (side == "target" == g.r)
1097
1146
  p[g.y] += h;
1098
1147
  return p;
1099
1148
  }
1100
- static nodePortOffset(g, nodeId, port) {
1101
- if (!port) return g.options.defaultPortOffset;
1102
- return g.options.defaultPortOffset;
1149
+ static nodePortOffset(g, nodeId, seg, side) {
1150
+ const node = g.getNode(nodeId);
1151
+ const dir = side == "source" ? "out" : "in";
1152
+ const portId = seg[side].port;
1153
+ let min = 0, size = node.dims?.[g.w] ?? 0;
1154
+ if (portId) {
1155
+ const ports = node.ports?.[dir];
1156
+ const port = ports?.find((p) => p.id === portId);
1157
+ if (port?.offset !== void 0) {
1158
+ min = port.offset;
1159
+ size = port.size ?? 0;
1160
+ }
1161
+ }
1162
+ const alt = side == "source" ? "target" : "source";
1163
+ let segs = [];
1164
+ const keyOf = (seg2) => `${seg2.type ?? ""}:${seg2[side].marker ?? ""}`;
1165
+ for (const segId of node.segs[dir])
1166
+ segs.push(g.getSeg(segId));
1167
+ if (portId) segs = segs.filter((s) => s[side].port == portId);
1168
+ const groups = Object.groupBy(segs, (s) => keyOf(s));
1169
+ const posMap = /* @__PURE__ */ new Map();
1170
+ for (const [key, segs2] of Object.entries(groups)) {
1171
+ let pos = Infinity;
1172
+ for (const seg2 of segs2) pos = Math.min(pos, seg2.node(g, alt).lpos);
1173
+ posMap.set(key, pos);
1174
+ }
1175
+ const keys = [...posMap.keys()].sort((a, b) => posMap.get(a) - posMap.get(b));
1176
+ const gap = size / (keys.length + 1);
1177
+ const index = keys.indexOf(keyOf(seg));
1178
+ return min + (index + 1) * gap;
1103
1179
  }
1104
1180
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1105
1181
  const node = g.getNode(nodeId);
1182
+ log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1106
1183
  if (!conservative)
1107
1184
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1108
1185
  const space = g.options.nodeMargin;
@@ -1133,7 +1210,7 @@ var Layout = class _Layout {
1133
1210
  g.getNode(node.aligned[dir]).setAligned(g, alt, void 0);
1134
1211
  if (otherId)
1135
1212
  g.getNode(otherId).setAligned(g, alt, nodeId);
1136
- node.setAligned(g, dir, otherId);
1213
+ node.setAligned(g, dir, otherId).setLayerPos(g, lpos);
1137
1214
  }
1138
1215
  static *aligned(g, nodeId, dir) {
1139
1216
  const visit = function* (node2, dir2) {
@@ -1193,39 +1270,141 @@ var Layout = class _Layout {
1193
1270
  let pos = 0;
1194
1271
  const dir = g.r ? -1 : 1;
1195
1272
  const trackSep = Math.max(
1196
- g.options.layerMargin,
1197
1273
  g.options.edgeSpacing,
1198
1274
  g.options.turnRadius
1199
1275
  );
1276
+ const marginSep = Math.max(
1277
+ g.options.edgeSpacing,
1278
+ g.options.layerMargin,
1279
+ g.options.turnRadius + g.options.markerSize
1280
+ );
1200
1281
  for (const layerId of g.layerList) {
1201
1282
  let layer = g.getLayer(layerId);
1202
1283
  let height;
1203
- console.log(`getCoords: layer = ${layerId} at ${layer.index}`);
1204
1284
  if (g.dirtyLayers.has(layerId)) {
1205
1285
  height = (0, import_immutable7.Seq)(layer.nodes(g)).map((node) => node.dims?.[g.h] ?? 0).max() ?? 0;
1206
1286
  layer = layer.setSize(g, height);
1207
1287
  } else height = layer.size;
1208
- console.log(`getCoords: layer = ${layerId}: pos = ${pos}, height = ${height}`);
1209
1288
  for (const node of layer.nodes(g)) {
1210
1289
  if (!g.dirtyNodes.has(node.id) && pos == layer.pos) continue;
1211
1290
  const npos = { [g.x]: node.lpos, [g.y]: pos };
1212
1291
  if (!g.n) npos[g.y] += dir * height;
1213
1292
  if (g.r == g.n) npos[g.y] -= node.dims?.[g.h] ?? 0;
1214
- console.log(`getCoords: node = ${node.id}: pos:`, npos);
1215
1293
  node.setPos(g, npos);
1216
1294
  }
1217
1295
  layer = layer.setPos(g, pos);
1218
- pos += dir * (height + trackSep);
1296
+ pos += dir * (height + marginSep);
1219
1297
  for (const track of layer.tracks) {
1220
1298
  for (const segId of track)
1221
1299
  g.getSeg(segId).setTrackPos(g, pos);
1222
- pos += dir * g.options.edgeSpacing;
1300
+ pos += dir * trackSep;
1223
1301
  }
1302
+ pos += dir * (marginSep - trackSep);
1224
1303
  }
1225
1304
  }
1226
1305
  };
1227
1306
 
1307
+ // src/canvas/marker.tsx
1308
+ var import_marker = __toESM(require("./marker.css?raw"), 1);
1309
+ var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1310
+ function arrow(size, classPrefix, reverse = false) {
1311
+ const h = size / 1.5;
1312
+ const w = size;
1313
+ const ry = h / 2;
1314
+ const suffix = reverse ? "-reverse" : "";
1315
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1316
+ "marker",
1317
+ {
1318
+ id: `g3p-marker-arrow${suffix}`,
1319
+ className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1320
+ markerWidth: size,
1321
+ markerHeight: size,
1322
+ refX: "2",
1323
+ refY: ry,
1324
+ orient: reverse ? "auto-start-reverse" : "auto",
1325
+ markerUnits: "userSpaceOnUse",
1326
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M0,0 L0,${h} L${w},${ry} z` })
1327
+ }
1328
+ );
1329
+ }
1330
+ function circle(size, classPrefix, reverse = false) {
1331
+ const r = size / 3;
1332
+ const cy = size / 2;
1333
+ const suffix = reverse ? "-reverse" : "";
1334
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1335
+ "marker",
1336
+ {
1337
+ id: `g3p-marker-circle${suffix}`,
1338
+ className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1339
+ markerWidth: size,
1340
+ markerHeight: size,
1341
+ refX: "2",
1342
+ refY: cy,
1343
+ orient: reverse ? "auto-start-reverse" : "auto",
1344
+ markerUnits: "userSpaceOnUse",
1345
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: r + 2, cy, r })
1346
+ }
1347
+ );
1348
+ }
1349
+ function diamond(size, classPrefix, reverse = false) {
1350
+ const w = size * 0.7;
1351
+ const h = size / 2;
1352
+ const cy = size / 2;
1353
+ const suffix = reverse ? "-reverse" : "";
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ "marker",
1356
+ {
1357
+ id: `g3p-marker-diamond${suffix}`,
1358
+ className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1359
+ markerWidth: size,
1360
+ markerHeight: size,
1361
+ refX: "2",
1362
+ refY: cy,
1363
+ orient: reverse ? "auto-start-reverse" : "auto",
1364
+ markerUnits: "userSpaceOnUse",
1365
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `M2,${cy} L${2 + w / 2},${cy - h / 2} L${2 + w},${cy} L${2 + w / 2},${cy + h / 2} z` })
1366
+ }
1367
+ );
1368
+ }
1369
+ function bar(size, classPrefix, reverse = false) {
1370
+ const h = size * 0.6;
1371
+ const cy = size / 2;
1372
+ const suffix = reverse ? "-reverse" : "";
1373
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1374
+ "marker",
1375
+ {
1376
+ id: `g3p-marker-bar${suffix}`,
1377
+ className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1378
+ markerWidth: size,
1379
+ markerHeight: size,
1380
+ refX: "2",
1381
+ refY: cy,
1382
+ orient: reverse ? "auto-start-reverse" : "auto",
1383
+ markerUnits: "userSpaceOnUse",
1384
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: cy - h / 2, x2: "2", y2: cy + h / 2, "stroke-width": "2" })
1385
+ }
1386
+ );
1387
+ }
1388
+ function none(size, classPrefix, reverse = false) {
1389
+ return void 0;
1390
+ }
1391
+ function normalize(data) {
1392
+ let source = data.source?.marker ?? data.style?.marker?.source;
1393
+ let target = data.target?.marker ?? data.style?.marker?.target ?? "arrow";
1394
+ if (source == "none") source = void 0;
1395
+ if (target == "none") target = void 0;
1396
+ return { source, target };
1397
+ }
1398
+ var markerDefs = {
1399
+ arrow,
1400
+ circle,
1401
+ diamond,
1402
+ bar,
1403
+ none
1404
+ };
1405
+
1228
1406
  // src/graph/services/lines.ts
1407
+ var log7 = logger("lines");
1229
1408
  var Lines = class _Lines {
1230
1409
  static layoutSeg(g, seg) {
1231
1410
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1300,7 +1479,12 @@ var Lines = class _Lines {
1300
1479
  const radius = g.options.turnRadius;
1301
1480
  const p1 = Layout.anchorPos(g, seg, "source");
1302
1481
  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);
1482
+ const source = seg.sourceNode(g);
1483
+ const target = seg.targetNode(g);
1484
+ const marker = normalize(seg);
1485
+ if (source.isDummy) marker.source = void 0;
1486
+ if (target.isDummy) marker.target = void 0;
1487
+ const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius, marker) : _Lines.createDirectPath(g, p1, p2, radius, marker);
1304
1488
  const svg = _Lines.pathToSVG(path);
1305
1489
  seg.setSVG(g, svg);
1306
1490
  }
@@ -1326,7 +1510,7 @@ var Lines = class _Lines {
1326
1510
  }
1327
1511
  return line;
1328
1512
  }
1329
- static pathBuilder(g, start, end, trackPos, radius) {
1513
+ static pathBuilder(g, start, end, trackPos, radius, marker) {
1330
1514
  const { x, y } = g;
1331
1515
  const lr = end[x] > start[x];
1332
1516
  const d = lr ? 1 : -1;
@@ -1338,6 +1522,8 @@ var Lines = class _Lines {
1338
1522
  if (g.r) s = 1 - s;
1339
1523
  if (!lr) s = 1 - s;
1340
1524
  if (!g.v) s = 1 - s;
1525
+ if (marker.source) start[y] += o * (g.options.markerSize - 1);
1526
+ if (marker.target) end[y] -= o * (g.options.markerSize - 1);
1341
1527
  const p = { ...start, s };
1342
1528
  const path = [];
1343
1529
  const advance = (p2, type) => {
@@ -1346,8 +1532,8 @@ var Lines = class _Lines {
1346
1532
  return { x, y, lr, d, o, rd, ro, t, s, p, path, advance };
1347
1533
  }
1348
1534
  // 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);
1535
+ static createRailroadPath(g, start, end, trackPos, radius, marker) {
1536
+ const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius, marker);
1351
1537
  advance({ [y]: t - ro }, "line");
1352
1538
  advance({ [x]: p[x] + rd, [y]: t }, "arc");
1353
1539
  advance({ [x]: end[x] - rd }, "line");
@@ -1356,8 +1542,8 @@ var Lines = class _Lines {
1356
1542
  return path;
1357
1543
  }
1358
1544
  // 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);
1545
+ static createDirectPath(g, start, end, radius, marker) {
1546
+ const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius, marker);
1361
1547
  const dx = Math.abs(end.x - start.x);
1362
1548
  const dy = Math.abs(end.y - start.y);
1363
1549
  const d_ = { x: dx, y: dy };
@@ -1448,21 +1634,14 @@ var Lines = class _Lines {
1448
1634
  }
1449
1635
  };
1450
1636
 
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
1637
+ // src/graph/graph.ts
1638
+ var emptyChanges = {
1639
+ addedNodes: [],
1640
+ removedNodes: [],
1641
+ updatedNodes: [],
1642
+ addedEdges: [],
1643
+ removedEdges: [],
1644
+ updatedEdges: []
1466
1645
  };
1467
1646
  var Graph = class _Graph {
1468
1647
  prior;
@@ -1491,30 +1670,10 @@ var Graph = class _Graph {
1491
1670
  x;
1492
1671
  y;
1493
1672
  d;
1494
- constructor({ prior, changes, options, nodes, edges } = {}) {
1673
+ constructor({ prior, changes, options }) {
1674
+ this.options = prior?.options ?? options;
1675
+ this.changes = changes ?? emptyChanges;
1495
1676
  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
1677
  this.r = this.options.orientation === "BT" || this.options.orientation === "RL";
1519
1678
  this.v = this.options.orientation === "TB" || this.options.orientation === "BT";
1520
1679
  this.h = this.v ? "h" : "w";
@@ -1530,38 +1689,41 @@ var Graph = class _Graph {
1530
1689
  this.n = true;
1531
1690
  else
1532
1691
  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
- }
1692
+ if (this.dirty) this.processUpdate();
1693
+ }
1694
+ processUpdate() {
1695
+ try {
1696
+ this.beginMutate();
1697
+ this.applyChanges();
1698
+ Cycles.checkCycles(this);
1699
+ Layers.updateLayers(this);
1700
+ Dummy.updateDummies(this);
1701
+ Dummy.mergeDummies(this);
1702
+ Layout.positionNodes(this);
1703
+ Layout.alignAll(this);
1704
+ Lines.trackEdges(this);
1705
+ Layout.getCoords(this);
1706
+ Lines.pathEdges(this);
1707
+ } catch (e) {
1708
+ this.initFromPrior(this.prior);
1709
+ throw e;
1710
+ } finally {
1711
+ this.endMutate();
1552
1712
  }
1553
1713
  }
1554
1714
  applyChanges() {
1555
1715
  for (const edge of this.changes.removedEdges)
1556
- this.getEdge(Edge.id(edge)).delSelf(this);
1716
+ Edge.del(this, edge);
1557
1717
  for (const node of this.changes.removedNodes)
1558
- this.getNode(node.id).delSelf(this);
1718
+ Node.del(this, node);
1559
1719
  for (const node of this.changes.addedNodes)
1560
1720
  Node.addNormal(this, node);
1561
1721
  for (const edge of this.changes.addedEdges)
1562
1722
  Edge.add(this, edge);
1563
1723
  for (const node of this.changes.updatedNodes)
1564
- this.dirtyNodes.add(node.id);
1724
+ Node.update(this, node);
1725
+ for (const edge of this.changes.updatedEdges)
1726
+ Edge.update(this, edge);
1565
1727
  }
1566
1728
  layerAt(index) {
1567
1729
  while (index >= this.layerList.size)
@@ -1638,24 +1800,24 @@ var Graph = class _Graph {
1638
1800
  nodes.forEach((node) => mutator.addNode(node));
1639
1801
  });
1640
1802
  }
1641
- addEdges(...edges) {
1803
+ removeNodes(...nodes) {
1642
1804
  return this.withMutations((mutator) => {
1643
- edges.forEach((edge) => mutator.addEdge(edge));
1805
+ nodes.forEach((node) => mutator.removeNode(node));
1644
1806
  });
1645
1807
  }
1646
- addEdge(edge) {
1808
+ removeNode(node) {
1647
1809
  return this.withMutations((mutator) => {
1648
- mutator.addEdge(edge);
1810
+ mutator.removeNode(node);
1649
1811
  });
1650
1812
  }
1651
- removeNodes(...nodes) {
1813
+ addEdges(...edges) {
1652
1814
  return this.withMutations((mutator) => {
1653
- nodes.forEach((node) => mutator.removeNode(node));
1815
+ edges.forEach((edge) => mutator.addEdge(edge));
1654
1816
  });
1655
1817
  }
1656
- removeNode(node) {
1818
+ addEdge(edge) {
1657
1819
  return this.withMutations((mutator) => {
1658
- mutator.removeNode(node);
1820
+ mutator.addEdge(edge);
1659
1821
  });
1660
1822
  }
1661
1823
  removeEdges(...edges) {
@@ -1705,6 +1867,14 @@ var Graph = class _Graph {
1705
1867
  this.nextLayerId = prior?.nextLayerId ?? 0;
1706
1868
  this.nextDummyId = prior?.nextDummyId ?? 0;
1707
1869
  this.prior = prior;
1870
+ this.dirtyNodes = /* @__PURE__ */ new Set();
1871
+ this.dirtyEdges = /* @__PURE__ */ new Set();
1872
+ this.dirtyLayers = /* @__PURE__ */ new Set();
1873
+ this.dirtySegs = /* @__PURE__ */ new Set();
1874
+ this.delNodes = /* @__PURE__ */ new Set();
1875
+ this.delEdges = /* @__PURE__ */ new Set();
1876
+ this.delSegs = /* @__PURE__ */ new Set();
1877
+ this.dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.updatedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1708
1878
  }
1709
1879
  beginMutate() {
1710
1880
  this.nodes = this.nodes.asMutable();
@@ -1734,15 +1904,20 @@ var Graph = class _Graph {
1734
1904
  }
1735
1905
  };
1736
1906
 
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}";
1907
+ // src/common.ts
1908
+ var screenPos = (x, y) => ({ x, y });
1909
+ var canvasPos = (x, y) => ({ x, y });
1910
+ var graphPos = (x, y) => ({ x, y });
1911
+
1912
+ // src/canvas/node.tsx
1913
+ var import_node3 = __toESM(require("./node.css?raw"), 1);
1739
1914
 
1740
1915
  // src/canvas/styler.ts
1741
1916
  var injected = {};
1742
- function styler(name, styles, prefix) {
1917
+ function styler(name, styles4, prefix) {
1743
1918
  if (prefix === "g3p" && !injected[name]) {
1744
1919
  const style = document.createElement("style");
1745
- style.textContent = styles;
1920
+ style.textContent = styles4;
1746
1921
  document.head.appendChild(style);
1747
1922
  injected[name] = true;
1748
1923
  }
@@ -1754,98 +1929,106 @@ function styler(name, styles, prefix) {
1754
1929
  };
1755
1930
  }
1756
1931
 
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}";
1759
-
1760
1932
  // src/canvas/node.tsx
1761
1933
  var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
1934
+ var log8 = logger("canvas");
1762
1935
  var Node2 = class {
1763
1936
  selected;
1764
1937
  hovered;
1765
1938
  container;
1766
- dims;
1767
1939
  content;
1768
- measured;
1940
+ canvas;
1941
+ data;
1942
+ classPrefix;
1769
1943
  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;
1944
+ pos;
1945
+ constructor(canvas, data, isDummy = false) {
1946
+ this.canvas = canvas;
1947
+ this.data = data;
1948
+ this.selected = false;
1949
+ this.hovered = false;
1950
+ this.classPrefix = canvas.classPrefix;
1951
+ this.isDummy = isDummy;
1952
+ if (this.isDummy) {
1953
+ const size = canvas.dummyNodeSize;
1788
1954
  } else {
1789
- this.measured = true;
1955
+ const render = data.render ?? canvas.renderNode;
1956
+ this.content = this.renderContent(render(data.data));
1790
1957
  }
1791
1958
  }
1792
- getSize() {
1793
- const rect = this.content.getBoundingClientRect();
1794
- this.dims = { w: rect.width, h: rect.height };
1795
- this.measured = true;
1959
+ remove() {
1960
+ this.container.remove();
1961
+ }
1962
+ append() {
1963
+ console.log("append", this);
1964
+ this.canvas.group.appendChild(this.container);
1965
+ }
1966
+ needsContentSize() {
1967
+ return !this.isDummy && this.content instanceof HTMLElement;
1968
+ }
1969
+ needsContainerSize() {
1970
+ return !this.isDummy;
1796
1971
  }
1797
1972
  handleClick(e) {
1798
1973
  e.stopPropagation();
1799
- this.onClick?.(this.data, e);
1800
1974
  }
1801
1975
  handleMouseEnter(e) {
1802
- this.onMouseEnter?.(this.data, e);
1803
1976
  }
1804
1977
  handleMouseLeave(e) {
1805
- this.onMouseLeave?.(this.data, e);
1806
1978
  }
1807
1979
  handleContextMenu(e) {
1808
- if (this.onContextMenu) {
1809
- e.stopPropagation();
1810
- this.onContextMenu(this.data, e);
1811
- }
1812
1980
  }
1813
1981
  handleMouseDown(e) {
1814
- this.onMouseDown?.(this.data, e);
1815
1982
  }
1816
1983
  handleMouseUp(e) {
1817
- this.onMouseUp?.(this.data, e);
1818
1984
  }
1819
1985
  setPos(pos) {
1820
- console.log(`setPos:`, this, pos);
1821
1986
  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)(
1987
+ const { x, y } = pos;
1988
+ this.container.setAttribute("transform", `translate(${x}, ${y})`);
1989
+ }
1990
+ hasPorts() {
1991
+ return !!this.data?.ports?.in?.length || !!this.data?.ports?.out?.length;
1992
+ }
1993
+ renderContent(el) {
1994
+ const hasPorts = this.hasPorts();
1995
+ el = this.renderBorder(el);
1996
+ if (hasPorts)
1997
+ el = this.renderOutsidePorts(el);
1998
+ return el;
1999
+ }
2000
+ renderContainer() {
2001
+ const c = styler("node", import_node3.default, this.classPrefix);
2002
+ const hasPorts = this.hasPorts();
2003
+ const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
2004
+ const nodeType = this.data?.type;
2005
+ const typeClass = nodeType ? `g3p-node-type-${nodeType}` : "";
2006
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1828
2007
  "g",
1829
2008
  {
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),
2009
+ className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
2010
+ onClick: (e) => this.handleClick(e),
2011
+ onMouseEnter: (e) => this.handleMouseEnter(e),
2012
+ onMouseLeave: (e) => this.handleMouseLeave(e),
2013
+ onContextMenu: (e) => this.handleContextMenu(e),
2014
+ onMouseDown: (e) => this.handleMouseDown(e),
2015
+ onMouseUp: (e) => this.handleMouseUp(e),
1838
2016
  style: { cursor: "pointer" },
1839
- children: this.isDummy ? this.renderDummy() : this.renderContent()
2017
+ children: inner
1840
2018
  }
1841
2019
  );
1842
2020
  }
2021
+ renderForeign() {
2022
+ const { w, h } = this.data.dims;
2023
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
2024
+ }
1843
2025
  renderDummy() {
1844
- const c = styler("node", node_default, this.classPrefix);
1845
- let { w, h } = this.dims;
2026
+ const c = styler("node", import_node3.default, this.classPrefix);
2027
+ let w = this.canvas.dummyNodeSize;
2028
+ let h = this.canvas.dummyNodeSize;
1846
2029
  w /= 2;
1847
2030
  h /= 2;
1848
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2031
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
1849
2032
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1850
2033
  "ellipse",
1851
2034
  {
@@ -1864,157 +2047,190 @@ var Node2 = class {
1864
2047
  rx: w,
1865
2048
  ry: h,
1866
2049
  fill: "none",
1867
- className: c("border"),
1868
- strokeWidth: "2"
2050
+ className: c("border")
1869
2051
  }
1870
2052
  )
1871
2053
  ] });
1872
2054
  }
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
- )
2055
+ measure(isVertical) {
2056
+ const rect = this.content.getBoundingClientRect();
2057
+ const data = this.data;
2058
+ data.dims = { w: rect.width, h: rect.height };
2059
+ for (const dir of ["in", "out"]) {
2060
+ const ports = data.ports?.[dir];
2061
+ if (!ports) continue;
2062
+ for (const port of ports) {
2063
+ const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2064
+ if (!el) continue;
2065
+ const portRect = el.getBoundingClientRect();
2066
+ if (isVertical) {
2067
+ port.offset = portRect.left - rect.left;
2068
+ port.size = portRect.width;
2069
+ } else {
2070
+ port.offset = portRect.top - rect.top;
2071
+ port.size = portRect.height;
1917
2072
  }
1918
- )
2073
+ }
2074
+ }
2075
+ }
2076
+ getPortPosition(dir) {
2077
+ const o = this.canvas.orientation;
2078
+ if (dir === "in") {
2079
+ if (o === "TB") return "top";
2080
+ if (o === "BT") return "bottom";
2081
+ if (o === "LR") return "left";
2082
+ return "right";
2083
+ } else {
2084
+ if (o === "TB") return "bottom";
2085
+ if (o === "BT") return "top";
2086
+ if (o === "LR") return "right";
2087
+ return "left";
2088
+ }
2089
+ }
2090
+ isVerticalOrientation() {
2091
+ const o = this.canvas.orientation;
2092
+ return o === "TB" || o === "BT";
2093
+ }
2094
+ isReversedOrientation() {
2095
+ const o = this.canvas.orientation;
2096
+ return o === "BT" || o === "RL";
2097
+ }
2098
+ renderPortRow(dir, inout) {
2099
+ const ports = this.data?.ports?.[dir];
2100
+ if (!ports?.length) return null;
2101
+ const c = styler("node", import_node3.default, this.classPrefix);
2102
+ const pos = this.getPortPosition(dir);
2103
+ const isVertical = this.isVerticalOrientation();
2104
+ const layoutClass = isVertical ? "row" : "col";
2105
+ const rotateLabels = false;
2106
+ const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2107
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2108
+ "div",
2109
+ {
2110
+ id: `g3p-port-${this.data.id}-${port.id}`,
2111
+ className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2112
+ children: port.label ?? port.id
2113
+ }
2114
+ )) });
2115
+ }
2116
+ renderInsidePorts(el) {
2117
+ const c = styler("node", import_node3.default, this.classPrefix);
2118
+ const isVertical = this.isVerticalOrientation();
2119
+ const isReversed = this.isReversedOrientation();
2120
+ let inPorts = this.renderPortRow("in", "in");
2121
+ let outPorts = this.renderPortRow("out", "in");
2122
+ if (!inPorts && !outPorts) return el;
2123
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2124
+ const wrapperClass = isVertical ? "v" : "h";
2125
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2126
+ inPorts,
2127
+ el,
2128
+ outPorts
1919
2129
  ] });
1920
2130
  }
2131
+ renderOutsidePorts(el) {
2132
+ const c = styler("node", import_node3.default, this.classPrefix);
2133
+ const isVertical = this.isVerticalOrientation();
2134
+ const isReversed = this.isReversedOrientation();
2135
+ let inPorts = this.renderPortRow("in", "out");
2136
+ let outPorts = this.renderPortRow("out", "out");
2137
+ if (!inPorts && !outPorts) return el;
2138
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2139
+ const wrapperClass = isVertical ? "v" : "h";
2140
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2141
+ inPorts,
2142
+ el,
2143
+ outPorts
2144
+ ] });
2145
+ }
2146
+ renderBorder(el) {
2147
+ const c = styler("node", import_node3.default, this.classPrefix);
2148
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: c("border"), children: el });
2149
+ }
1921
2150
  };
1922
2151
 
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
2152
  // src/canvas/seg.tsx
2153
+ var import_seg3 = __toESM(require("./seg.css?raw"), 1);
1927
2154
  var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
2155
+ var log9 = logger("canvas");
1928
2156
  var Seg2 = class {
2157
+ id;
1929
2158
  selected;
1930
2159
  hovered;
1931
- constructor(options) {
2160
+ canvas;
2161
+ classPrefix;
2162
+ type;
2163
+ svg;
2164
+ el;
2165
+ source;
2166
+ target;
2167
+ constructor(canvas, data, g) {
2168
+ this.id = data.id;
2169
+ this.canvas = canvas;
1932
2170
  this.selected = false;
1933
2171
  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";
2172
+ this.svg = data.svg;
2173
+ this.classPrefix = canvas.classPrefix;
2174
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2175
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2176
+ this.type = data.type;
2177
+ this.el = this.render();
1948
2178
  }
1949
2179
  handleClick(e) {
1950
2180
  e.stopPropagation();
1951
- this.onClick?.(this.edgeData, e);
1952
2181
  }
1953
2182
  handleMouseEnter(e) {
1954
- this.onMouseEnter?.(this.edgeData, e);
1955
2183
  }
1956
2184
  handleMouseLeave(e) {
1957
- this.onMouseLeave?.(this.edgeData, e);
1958
2185
  }
1959
2186
  handleContextMenu(e) {
1960
- if (this.onContextMenu) {
1961
- e.stopPropagation();
1962
- this.onContextMenu(this.edgeData, e);
1963
- }
1964
2187
  }
1965
- renderTerminals() {
1966
- return {
1967
- source: this.renderTerminal(this.attrs.sourceTerminal, "source"),
1968
- target: this.renderTerminal(this.attrs.targetTerminal, "target")
1969
- };
2188
+ append() {
2189
+ this.canvas.group.appendChild(this.el);
2190
+ }
2191
+ remove() {
2192
+ this.el.remove();
1970
2193
  }
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);
2194
+ update(data) {
2195
+ this.svg = data.svg;
2196
+ this.type = data.type;
2197
+ this.source = data.source;
2198
+ this.target = data.target;
2199
+ this.remove();
2200
+ this.el = this.render();
2201
+ this.append();
1976
2202
  }
1977
2203
  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();
2204
+ const c = styler("seg", import_seg3.default, this.classPrefix);
2205
+ let { source, target } = normalize(this);
2206
+ if (this.source.isDummy) source = void 0;
2207
+ if (this.target.isDummy) target = void 0;
2208
+ const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
1989
2209
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1990
2210
  "g",
1991
2211
  {
1992
2212
  ref: (el) => this.el = el,
1993
- id: `g3p-seg-${this.segId}`,
1994
- className: c("container"),
2213
+ id: `g3p-seg-${this.id}`,
2214
+ className: `${c("container")} ${typeClass}`.trim(),
1995
2215
  onClick: this.handleClick.bind(this),
1996
2216
  onMouseEnter: this.handleMouseEnter.bind(this),
1997
2217
  onMouseLeave: this.handleMouseLeave.bind(this),
1998
2218
  onContextMenu: this.handleContextMenu.bind(this),
1999
2219
  children: [
2000
- source?.defs,
2001
- target?.defs,
2002
2220
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2003
2221
  "path",
2004
2222
  {
2005
2223
  d: this.svg,
2006
- ...styleAttrs,
2007
2224
  fill: "none",
2008
2225
  className: c("line"),
2009
- markerStart: source ? `url(#${source.id})` : void 0,
2010
- markerEnd: target ? `url(#${target.id})` : void 0
2226
+ markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2227
+ markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2011
2228
  }
2012
2229
  ),
2013
2230
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2014
2231
  "path",
2015
2232
  {
2016
2233
  d: this.svg,
2017
- ...hoverAttrs,
2018
2234
  stroke: "transparent",
2019
2235
  fill: "none",
2020
2236
  className: c("hitbox"),
@@ -2025,29 +2241,48 @@ var Seg2 = class {
2025
2241
  }
2026
2242
  );
2027
2243
  }
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
2244
  };
2048
2245
 
2049
2246
  // src/canvas/canvas.tsx
2247
+ var import_canvas = __toESM(require("./canvas.css?raw"), 1);
2248
+ var import_zoom = __toESM(require("./zoom.css?raw"), 1);
2050
2249
  var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2250
+ var log10 = logger("canvas");
2251
+ var themeVarMap = {
2252
+ // Canvas
2253
+ bg: "--g3p-bg",
2254
+ shadow: "--g3p-shadow",
2255
+ // Node
2256
+ border: "--g3p-border",
2257
+ borderHover: "--g3p-border-hover",
2258
+ borderSelected: "--g3p-border-selected",
2259
+ text: "--g3p-text",
2260
+ textMuted: "--g3p-text-muted",
2261
+ // Port
2262
+ bgHover: "--g3p-port-bg-hover",
2263
+ // Edge
2264
+ color: "--g3p-edge-color"
2265
+ };
2266
+ function themeToCSS(theme, selector, prefix = "") {
2267
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2268
+ if (!entries.length) return "";
2269
+ let css = `${selector} {
2270
+ `;
2271
+ for (const [key, value] of entries) {
2272
+ let cssVar = themeVarMap[key];
2273
+ if (key === "bg" && prefix === "node") {
2274
+ cssVar = "--g3p-bg-node";
2275
+ } else if (key === "bg" && prefix === "port") {
2276
+ cssVar = "--g3p-port-bg";
2277
+ }
2278
+ if (cssVar) {
2279
+ css += ` ${cssVar}: ${value};
2280
+ `;
2281
+ }
2282
+ }
2283
+ css += "}\n";
2284
+ return css;
2285
+ }
2051
2286
  var Canvas = class {
2052
2287
  container;
2053
2288
  root;
@@ -2055,26 +2290,27 @@ var Canvas = class {
2055
2290
  transform;
2056
2291
  bounds;
2057
2292
  measurement;
2058
- nodes;
2059
- segs;
2293
+ allNodes;
2294
+ curNodes;
2295
+ curSegs;
2060
2296
  updating;
2297
+ // Pan-zoom state
2298
+ isPanning = false;
2299
+ panStart = null;
2300
+ transformStart = null;
2301
+ panScale = null;
2302
+ zoomControls;
2061
2303
  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();
2304
+ Object.assign(this, options);
2305
+ this.allNodes = /* @__PURE__ */ new Map();
2306
+ this.curNodes = /* @__PURE__ */ new Map();
2307
+ this.curSegs = /* @__PURE__ */ new Map();
2076
2308
  this.updating = false;
2309
+ this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2310
+ this.transform = { x: 0, y: 0, scale: 1 };
2077
2311
  this.createMeasurementContainer();
2312
+ this.createCanvasContainer();
2313
+ if (this.panZoom) this.setupPanZoom();
2078
2314
  }
2079
2315
  createMeasurementContainer() {
2080
2316
  this.measurement = document.createElement("div");
@@ -2087,85 +2323,94 @@ var Canvas = class {
2087
2323
  `;
2088
2324
  document.body.appendChild(this.measurement);
2089
2325
  }
2090
- update(callback) {
2091
- this.updating = true;
2092
- callback();
2093
- this.updating = false;
2326
+ getNode(key) {
2327
+ const node = this.allNodes.get(key);
2328
+ if (!node) throw new Error(`node not found: ${key}`);
2329
+ return node;
2330
+ }
2331
+ update() {
2094
2332
  let bx0 = Infinity, by0 = Infinity;
2095
2333
  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;
2334
+ for (const node of this.curNodes.values()) {
2335
+ const { x, y } = node.pos;
2336
+ const { w, h } = node.data.dims;
2337
+ const nx0 = x, nx1 = x + w;
2338
+ const ny0 = y, ny1 = y + h;
2099
2339
  bx0 = Math.min(bx0, nx0);
2100
2340
  by0 = Math.min(by0, ny0);
2101
2341
  bx1 = Math.max(bx1, nx1);
2102
2342
  by1 = Math.max(by1, ny1);
2103
2343
  }
2104
2344
  this.bounds = { min: { x: bx0, y: by0 }, max: { x: bx1, y: by1 } };
2105
- console.log("bounds", this.bounds);
2106
2345
  this.root.setAttribute("viewBox", this.viewBox());
2107
2346
  }
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);
2347
+ addNode(gnode) {
2348
+ if (this.curNodes.has(gnode.id))
2349
+ throw new Error("node already exists");
2350
+ const { key } = gnode;
2351
+ let node;
2352
+ if (gnode.isDummy) {
2353
+ node = new Node2(this, gnode, true);
2354
+ node.renderContainer();
2355
+ node.setPos(gnode.pos);
2356
+ this.allNodes.set(key, node);
2357
+ } else {
2358
+ if (!this.allNodes.has(key))
2359
+ throw new Error("node has not been measured");
2360
+ node = this.getNode(key);
2361
+ }
2362
+ this.curNodes.set(gnode.id, node);
2363
+ node.append();
2364
+ }
2365
+ updateNode(gnode) {
2366
+ if (gnode.isDummy) throw new Error("dummy node cannot be updated");
2367
+ const node = this.getNode(gnode.key);
2368
+ const cur = this.curNodes.get(gnode.id);
2369
+ if (cur) cur.remove();
2370
+ this.curNodes.set(gnode.id, node);
2371
+ node.append();
2372
+ }
2373
+ deleteNode(gnode) {
2374
+ const node = this.getNode(gnode.key);
2375
+ this.curNodes.delete(gnode.id);
2376
+ node.remove();
2377
+ }
2378
+ addSeg(gseg, g) {
2379
+ if (this.curSegs.has(gseg.id))
2380
+ throw new Error("seg already exists");
2381
+ const seg = new Seg2(this, gseg, g);
2382
+ this.curSegs.set(gseg.id, seg);
2383
+ seg.append();
2384
+ }
2385
+ updateSeg(gseg) {
2386
+ const seg = this.curSegs.get(gseg.id);
2133
2387
  if (!seg) throw new Error("seg not found");
2134
- seg.setSVG(opts.svg);
2388
+ seg.update(gseg);
2135
2389
  }
2136
- deleteSeg(opts) {
2137
- const seg = this.segs.get(opts.segId);
2390
+ deleteSeg(gseg) {
2391
+ const seg = this.curSegs.get(gseg.id);
2138
2392
  if (!seg) throw new Error("seg not found");
2139
- seg.el.remove();
2140
- this.segs.delete(seg.segId);
2393
+ this.curSegs.delete(gseg.id);
2394
+ seg.remove();
2141
2395
  }
2142
- async measure(nodes) {
2143
- const newNodes = [];
2396
+ async measureNodes(nodes) {
2397
+ const newNodes = /* @__PURE__ */ new Map();
2144
2398
  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
- }
2399
+ const node = new Node2(this, data);
2400
+ newNodes.set(data.data, node);
2401
+ this.measurement.appendChild(node.content);
2157
2402
  }
2158
- return new Promise((resolve) => {
2159
- requestAnimationFrame(() => {
2160
- for (const node of newNodes)
2161
- node.getSize();
2162
- this.measurement.textContent = "";
2163
- resolve();
2164
- });
2165
- });
2166
- }
2167
- getDims(node) {
2168
- return this.nodes.get(node).dims;
2403
+ await new Promise(requestAnimationFrame);
2404
+ const isVertical = this.orientation === "TB" || this.orientation === "BT";
2405
+ for (const node of newNodes.values()) {
2406
+ node.measure(isVertical);
2407
+ const { id, version } = node.data;
2408
+ const key = `${id}:${version}`;
2409
+ this.allNodes.set(key, node);
2410
+ node.renderContainer();
2411
+ }
2412
+ this.measurement.innerHTML = "";
2413
+ return newNodes;
2169
2414
  }
2170
2415
  onClick(e) {
2171
2416
  console.log("click", e);
@@ -2177,17 +2422,47 @@ var Canvas = class {
2177
2422
  return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2178
2423
  }
2179
2424
  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}`;
2181
- }
2182
- render() {
2183
- const c = styler("canvas", canvas_default, this.classPrefix);
2184
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2425
+ const p = this.padding;
2426
+ const x = this.bounds.min.x - p;
2427
+ const y = this.bounds.min.y - p;
2428
+ const w = this.bounds.max.x - this.bounds.min.x + p * 2;
2429
+ const h = this.bounds.max.y - this.bounds.min.y + p * 2;
2430
+ return `${x} ${y} ${w} ${h}`;
2431
+ }
2432
+ generateDynamicStyles() {
2433
+ let css = "";
2434
+ const prefix = this.classPrefix;
2435
+ css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
2436
+ for (const [type, vars] of Object.entries(this.nodeTypes)) {
2437
+ css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
2438
+ }
2439
+ for (const [type, vars] of Object.entries(this.edgeTypes)) {
2440
+ css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
2441
+ }
2442
+ return css;
2443
+ }
2444
+ createCanvasContainer() {
2445
+ const markerStyleEl = document.createElement("style");
2446
+ markerStyleEl.textContent = import_marker.default;
2447
+ document.head.appendChild(markerStyleEl);
2448
+ const zoomStyleEl = document.createElement("style");
2449
+ zoomStyleEl.textContent = import_zoom.default;
2450
+ document.head.appendChild(zoomStyleEl);
2451
+ const dynamicStyles = this.generateDynamicStyles();
2452
+ if (dynamicStyles) {
2453
+ const themeStyleEl = document.createElement("style");
2454
+ themeStyleEl.textContent = dynamicStyles;
2455
+ document.head.appendChild(themeStyleEl);
2456
+ }
2457
+ const c = styler("canvas", import_canvas.default, this.classPrefix);
2458
+ const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2459
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2185
2460
  "div",
2186
2461
  {
2187
- className: c("container"),
2462
+ className: `${c("container")} ${colorModeClass}`.trim(),
2188
2463
  ref: (el) => this.container = el,
2189
2464
  onContextMenu: this.onContextMenu.bind(this),
2190
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2465
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2191
2466
  "svg",
2192
2467
  {
2193
2468
  ref: (el) => this.root = el,
@@ -2197,70 +2472,308 @@ var Canvas = class {
2197
2472
  viewBox: this.viewBox(),
2198
2473
  preserveAspectRatio: "xMidYMid meet",
2199
2474
  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
- )
2475
+ children: [
2476
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2477
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2478
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
2479
+ ] }),
2480
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2481
+ "g",
2482
+ {
2483
+ ref: (el) => this.group = el,
2484
+ transform: this.groupTransform()
2485
+ }
2486
+ )
2487
+ ]
2207
2488
  }
2208
2489
  )
2209
2490
  }
2210
2491
  );
2211
2492
  }
2493
+ // ==================== Pan-Zoom ====================
2494
+ setupPanZoom() {
2495
+ this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false });
2496
+ this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2497
+ document.addEventListener("mousemove", this.onMouseMove.bind(this));
2498
+ document.addEventListener("mouseup", this.onMouseUp.bind(this));
2499
+ this.createZoomControls();
2500
+ }
2501
+ /** Convert screen coordinates to canvas-relative coordinates */
2502
+ screenToCanvas(screen) {
2503
+ const rect = this.container.getBoundingClientRect();
2504
+ return canvasPos(screen.x - rect.left, screen.y - rect.top);
2505
+ }
2506
+ /**
2507
+ * Get the effective scale from canvas pixels to graph units,
2508
+ * accounting for preserveAspectRatio="xMidYMid meet" which uses
2509
+ * the smaller scale (to fit) and centers the content.
2510
+ */
2511
+ getEffectiveScale() {
2512
+ const vb = this.currentViewBox();
2513
+ const rect = this.container.getBoundingClientRect();
2514
+ const scaleX = vb.w / rect.width;
2515
+ const scaleY = vb.h / rect.height;
2516
+ const scale = Math.max(scaleX, scaleY);
2517
+ const actualW = rect.width * scale;
2518
+ const actualH = rect.height * scale;
2519
+ const offsetX = (actualW - vb.w) / 2;
2520
+ const offsetY = (actualH - vb.h) / 2;
2521
+ return { scale, offsetX, offsetY };
2522
+ }
2523
+ /** Convert canvas coordinates to graph coordinates */
2524
+ canvasToGraph(canvas) {
2525
+ const vb = this.currentViewBox();
2526
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
2527
+ return graphPos(
2528
+ vb.x - offsetX + canvas.x * scale,
2529
+ vb.y - offsetY + canvas.y * scale
2530
+ );
2531
+ }
2532
+ /** Get current viewBox as an object */
2533
+ currentViewBox() {
2534
+ const p = this.padding;
2535
+ const t = this.transform;
2536
+ const baseX = this.bounds.min.x - p;
2537
+ const baseY = this.bounds.min.y - p;
2538
+ const baseW = this.bounds.max.x - this.bounds.min.x + p * 2;
2539
+ const baseH = this.bounds.max.y - this.bounds.min.y + p * 2;
2540
+ const cx = baseX + baseW / 2;
2541
+ const cy = baseY + baseH / 2;
2542
+ const w = baseW / t.scale;
2543
+ const h = baseH / t.scale;
2544
+ const x = cx - w / 2 - t.x;
2545
+ const y = cy - h / 2 - t.y;
2546
+ return { x, y, w, h };
2547
+ }
2548
+ onWheel(e) {
2549
+ e.preventDefault();
2550
+ const zoomFactor = 1.1;
2551
+ const delta = e.deltaY > 0 ? 1 / zoomFactor : zoomFactor;
2552
+ const screenCursor = screenPos(e.clientX, e.clientY);
2553
+ const canvasCursor = this.screenToCanvas(screenCursor);
2554
+ const graphCursor = this.canvasToGraph(canvasCursor);
2555
+ const oldScale = this.transform.scale;
2556
+ const newScale = Math.max(0.1, Math.min(10, oldScale * delta));
2557
+ this.transform.scale = newScale;
2558
+ const newGraphCursor = this.canvasToGraph(canvasCursor);
2559
+ this.transform.x += newGraphCursor.x - graphCursor.x;
2560
+ this.transform.y += newGraphCursor.y - graphCursor.y;
2561
+ this.applyTransform();
2562
+ }
2563
+ onMouseDown(e) {
2564
+ if (e.button !== 0) return;
2565
+ if (e.target.closest(".g3p-zoom-controls")) return;
2566
+ this.isPanning = true;
2567
+ this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2568
+ this.transformStart = { ...this.transform };
2569
+ const { scale } = this.getEffectiveScale();
2570
+ this.panScale = { x: scale, y: scale };
2571
+ this.container.style.cursor = "grabbing";
2572
+ e.preventDefault();
2573
+ }
2574
+ onMouseMove(e) {
2575
+ if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
2576
+ const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2577
+ const dx = current.x - this.panStart.x;
2578
+ const dy = current.y - this.panStart.y;
2579
+ this.transform.x = this.transformStart.x + dx * this.panScale.x;
2580
+ this.transform.y = this.transformStart.y + dy * this.panScale.y;
2581
+ this.applyTransform();
2582
+ }
2583
+ onMouseUp(e) {
2584
+ if (!this.isPanning) return;
2585
+ this.isPanning = false;
2586
+ this.panStart = null;
2587
+ this.transformStart = null;
2588
+ this.panScale = null;
2589
+ this.container.style.cursor = "";
2590
+ }
2591
+ applyTransform() {
2592
+ const vb = this.currentViewBox();
2593
+ this.root.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
2594
+ this.updateZoomLevel();
2595
+ }
2596
+ createZoomControls() {
2597
+ const c = styler("zoom", import_zoom.default, this.classPrefix);
2598
+ this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: c("controls"), children: [
2599
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2602
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
2603
+ ] });
2604
+ this.container.appendChild(this.zoomControls);
2605
+ }
2606
+ updateZoomLevel() {
2607
+ const level = this.container.querySelector("#g3p-zoom-level");
2608
+ if (level) {
2609
+ level.textContent = `${Math.round(this.transform.scale * 100)}%`;
2610
+ }
2611
+ }
2612
+ zoomIn() {
2613
+ this.transform.scale = Math.min(10, this.transform.scale * 1.2);
2614
+ this.applyTransform();
2615
+ }
2616
+ zoomOut() {
2617
+ this.transform.scale = Math.max(0.1, this.transform.scale / 1.2);
2618
+ this.applyTransform();
2619
+ }
2620
+ zoomReset() {
2621
+ this.transform = { x: 0, y: 0, scale: 1 };
2622
+ this.applyTransform();
2623
+ }
2212
2624
  };
2213
2625
 
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
- });
2626
+ // src/canvas/render-node.tsx
2627
+ var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2628
+ function renderNode(node) {
2629
+ if (typeof node == "string") node = { id: node };
2630
+ const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
2631
+ const detail = node?.detail ?? node?.description ?? node?.subtitle;
2632
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-node-default", children: [
2633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-title", children: title }),
2634
+ detail && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-detail", children: detail })
2635
+ ] });
2636
+ }
2637
+
2638
+ // src/api/defaults.ts
2639
+ function applyDefaults(options) {
2640
+ const { graph: graph2, canvas, props } = defaults();
2641
+ return {
2642
+ graph: { ...graph2, ...options?.graph },
2643
+ canvas: { ...canvas, ...options?.canvas },
2644
+ props: { ...props, ...options?.props }
2645
+ };
2646
+ }
2647
+ function defaults() {
2648
+ return {
2649
+ graph: {
2650
+ mergeOrder: ["target", "source"],
2651
+ nodeMargin: 15,
2652
+ dummyNodeSize: 15,
2653
+ nodeAlign: "natural",
2654
+ edgeSpacing: 10,
2655
+ turnRadius: 10,
2656
+ orientation: "TB",
2657
+ layerMargin: 5,
2658
+ alignIterations: 5,
2659
+ alignThreshold: 10,
2660
+ separateTrackSets: true,
2661
+ markerSize: 10,
2662
+ layoutSteps: null
2663
+ },
2664
+ canvas: {
2665
+ renderNode,
2666
+ classPrefix: "g3p",
2667
+ width: "100%",
2668
+ height: "100%",
2669
+ padding: 20,
2670
+ editable: false,
2671
+ panZoom: true,
2672
+ markerSize: 10,
2673
+ colorMode: "system",
2674
+ theme: {},
2675
+ nodeTypes: {},
2676
+ edgeTypes: {}
2677
+ },
2678
+ props: {}
2679
+ };
2680
+ }
2681
+
2682
+ // src/api/updater.ts
2683
+ var Updater = class _Updater {
2684
+ update;
2685
+ constructor() {
2686
+ this.update = {
2687
+ addNodes: [],
2688
+ removeNodes: [],
2689
+ updateNodes: [],
2690
+ addEdges: [],
2691
+ removeEdges: [],
2692
+ updateEdges: []
2693
+ };
2694
+ }
2695
+ describe(desc) {
2696
+ this.update.description = desc;
2697
+ return this;
2698
+ }
2699
+ addNode(node) {
2700
+ this.update.addNodes.push(node);
2701
+ return this;
2702
+ }
2703
+ deleteNode(node) {
2704
+ this.update.removeNodes.push(node);
2705
+ return this;
2706
+ }
2707
+ updateNode(node) {
2708
+ this.update.updateNodes.push(node);
2709
+ return this;
2710
+ }
2711
+ addEdge(edge) {
2712
+ this.update.addEdges.push(edge);
2713
+ return this;
2714
+ }
2715
+ deleteEdge(edge) {
2716
+ this.update.removeEdges.push(edge);
2717
+ return this;
2718
+ }
2719
+ updateEdge(edge) {
2720
+ this.update.updateEdges.push(edge);
2721
+ return this;
2722
+ }
2723
+ static add(nodes, edges) {
2724
+ const updater = new _Updater();
2725
+ updater.update.addNodes = nodes;
2726
+ updater.update.addEdges = edges;
2727
+ return updater;
2728
+ }
2729
+ };
2730
+
2731
+ // src/api/api.ts
2732
+ var log11 = logger("api");
2230
2733
  var API = class {
2231
2734
  state;
2232
2735
  seq;
2233
2736
  index;
2234
2737
  canvas;
2235
- _options;
2236
2738
  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;
2739
+ history;
2740
+ nodeIds;
2741
+ edgeIds;
2742
+ nodeVersions;
2743
+ nextNodeId;
2744
+ nextEdgeId;
2745
+ root;
2746
+ constructor(args) {
2747
+ this.root = args.root;
2748
+ this.options = applyDefaults(args.options);
2749
+ let graph2 = new Graph({ options: this.options.graph });
2750
+ this.state = { graph: graph2, update: null };
2257
2751
  this.seq = [this.state];
2258
2752
  this.index = 0;
2259
- this.canvas = new Canvas(this._options);
2260
- this.canvas.render();
2753
+ this.nodeIds = /* @__PURE__ */ new Map();
2754
+ this.edgeIds = /* @__PURE__ */ new Map();
2755
+ this.nodeVersions = /* @__PURE__ */ new Map();
2756
+ this.nextNodeId = 1;
2757
+ this.nextEdgeId = 1;
2758
+ this.canvas = new Canvas({
2759
+ ...this.options.canvas,
2760
+ dummyNodeSize: this.options.graph.dummyNodeSize,
2761
+ orientation: this.options.graph.orientation
2762
+ });
2763
+ if (args.history) {
2764
+ this.history = args.history;
2765
+ } else if (args.nodes) {
2766
+ this.history = [Updater.add(args.nodes, args.edges || []).update];
2767
+ } else {
2768
+ this.history = [];
2769
+ }
2261
2770
  }
2262
- render() {
2263
- return this.canvas.container;
2771
+ async init() {
2772
+ const root = document.getElementById(this.root);
2773
+ if (!root) throw new Error("root element not found");
2774
+ root.appendChild(this.canvas.container);
2775
+ for (const update of this.history)
2776
+ await this.applyUpdate(update);
2264
2777
  }
2265
2778
  nav(nav) {
2266
2779
  let newIndex;
@@ -2285,36 +2798,27 @@ var API = class {
2285
2798
  this.state = this.seq[this.index];
2286
2799
  }
2287
2800
  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);
2305
- }
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);
2312
- }
2313
- for (const newSeg of newState.segs.values()) {
2314
- if (!oldState.segs.has(newSeg.segId))
2315
- this.canvas.addSeg(newSeg);
2316
- }
2317
- });
2801
+ const oldGraph = this.seq[oldIndex].graph;
2802
+ const newGraph = this.seq[newIndex].graph;
2803
+ for (const oldNode of oldGraph.nodes.values()) {
2804
+ const newNode = newGraph.nodes.get(oldNode.id);
2805
+ if (!newNode) this.canvas.deleteNode(oldNode);
2806
+ }
2807
+ for (const newNode of newGraph.nodes.values()) {
2808
+ if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
2809
+ }
2810
+ for (const oldSeg of oldGraph.segs.values()) {
2811
+ const newSeg = newGraph.segs.get(oldSeg.id);
2812
+ if (!newSeg)
2813
+ this.canvas.deleteSeg(oldSeg);
2814
+ else if (oldSeg.svg != newSeg.svg)
2815
+ this.canvas.updateSeg(newSeg);
2816
+ }
2817
+ for (const newSeg of newGraph.segs.values()) {
2818
+ if (!oldGraph.segs.has(newSeg.id))
2819
+ this.canvas.addSeg(newSeg, newGraph);
2820
+ }
2821
+ this.canvas.update();
2318
2822
  }
2319
2823
  async addNode(node) {
2320
2824
  await this.update((update) => update.addNode(node));
@@ -2331,190 +2835,175 @@ var API = class {
2331
2835
  async deleteEdge(edge) {
2332
2836
  await this.update((update) => update.deleteEdge(edge));
2333
2837
  }
2334
- 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)
2838
+ async applyUpdate(update) {
2839
+ log11.info("applyUpdate", update);
2840
+ const nodes = await this.measureNodes(update);
2841
+ const graph2 = this.state.graph.withMutations((mut) => {
2842
+ for (const edge of update.removeEdges ?? [])
2843
+ this._removeEdge(edge, mut);
2844
+ for (const node of update.removeNodes ?? [])
2352
2845
  this._removeNode(node, mut);
2353
- for (const edge of update.addedEdges)
2846
+ for (const node of update.addNodes ?? [])
2847
+ this._addNode(nodes.get(node), mut);
2848
+ for (const node of update.updateNodes ?? [])
2849
+ this._updateNode(nodes.get(node), mut);
2850
+ for (const edge of update.addEdges ?? [])
2354
2851
  this._addEdge(edge, mut);
2355
- for (const edge of update.removedEdges)
2356
- this._removeEdge(edge, mut);
2852
+ for (const edge of update.updateEdges ?? [])
2853
+ this._updateEdge(edge, mut);
2357
2854
  });
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
- };
2855
+ this.state = { graph: graph2, update };
2856
+ this.setNodePositions();
2394
2857
  this.seq.splice(this.index + 1);
2395
2858
  this.seq.push(this.state);
2396
2859
  this.nav("last");
2397
2860
  }
2398
- async measureNodes(update) {
2399
- const nodes = update.updatedNodes.concat(update.addedNodes);
2400
- await this.canvas.measure(nodes);
2401
- }
2402
- getDims(node) {
2403
- return this.canvas.getDims(node);
2404
- }
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);
2414
- }
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 {
2861
+ setNodePositions() {
2862
+ const { graph: graph2 } = this.state;
2863
+ for (const nodeId of graph2.dirtyNodes) {
2864
+ const node = graph2.getNode(nodeId);
2865
+ if (!node.isDummy)
2866
+ this.canvas.getNode(node.key).setPos(node.pos);
2428
2867
  }
2429
2868
  }
2430
- _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) });
2869
+ async update(callback) {
2870
+ const updater = new Updater();
2871
+ callback(updater);
2872
+ await this.applyUpdate(updater.update);
2440
2873
  }
2441
- _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);
2874
+ async measureNodes(update) {
2875
+ const data = [];
2876
+ for (const set of [update.updateNodes, update.addNodes])
2877
+ for (const node of set ?? [])
2878
+ data.push(this.parseNode(node, true));
2879
+ return await this.canvas.measureNodes(data);
2880
+ }
2881
+ parseNode(data, bumpVersion = false) {
2882
+ const get = this.options.props.node;
2883
+ let props;
2884
+ if (get) props = get(data);
2885
+ else if (!data) throw new Error(`invalid node ${data}`);
2886
+ else if (typeof data == "string") props = { id: data };
2887
+ else if (typeof data == "object") props = data;
2888
+ else throw new Error(`invalid node ${data}`);
2889
+ let { id, title, text, type, render } = props;
2890
+ id ??= this.getNodeId(data);
2891
+ const ports = this.parsePorts(props.ports);
2892
+ let version = this.nodeVersions.get(data);
2893
+ if (!version) version = 1;
2894
+ else if (bumpVersion) version++;
2895
+ this.nodeVersions.set(data, version);
2896
+ return { id, data, ports, title, text, type, render, version };
2897
+ }
2898
+ parseEdge(data) {
2899
+ const get = this.options.props.edge;
2900
+ let props;
2901
+ if (get) props = get(data);
2902
+ else if (!data) throw new Error(`invalid edge ${data}`);
2903
+ else if (typeof data == "string") props = this.parseStringEdge(data);
2904
+ else if (typeof data == "object") props = data;
2905
+ else throw new Error(`invalid edge ${data}`);
2906
+ let { id, source, target, type } = props;
2907
+ id ??= this.getEdgeId(data);
2908
+ source = this.parseEdgeEnd(source);
2909
+ target = this.parseEdgeEnd(target);
2910
+ const edge = { id, source, target, type, data };
2911
+ return edge;
2447
2912
  }
2448
- _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);
2913
+ parseEdgeEnd(end) {
2914
+ if (!end) throw new Error(`edge has an undefined source or target`);
2915
+ if (typeof end == "string") return { id: end };
2916
+ if (typeof end == "object") {
2917
+ const keys = Object.keys(end);
2918
+ const pidx = keys.indexOf("port");
2919
+ if (pidx != -1) {
2920
+ if (typeof end.port != "string") return end;
2921
+ keys.splice(pidx, 1);
2922
+ }
2923
+ if (keys.length != 1) return end;
2924
+ if (keys[0] == "id") return end;
2925
+ if (keys[0] != "node") return end;
2926
+ const id = this.nodeIds.get(end.node);
2927
+ if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
2928
+ return { id, port: end.port };
2929
+ }
2930
+ throw new Error(`invalid edge end ${end}`);
2457
2931
  }
2458
- _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);
2932
+ parseStringEdge(str) {
2933
+ const [source, target] = str.split(/\s*(?::|-+>?)\s*/);
2934
+ return { source, target };
2465
2935
  }
2466
- _onOptionChange(prop) {
2936
+ parsePorts(ports) {
2937
+ const fixed = { in: null, out: null };
2938
+ for (const key of ["in", "out"]) {
2939
+ if (ports?.[key] && ports[key].length > 0)
2940
+ fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2941
+ }
2942
+ return fixed;
2467
2943
  }
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 = [];
2944
+ getNodeId(node) {
2945
+ let id = this.nodeIds.get(node);
2946
+ if (!id) {
2947
+ id = `n${this.nextNodeId++}`;
2948
+ this.nodeIds.set(node, id);
2949
+ }
2950
+ return id;
2484
2951
  }
2485
- describe(desc) {
2486
- this.desc = desc;
2952
+ getEdgeId(edge) {
2953
+ let id = this.edgeIds.get(edge);
2954
+ if (!id) {
2955
+ id = `e${this.nextEdgeId++}`;
2956
+ this.edgeIds.set(edge, id);
2957
+ }
2958
+ return id;
2487
2959
  }
2488
- addNode(node) {
2489
- this.addedNodes.push(node);
2960
+ _addNode(node, mut) {
2961
+ const { data, id: newId } = node.data;
2962
+ const oldId = this.nodeIds.get(data);
2963
+ console.log("addNode", node, oldId, newId);
2964
+ if (oldId && oldId != newId)
2965
+ throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2966
+ this.nodeIds.set(data, newId);
2967
+ mut.addNode(node.data);
2490
2968
  }
2491
- deleteNode(node) {
2492
- this.removedNodes.push(node);
2969
+ _removeNode(node, mut) {
2970
+ const id = this.nodeIds.get(node);
2971
+ if (!id) throw new Error(`removing node ${node} which does not exist`);
2972
+ mut.removeNode({ id });
2493
2973
  }
2494
- updateNode(node) {
2495
- this.updatedNodes.push(node);
2974
+ _updateNode(node, mut) {
2975
+ const { data, id: newId } = node.data;
2976
+ const oldId = this.nodeIds.get(data);
2977
+ if (!oldId) throw new Error(`updating unknown node ${node}`);
2978
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
2979
+ mut.updateNode(node.data);
2496
2980
  }
2497
- addEdge(edge) {
2498
- this.addedEdges.push(edge);
2981
+ _addEdge(edge, mut) {
2982
+ const data = this.parseEdge(edge);
2983
+ const id = this.edgeIds.get(edge);
2984
+ if (id && id != data.id)
2985
+ throw new Error(`edge id changed from ${id} to ${data.id}`);
2986
+ this.edgeIds.set(edge, data.id);
2987
+ mut.addEdge(data);
2499
2988
  }
2500
- deleteEdge(edge) {
2501
- this.removedEdges.push(edge);
2989
+ _removeEdge(edge, mut) {
2990
+ const id = this.edgeIds.get(edge);
2991
+ if (!id) throw new Error(`removing edge ${edge} which does not exist`);
2992
+ mut.removeEdge(this.parseEdge(edge));
2502
2993
  }
2503
- updateEdge(edge) {
2504
- this.updatedEdges.push(edge);
2994
+ _updateEdge(edge, mut) {
2995
+ const id = this.edgeIds.get(edge);
2996
+ if (!id) throw new Error(`updating unknown edge ${edge}`);
2997
+ const data = this.parseEdge(edge);
2998
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
2999
+ mut.updateEdge(data);
2505
3000
  }
2506
3001
  };
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
- }
3002
+
3003
+ // src/index.ts
3004
+ async function graph(args = { root: "app" }) {
3005
+ const api = new API(args);
3006
+ await api.init();
2518
3007
  return api;
2519
3008
  }
2520
3009
  var index_default = graph;